diff --git a/messenger/LICENCE b/messenger/LICENCE new file mode 100644 index 0000000..d3f4c4c --- /dev/null +++ b/messenger/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Sai Raj + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/messenger/README.md b/messenger/README.md new file mode 100644 index 0000000..3c4abe6 --- /dev/null +++ b/messenger/README.md @@ -0,0 +1,80 @@ +# FLO Messenger + +• Messenger is a blockchain-based decentralized messaging app that uses Bitcoin or FLO blockchain addresses as user identities. Instead of a centralized server, messages are encrypted and stored in the users' browsers. +• Bitcoin or FLO blockchain addresses can communicate with each other using a messaging interface +• Messenger comes with "Multisig" where users can create multi-sig addresses and using a messaging interface make transactions on the multi-sig. +• Switching browsers or devices won't bring back old messages. Remember to back up and import to access your messages in the new browser/device. That's the security of Messenger. + +#### Note: +Do not lose the private key. Copy and save it securely. Once a private key is lost, it cannot be recovered + +### Live URL for FLO Messenger: +*https://ranchimall.github.io/messenger/* + +## Messenger Architecture +### Product Pipelines +1. The core feature of the product is pipelines. A pipeline is created by invloking inbuilt models +2. Right now we have models for Multisig creation for Bitcoin and FLO Multisigs. +3. What is pipeline ? +• It has an ID +• It has model like TYPE_BTC_MULTISIG +• It has members like different Bitcoin IDs or FLO IDs +• It has an encryption key unique to the pipeline, and known just to members of that pipeline +4. A pipeline sends custom messages defined as per a model to an attached group +5. Pipeline ID could be a recipient of a message. Then every Bitcoin or FLO Address will get the message with the action needed for that pipeline +6. Details of the technical functions are available here- [Functions](docs/functions.md) + +## How to use Messenger +### General messaging +1. Go to the homepage of Messenger +2. Sign in using a Bitcoin or FLO blockchain private key +3. In case you don't have the private key, generate using + FLO Wallet (for FLO address and private key): https://ranchimall.github.io/flowallet/ + BTC Wallet (for Bitcoin address and private key): https://ranchimall.github.io/btcwallet/ +** Note: FLO address or FLO ID and private key can be created from Messenger's homepage as well +4. To start a new message or chat, click on the "New chat" button +5. Add a FLO ID or a Bitcoin address as a contact +6. Select the contact to start messaging +** Note: Until the receiver replies, the message is not encrypted. + +### Mail +1. Mail is similar to Messaging except the user can send messages to multiple FLO IDs or Bitcoin addresses at the same time +2. Go to "Mail" and enter the recipient's FLO or Bitcoin address +3. Separate multiple addresses with a comma +4. Type a mail and send + +### Multisig messaging +1. Go to "Multisig" on the homepage +2. To create a Bitcoin multisig, click on "BTC" +3. To create a FLO multisig, click on "FLO" +4. To add BTC or FLO addresses in the new multisig, select contacts that are to be added +5. Contacts have to be saved in advance before creating a multisig address +6. After selecting the contacts, click "Next" and give the multisig address a label name +7. Select the minimum number of signatures required for the multisig +8. Click "Create" and the multisig address will be created + +### Sending a multisig transaction +1. The user must have some balance in the multisig address +2. Go to "Multisig" and click on "init transaction" +3. Enter the receiver's BTC address for a Bitcoin multisig or FLO address for a FLO multisig +4. Enter the amount to be transferred +5. Multiple addresses can be added as receivers with different amounts for each address +6. Click on "Initiate" to initiate the transaction from the multisig address +7. Associated multisig owners will be notified of this transaction +8. Once the required number of signatures is approved, the transaction will take place from the multisig address + +### Requests +1. Multisig owners will get a notification under the "Request" tab for multisig transaction approvals +2. They can approve or deny a multisig transaction request + +# Messenger Documentation + +- [Product Overview](docs/product-overview.md) +- [Features](docs/features.md) +- [Usage](docs/usage.md) +- [Functions](docs/functions.md) +- [Technical Architecture](docs/technical-architecture.md) +- [Additional Resources](docs/additional-resources.md) +- [Changelog](docs/changelog.md) + + diff --git a/messenger/assets/add contact illustration.svg b/messenger/assets/add contact illustration.svg new file mode 100644 index 0000000..322fbd8 --- /dev/null +++ b/messenger/assets/add contact illustration.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/messenger/assets/corner.svg b/messenger/assets/corner.svg new file mode 100644 index 0000000..d8a0384 --- /dev/null +++ b/messenger/assets/corner.svg @@ -0,0 +1,3 @@ + + + diff --git a/messenger/assets/illustrations-2.ai b/messenger/assets/illustrations-2.ai new file mode 100644 index 0000000..1d5334a --- /dev/null +++ b/messenger/assets/illustrations-2.ai @@ -0,0 +1,2038 @@ +%PDF-1.6 % +1 0 obj <>/OCGs[33 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + illustrations-2 + + + Adobe Illustrator 24.3 (Windows) + 2021-02-14T20:31:57+06:30 + 2021-02-14T20:32:06+05:30 + 2021-02-14T20:32:06+05:30 + + + + 256 + 108 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAbAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FUo1 Hzd5c03TZdT1G+SzsYFjeaecNGFSZ1jSQhgDwLuBypT3xVEfp/RzbwXEd0k9vdI0lvPBWaN0SnJl aIOtBXriq9dY00sg9anqFFjZlZVZpQGRQxAXkwYHjWuKoLWvOXljRNKvdV1XUYrWw05BLeStUlEZ +CNwUM55sQF4g8u2KpbD+av5eS+Uk83rrluvl2R/SW/k5xj1K09P03VZA+32eNab9MVZJaXttd2s F1AxMVxGssXNWjYo4DLVHCupow2YAjviqRn8x/In1fU7hNdtJYtH4HVDDIJWg9VuKc1j5N8TbDbF UYvm3y81votwLv8AdeYmRdHPpyVnMkDXCjjx5J+6RmPMClN98VVn8xaLH5gi8vPdoNantnvorLfm 1vG4jaStONObU61+44qlGj/mj+XWsW97cab5k0+4h05DLfOLiNRDGtAZX5FaR1IHP7PviqJn/MDy Nb6TbavNr9gml3jMlpeG4i9KVkBLqjctygB5Dt3xVPkdJEWRCGRwGVh0IO4IxVvFXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqkGr+R9A1fTLvS79JZbC8VI5oVleM+kkiSGFXQrII 2MYBXl0qBSpxVV03yfoGk2NrYaLaR6VZ2fL0be0iiVAXKsWIZHq9UB5dffFUZb6NZwTxzJUmFeMQ YK3H92sZIYrz3WNa/FQ4qlOufl95b1vyxf8Alq/ikbTNRCCcI5WQGJ1kiZWH7SOitU1qftV3xVLt J/J3yJp2l6fpn1I3lppt5JqcMV0Q6vfS/wDHzIihEZ0WqoOPFR0XpRVlT6PpL3Ju3srdrosHM5iQ yFxwo3Kla/uY9/8AJXwGKsc0j8pPy30mPUorHQLVIdXX09QgkDTRSRhuQjEcpdEQNuEQBQe2KoGX 8kfy+l0O10d7Sf6vp73cmlTC5mE1kb0/vBbMG/dqoACLSgp41JVT1fI/l9fM9l5m4TNq9hZNpsEz zzODAxB+NWYhm2+0dz1apCkKpBo35DflJo1pqFpp/l6KO31WL6vfo81zN6kYdZAoMsrleLxqylSC CKjfFVeb8k/ytm0Sw0OTy/AdL015JLW35S7PNxMheTn6knPgvLmxrxFegxVmkEENvBHBAixQxKEi jQBVVVFFVQOgAxVfirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi qX65r+k6JZm71KcQx9EXq7nwRRuTgMgGjUanHhjxTNB5nrX5yalIK6RbxQRMfhaasktPGgIVflv8 8pOQvNaj2gmT+7AA89ynP5eeeNX1pLuHUJFkuLcq6OEVao9QRRQPslfxynLmlHk7XsPXS1AkMn1R +5nttciYEEUcdRl2HMJ+93U4UrZewdirsVdiqUeZHdLWIqxU+p1Bp2OYesJER73O0ABkb7mPfWJ/ 9+N/wRzX8Z73a8Ee531if/fjf8EceM968Ee531if/fjf8EceM968Ee531if/AH43/BHHjPevBHud 9Yn/AN+N/wAEceM968Ee531if/fjf8EceM968Ee531if/fjf8EceM968Ee531if/AH43/BHHjPev BHud9Yn/AN+N/wAEceM968Ee531if/fjf8EceM968Ee531if/fjf8EceM968Ee5msG8EZP8AKv6s 3UeQeen9RX5Ji7FXYqwzX/zT0HTXkhsx+kbiL7fpuFiHt6lGqfkpHvlcsgDptX21ixGo+s/Z80q8 rfmreaxra2FxZxQRzKxgKFi3JRyoxO32Qe2VTzEC2vs3tg583hyFXyeg292kp4kcX8PHJYs4nt1e glClfL2DsVdirsVQWs6tbaTpdzqNyf3NshYjux6Ko92YgYCaDTqM0cUDOXIPnLzp5jutee8vLmXn IInAQfZjXiSEVewH49cx7J5vB6jVTzZOKX49zLfyZsNNvvJV7exaZBq+p2pigihuAjKSIUYoC9VU /HuctgA7/sfR45RlKcQTxdd9npWk6Xbw6VbXr6Tb6TqUqAXVvbqihSdypKbNSmUaqI4XodNp8cJX GIj7hSMhkMcgcduo9swsc+E25khYR1zqenWkSy3d1FbRvsjzOsYJ8AWIzbCQItwsmSOP6iI+9Xjk jljWSNw8bCqupBBHiCMkyBBFhdil2KpP5n/3ki/4yf8AGpzD1v0j3uf2f9R9zG81rtnYq7FXYqiY tOvpV5xwsVPQnav35ZHDM8g0yzwiaJU57a4gNJo2QnpUbH5HIygY8wzhkjLkVLIs3Yq7FXYq7FWc 2/8AcR/6i/qzeQ5B5yf1FfkmLsVeS/mh54eaefQ9PmKQQkx3boaGR/2kqP2V6EdzlM5WaeU7Z7SJ kcUDsOfn5MG/K7TtO1jztNp2pFpYS7yLByI5COFDQ0341O9MERZcLs3SxzZYiQ9NPYNB8u+VZjJf WeiT6Zc2bt6Tzh0YlS6VUMzAqQv3EYcsBwl6zB2dgxzEoxqQ96dIxVgw6g1GayMqNh25FpqtxF6P rMwRAPiZiAB8yc20JiQtxJ+nmvVldQykMp3BG4OTQDbeKXYqwb837a+n8sILYFo4ZhNcqvX01Uiv yUsCfv7ZTllRA73T9uYpz09x/hNn3bvnfXLWa3E1/aiokjMd5EP2kK0Dj3T9WQeRxSBqJ+CeeTtT vNG0yH9H3Mtn68KNL6DlOTleVWod9zjZ6MhqcmOR4ZEe56Z5S8x6xa2Ud15ivJf0dqb+lb3lzKAk UikLGPj/AN/M5C/6vvlGWRl6Ru9h2D4pxnJlkaPKz+Of6Eo0XznoGgfmRJ5Wutf1TVr+7pDJJd8Z bVbpyjRRR+nQRsEJDUTjXqewB0k/D8SvS7454mXD1Sf8zNUm/wASXqSsWEZjht61KoOKtQeHIk/T k8Q9IeB7YkZ6qYPKP6mffkreTPaanZmQvb25gkiU/stKH5ge3wDMnGXY+zuQmM49BX23+p6Vlr0b sVSfzP8A7yRf8ZP+NTmHrfpHvc/s/wCo+5jea12zsVaLqDQkAnpU4rSbaFYxzytNIOSRU4qehY/0 zK0uISNno4WszGIodWRZsnVLJ4Ip4milXkjf51GRlESFFlCZibDELmAwXEkJ3KEiviOxzTzjwkh3 uOfFEFSyLN2KuxV2Ks5t/wC4j/1F/Vm8hyDzk/qK/JMVlwzrBIYyA4UlCelabZGZoErRPJ8sa9pl 608sU5aHUraUsHbqJRWvLxDd8xwbfOJCWOZExv1QPlK+ubfVrrVoS9nqCSiMuhoyAIA4Hsd/nhts OWWMxMTVPUvK3nHzW9zdX1zczXejWig3of06BHPEtyfjQRqTI1D9lchkyGq6l6DsPNqM2UmUicce d/Z+tR8y+etG8r+b9O1HV/NdzcabqIMltplvDG1rFayIVEsjxAtIvqbqR8XzGVY9JOcSQPp5vWnN AUD15KX5t6zcJfx2rMws47cSrFuF9Ryw5sPYCm/TDh+l472hySlnjj/hq/vTn8k9Tumnu9PMpe2M AuBESaJIHCniO3INvmXjLH2fyy4pQv01b1jLXqXYqgtTQMiBhVTUMDuDXMPWDkW3HvYeP/ml5K02 y0G51XTVaF+SrNbgj0uMnwkjYldz22zF8eQDrv8AQ/pp5OKiPIcnlVjqlxapAixqywqqqr8WX4RQ V3Fcic8nKj7N6QS4iCfeU81r8wvMGr6TLo9/9XuNOuEEcluYlVeK0K0KCq8SBQilO2Rx5pQlxA7h 20tLAx4a2Yp+UFldW/5xaVK1nMbb1p6sysyRg20qR1dq+xGbvPqRLTkE+qVOCez8kMnFwngiPxZe /wDnryAurmS+s5Gju5DGJogvJXoyrz6rQqvX5ZqceWtnT9o9jDNPxImpdfPoyPyh5dtvLlt6FvIZ ZZWDXEzCnM9AOO9AO2GOY8QLmaDs6GmxmI3J5ll+bRudiqT+Z/8AeSL/AIyf8anMPW/SPe5/Z/1H 3MbzWu2Urm5htoHnmbjGgqT/AAGEC2UIGRoc2A6jfSXt29w+3I/Cv8qjoMy4xoU9FhxCEREJ5b+e dXsngW0kH1eNY/WidVPqOFHOrEch4bHJYiYuBPsrFkB4h6t6Pd+h6P5b8y2Wu2jSwgxzRUE8DGpU noQe4NNjmXCfE8trtDPTyo7g8im+TcFiOpSrLfTOu6lqA+NNv4Zp80rmS7zBHhgAUNlbc7FXYq7F Wc2/9xH/AKi/qzeQ5B5yf1FfkmKyZeUTjuVNPuyGQXEhMebCfMnkzSNcUyTIYbwLRLmOgbYbBqgh h9GauOQxaNb2Vh1BuQqXeHzsW1K0vLpEiLOZT63T7SgL0ptsMH5iSR7N6TuJ/wA5kdl+Yfm+w04W lpHDFAAQYPQUg8tjyPFi1R45WZkm3a4NFixQ4ICovMbvSLv/ABTp5j06UWnrRFoIfUaEK0oaQguK JXpTpm+02uvGTOXqqmjL2VORj4cTw35/gPqnzz5Nh160eeN2jv4YXRCqhhIoBYRkEj9roffNPjyc Lq+0uy46gid1OP2+SJ8k+U4PLVtySRpbyZV9aVhxoFH2AoLbeO++SOY3YT2b2XDTRO/FKXMs7zaO U7FVG8iaW3ZU+2N1+YyjU4zKBA5tmOQEt2HeZbC61PSJbCJyhmeJZDXj+6EqmUf8i+W2c+Msr3di IBJP+VfacgCQyzrGooq+sRQDoAAuPilk5fINn3mn/wCR7f8ANOHxStsH0zRNatorXzZan1IbWR+d tuSVDIfUr3Hwsp8Mu4xyeuz5Mcp+BI16a+J/Gz1/TtTjv7KG8t35QzqGWtKjxB9wdjlBySB5vJ5t P4cjGQ3CZ6eJJrgVFUT4mP6szNHxTn5BxM4EQnGbtwXYqk/mf/eSL/jJ/wAanMPW/SPe5/Z/1H3M ad0RSzsFUdWJoBmtduATyYb5k1b63c+jC/K1i6U6M3c/wzJxwoO70Wn4I2R6ik2Wua7FUXpmq3+m XQubKUxSjY03DDwYHYjCCRyac+nhljwzFhP5/wAyfMc0ZjIgQMKEqjA9N+rHrkpZCRTrYdh4Im/V 8/2KNj5vk9QLexjgf92Rggj5gk1zFlh7m3L2cK9B+aexatpkoql1Hv2LBT9xocqMD3Ovlp8g5xKI WeBvsyKfkQcjTWYEdF+LF2Ks5t/7iP8A1F/Vm8hyDzk/qK/JMXYqkd7HJBOy1PA7ofbOf1UZY5kd OjscREosHh/LvTvTZ7iR3upHd5ZA7UPJyV8NwtBvlHilvtcPy80uvV6f8ZHx8UrZYnceR59Q1HVI 7ByE0yRDHExr6r8a8eR6UI5D3ployUBb0mHVRw4cUZcpbl6J5Z1yXVNMSSWsd5CfRu4mqCsi9dj4 9crlKQPN0et0gxZKH0nce5P7FZZp1Xqi7ufbMjScU5106uuzCMQneb5wHYq7FUBfaaJSZYtpD9pe xzXavRcfqj9Tk4c9bHklTxvG3F1KsOxzTygYmiKc0SB5IK9OqiGQ2SwtMAfSWUsFJ7BiMOMRJHES Aid1tzQmh6dqGn6Ta2TGIGFKPTkRyO7ficOQx4jV03Zc0ssjOXMo3y/5cNqbn0XYpczGYqaLDFUA cIkUKANq9ySdz0pkxxnNQiKA5nv/AB3OPlzEEynIyLK7e3jgjCIPme5ObnDhjjjQdfOZkbKplrB2 KvPvzl1K/sNEsZLOZoHe54sy9SPTY0ynNEHm7bsmIMzfc8cm17WJjWa6aQ+L0b9YyoYwOj0cZmPL ZT/S1/8A79/4Vf6Y8IZeNPvd+lr/AP37/wAKv9MeEL40+936Wv8A/fv/AAq/0x4QvjT73fpa/wD9 +/8ACr/THhC+NPvd+lr/AP37/wAKv9MeEL40+936Wv8A/fv/AAq/0x4QvjT73fpa/wD9+/8ACr/T HhC+NPvd+lr/AP37/wAKv9MeEL40+9Wi8w61F/dXbx/6tB+oYDjj3MJSJ57qv+K/Mf8A1cJfvH9M HhR7mswj3PpLR3eTSbKRzyd7eJmY9yUBJzLjyDx+Yes+8ovJNbsVU7i3jnjKOPke4PtlWbDHJGiz hMxNhJ7jT7iEk05p/Mv8RmjzaOcPMOfDNGSBlSdv7uQJ81r/ABzFFdW1K9H0jUrMXTXF3HJPdTtO 7xQ8BuqqBRmc7cfHLssoEjhBA8yk5MkgBMj0ihXcmeneXQdRl1ABjcTxpFJM1QoRCSOKdKknc9Tt XoMvw4p5IiIFRu7aMmQRNk35Mmt7eOCPgn0nuTm4w4Y440HBnMyNlVy5g7FXYq7FVOf6vw/fceP+ VT+OVZeCvXVebOHFeyAf9Dcv+aeVM10vyt/2uSPFXQ/ojl8NK/5df47ZPF+Vvb7b/Sxn4qYLx4jj Tj2p0zZRqtuTjFvCh2KuxV5r+en/ABwdP/5ij/ybbK8jt+yPrl7ni2VO/dirsVdirsVdirsVdirs VdirsVfU2if8cWw/5hof+TYzIjyDx2b65e8o3C1OxV2KuxVQm+pV/e+ny/yqVzHyeF/Fw/GmyPH0 tbH+jq/B6VfbjXIw8Dpw/YmXidbROZTU7FX/2Q== + + + + proof:pdf + uuid:65E6390686CF11DBA6E2D887CEACB407 + xmp.did:29b885c7-1d7d-4a49-a10d-1956f36f23c4 + uuid:7bd8866a-e246-4960-96d3-b4031133ed2a + + xmp.iid:2c47b532-af4f-824f-8516-ed871b2a6e26 + xmp.did:2c47b532-af4f-824f-8516-ed871b2a6e26 + uuid:65E6390686CF11DBA6E2D887CEACB407 + proof:pdf + + + + + saved + xmp.iid:e59028b0-d8d0-f849-8939-b0e2bcbae288 + 2020-12-21T04:14:23+05:30 + Adobe Illustrator 24.3 (Windows) + / + + + saved + xmp.iid:29b885c7-1d7d-4a49-a10d-1956f36f23c4 + 2021-02-14T20:31:50+05:30 + Adobe Illustrator 24.3 (Windows) + / + + + + Web + AIRobin + Document + 1 + True + False + + 64.000000 + 64.000000 + Pixels + + + + + Arial-BoldMT + Arial + Bold + Open Type + Version 7.00 + False + arialbd.ttf + + + + + + 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 + + + R=233 G=69 B=96 + PROCESS + 100.000000 + RGB + 233 + 69 + 96 + + + R=255 G=202 B=138 + PROCESS + 100.000000 + RGB + 255 + 202 + 138 + + + + + + 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 35 0 obj <> endobj 36 0 obj <> endobj 37 0 obj <> endobj 38 0 obj <> endobj 45 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 46 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 512.0 512.0]/Type/Page>> endobj 47 0 obj <>/Resources<>>>/TrimBox[0.0 0.0 512.0 512.0]/Type/Page>> endobj 48 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 1280.0 720.0]/Type/Page>> endobj 49 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>/XObject<>>>/TrimBox[0.0 0.0 1280.0 720.0]/Type/Page>> endobj 50 0 obj <>/Resources<>>>/TrimBox[0.0 0.0 1280.0 720.0]/Type/Page>> endobj 67 0 obj <>stream +HwVu6PprqVr)[ +endstream endobj 68 0 obj <> endobj 33 0 obj <> endobj 70 0 obj [/View/Design] endobj 71 0 obj <>>> endobj 8 0 obj <> endobj 18 0 obj <> endobj 19 0 obj <>stream +%!PS-Adobe-3.0 +%%Creator: Adobe Illustrator(R) 24.0 +%%AI8_CreatorVersion: 24.3.0 +%%For: (sairaj mote) () +%%Title: (illustrations-2.ai) +%%CreationDate: 2/14/2021 8:31 PM +%%Canvassize: 16383 +%%BoundingBox: -1 -979 3584 533 +%%HiResBoundingBox: -0.002108390084686 -978.079999999998 3583.32116304838 532.88 +%%DocumentProcessColors: Cyan Magenta Yellow Black +%AI5_FileFormat 14.0 +%AI12_BuildNumber: 569 +%AI3_ColorUsage: Color +%AI7_ImageSettings: 0 +%%RGBProcessColor: 0.91372549533844 0.270588248968124 0.376470595598221 (R=233 G=69 B=96) +%%+ 1 0.792156875133514 0.541176497936249 (R=255 G=202 B=138) +%%+ 0 0 0 ([Registration]) +%AI3_Cropmarks: 1176 -64 1240 0 +%AI3_TemplateBox: 32.5 -32.5 32.5 -32.5 +%AI3_TileBox: 910.362213134766 -452.97509765625 1505.68222045898 388.944885253906 +%AI3_DocumentPreview: None +%AI5_ArtSize: 14400 14400 +%AI5_RulerUnits: 6 +%AI24_LargeCanvasScale: 1 +%AI9_ColorModel: 1 +%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 +%AI5_TargetResolution: 800 +%AI5_NumLayers: 1 +%AI9_OpenToView: 943.959999999999 89.2400000000007 2.77777777777778 1022 602 18 0 0 46 121 0 0 0 1 1 0 1 1 0 1 +%AI5_OpenViewLayers: 7 +%%PageOrigin:-368 -332 +%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 20 0 obj <>stream +%AI24_ZStandard_Data(/Xn +@Ȏ)[te e +p<%%Iw/9z: 3E;czhnhH~3gfʐs'3xr.r + NQnl3,eٷ`j_땦U:xwƗOCk)$FΒFgSsg+Cȥɺya: |, +ֆ( bsijzȓ o\wb&ShM(tV^Cc+ h8sEMPb>p&h8e1f(F|u׸ vhD+K(uS rUB:3|n|MVQq7!DL ]ˬZ5!K~ͽW3z(fVXQʐUYJ%o|){ws;;ˉW 78 O K(#5Q少բG?FBQ$4D<6|n?> /jq+, 1ax'X7ƍ2ԜQ[FϴTP Yo\<ƌ4#{ESr8`8ADYutц少Lc!6/Zu^iTV]j|gdZHGL(ϑ"E#d#MeL6+EmK+O*922ćYΰUW_ױ[ f(0K(p$TvGBPK( +4c3F1?)CAA!#@y䃒h ±™ Eɽj0. +LBڠ¨fBP<18+rB$ L( Fϊ*<YP, +?0mP,ra~*5F`f&@$OʟJ[;IgYE`QI(YT + EØP8e ]BA}j  +\@4DXxx@V&˧hcGB1M(,㡐P\:ZBQwP8‡% + ѠJ(0/O(L(RBa_8t_8So(($<' 3<㋟OTX$!9q4RGzQj"xeܸ%H(P$ +GBX{GqL( YG L(V@[ ##"D`_x\d'^ Y2 )v[ݝ2~4Sa^e%y*ChlOIU~+CNCDWc zghFsE“!e01M( FB17jÄ(3 +P`PLMeO(ф"H(,ϑP-pP4AxF="bx$ +ajBX%O(>s#3+_)$⑭MȰ5nq)t="xXp#X-hхXB:XYilqJ@! TP 3rEtdaU:]hL4c" I(a xp0 + hY(ɉ?cvR,94e{?)qUOi7JBh.C 9o^%fۃSԜ l$?VkRXmLkFzː9^˕]i1rĊ  0X@|BHp$92ĤP|H( xBGBQƣh0 E"0F1 ㋼-^tPT"CJJ: +1RV ǣFF EYY.a9kYނq p3Ǽ E2x+.AH( SBa|lӄ +n7 +E^Т.rbX4cH(Q(1d`0 `(0цԨe㍆h(409БP㎅h8C1>G=q|㏅c`S8\(0 +4k`#F 0`$K(PBh8Pa~H(cPXTv +WH(`4Qx(8 + BapT@S0S(+TAx$GxG>{Ï>Gpܑt:ơp4 Fc6ajPB4`,g$(1p, ƂX,.lQ Zt1 ?8n +t$#ļt6'Ё d1XT85q[\]7P(ְj +(G5jQ²jM=A b) 騈 +g:ެUiHhxEBD}$ Ƣx׳ + +0Ӈca$Y cqrrc5 EP H(;|rh裏&Ge4],>1^V>&P80Xol ++r$ <`$% .xpyhx `! r5<<X Tp "*ADPN  0\`BbBb*@ 4`0 @`HDÃ`B @@`$d #`!"AB"0 (@0a9 $D4T 'xxDA#D!b0pX( a!$DH + +pР$"xp@D0 TAb0 x T "0@<< > ! p +LPA  d  NAB`84pphhP44hh`hа`!x!#@d @HX  T 2@( @Lx8 ǣABhhhX4!bX, ED ,ADÄ L B "p`4H8DPႅ +X`AB(hpCh]r%#$ry^ <4`@ѠD`9!,0ႈ 8.@PB/9@ 4\er 2! T]`B5<@L 'h9-%H>v^V wXT}T`b0a0rIaK  0 .h0IUxö Q >!%+A~ +`!,R;ovC*C{3meHUˌԗ!`0.xp0 ,Dp$h`AB$?#hA &C %"D@(4@pPp`Xdh)AmdZUYIVhFdcU]]Nmnu@ Nʡʩse>:JTjGڑJ^峏ekZ"ps+&J^I+ٖfrTl1˺Y>dʹ_yޫ0|MS/ʶ?!2^?2 jު۟RݩjVE<;֜Kslfdjzǚ +Fl<.tć4Y-nd/CwêWjWt֐[M<d6URl"+cϥ̬pK{),ETf"|ez,IGxg;Ow;Y>!ue-W=zaإ6CDuצ =9SHv:x3']HX"S]g$[;Ic|J}/vtnCY5jHGTJ*_H\1K@{gd3if9Y_se3ITErN9M^Y.jݞgivyټ׶T\iIY<.SɃV2{[Jlbp|\<+Pt2KW;gWֵ=ӲCOqU%Us/Ó^*!u'U5yb6LRӨȾfr4ie4%b+7&'#RY {vW!V0n7ΔnC;-teW .H˩Z#++gBD+`96MTZOeEw윑6lMo5?W:HM;UصvJU9֒KhZ{Ϭ{#S ﯗlJw^X~TA, l4etX3}M?Nu:^S֕<=ٳ$/OxwoGOl zfnʸ,&9N=Bwtt|t| նL7MnR/NW2`ǻ*&TX;^i ৭rmmy>*ܬ{˖nN-Yfoٝcu2g \v챊a_Yx̳w:woRT)ul%Wz`R{Q;/5V|$z^$YgRO|[]4 U[oca}Wګ2B.\y.^+b~e{_UJ;RTܺSWmS£T4ft'KUoHVbVQn^lY!TΌgwJd㝏$-XTٞVHU\vt!⬴4r+yc)7%vi{μDb^)vڕœJ .scSN{iDY9kXO i% sRYBYe*=y,>q놉%_b/,u:[pLҚV^)9W}ut",&Xg" ]ug hc#չ)*-4k|Ò7ŻfJ0=#{.s6Dv$٩ Kakg˗A4酮$tT%v?D]1BnUXnNG*+;޲si/,tN,u=]ݕJوTgiU*d;ͥcb3V߫5|+>U<&el;vrTCo$JeAoSNς [j4k klx+%[)fg1*/ ҫ$42 IOJZ MK%AX^L9VUuNIc6:fMj]D+iVތ[`$jZ ]IKdN\&U7%ǥN.k|uzqM:a=tJj&WbcRx1*5Rcb:f"_f۔QUȮ,9ºl7yS^BY,e4}nuRZSB*J,Nbt/&2sx+onY龂T7誤KM2T>! iOrjJܼBG\{7%KSWA$n[K3wC3d{}nb@E@@4<8,HD,\`K!Y򱅐mdIWݫ٢ה9]$zlETGLor\4ʒg0MA#Wj"=+ +H̐ {&dEb&r*>sdS$u%rz$V_JM6,c9ݟD_#\eCT5Fs * $-fIJT2MFnY:&_SyJ13W};l_ݜmcei2LKSǰ\(  XP 8`%`X`8pEFR$ *Xxph! ,H(" (Ȕ y +8@F‚  (U-jbyDI7}J UI$EP`A@XP,Pp4N + R1'G#pP F[Ey7~X.);_4W<,V5ûc?''&tw1$I2!J]>a~G9!U+訊G*5* +hL|RÖلsV^\%VMs]Lu-4UZU`nΗYЖ-K˙ +j_1}nDteoy +ˇ0KMT/*DVU7T' +U&9fw#Dt31,%Tn*k' +D#~sWDj2D.)\mwE,c3 nƜ, /.aieJܽ\hK,tXrH?yXiYeX%}]3/J:=XF,+RGn~&G+ab"ey?*V9|ÿC83X4{U5-V_2&ɴީ ^7{.'莥iGGFԺU݄}ktFWКqUcԎؑ;g{c"oe;&{|KȯPWԔ:St+)?Ky,3E#sZOٔ^gcեM)#rao9Jq\m%cBDѬʜ XtHO>`|J-Ts%< +%NgvfUPI$WCgbyŨ$pZVg:y ҙ$+7n|#r'V58ث^ܑ^6,wfܚLަ^qeՂ'橬XZ.YyOn2,V㒉';3Lf6Ò_?,9sW"3XrLg;(ڐVYU,݄Wgt%5Vr)x{j XU<'٩BתX?3XY8{eJxʺ2o*5]"㽕6'펐Uݦ+D§Z[Y~j^$'lʬxZ)|W-X.,_kTXP +ѽ(`](IҘcd;$JRınjHIS*7͢k+Ő8b-hVgSꄙvF-WNpkrsf1{ygR97g1/@  "! ł2RRrD,`Wʘ=BMc}_%3%1 aU#S|fV(^JZYn#tI˱HV&48KN{3 mM* T%<6%k^XYf]: A:BBf]ri)q\ hd DV%,ّQ.oo*YVKzyNO+Ʋ,;34N?^3+C%<;&T&TWtܯLXXR:SN1f\P:e_V嫔{=t^T*ɥc( kKK;MfUo|/b dєj/RMsh!}J}^23|aCuIT_e|XuC^db ׿5k#NnB̬]F7]K`sHEdfXɲ[:N~ZW6&Y>ZѿC2LIeg٬b5"i~k}[Smʳ}dc4#`Y<")ͥ2o{ٺvŽU2 a(}Fٱ 3^GjdY#2;KH&ؽs4i{aҮ{CWwV +nkG֫߷mnG=nG5.Cɩ:/)eƜQߘh(Kɤ;$`:mw(m31FI03!>wWݩ[iizVQd$j_~v!,tB|Ҹg "hTL1 b4X*yKJ)1йˮ,/ϑ$E?_FcYu݈,YbmBv, +>B:3Ju,EsfȬ.\9Ud9S5sSEsSo{X!e6SuʜTN&MKvėE3CA+4;:vXu>Yϒszsժ'XUI3t⥝9sՖg+ eOl42o,ͮ殊箊g'GUo~fVX+.rˋM-4G̾iN6sOe*,U/SI);YNqyheùa +VQkwG.fgfXItO2:3ΌƈWg1rVg,snr҇H:o=K2c,VҎawz֑롕;T9+mV@N)DF.h{ Y*:eⶈnVȊ>fW;tvBm/lӐTVc!>}iS_ TU'y>B"] j||m˴cc*ɗ[tQa_s,'fgrUKEjyI25Xod$Q3'*үIM<vh4^2L׫ISZt ]QJ' Jg +N&y 'dOw'$Oi4%O\4,eBz.]Bh,*rJ5Nz]yM/"N%<ƫRajZh?īī):Wxg>+%M^w9W_u-$+{3kʟ6 kjT|]8XqsZb݆WY: g,OcZb|U샅ﵦU, VunwO_), J[%޽[C57.%lݽXfl\)CFn +s˾IޜhW dgߤ"8y/3*RuE?;3v%Sx2bę: -l05.+eLRS)J  88 EA٘^~FV>F9GjN!c4 2F,#VPiB5?pi~Vt +9qb~͑B08k`S9BC(';QYT? iI] zxU" ol8*H.~ +넲) zd;&E]\%# `&Qުk!_tF2T~vGI`!Zw ~|SZSܚC8 ;{ЎKQ: 8[ O +nzrp:D M}Ks7@̄6 +e|O2H 78n>U.^E3›|=y)!L<0&5BڪZGa=[P-t?g2(OsnVVn(~EdԂ(-|Hxlk~KokHNz<Ž&WG4u9sR KU%c 9,*4_H2Jޡil.T`I'Y?vX"jtoZ+Oͳuq]E' @^;r{fxѠ%T-waCqF'f@jakIp $VSRYI?Wii3Drfp%"/N(f7=:8t,K=E"T0K#l~F] #8*VvB@&$xCW=\!#d*LQXtb_A7m%;4kI/{PVDOe踑6M  LN1B7z^<9֏HOZ +&@HBC(#+3aU@#@:ɉRӧptQGVM9lc9_d͔}3洝V /nԎet8\2g֞ǫ_\_P8|p^ z /^&$Yg{k!RLGqR+ /E/6K0dB CGzAIlv?6 +9kRm:Sn϶8Iyq,viM/:OXkVuɕNy Wï۰ 1 `Ef{H#xӼǻ4($1^^enDa+׳\v!{O N0l}R()3GQwv'# ΕM85tDl+'91 X0敶NgBY%SHFXSItD 46Z +' !8J(gpí Iogx3D>J9^]~$OfJ3(8ayTtKDQccc1ps$_:E8Gu! BzmԕХ2 Je"&AO{" <{鿄^ծBSһXQݸtSeX)ZtFp:qJؖʜOA/- NM\)E|7sU!Nj_Aώ̟Ͽ C9,ExBƻ&IK PhXdQ w WUXK$;]v/ jS9;wUtUK')PnsW=xKO,W.5ܾEẢCh"~z8Xw;uVĠ' َ[Ө(^/:fKҋd%K./>aSÇxp=nGd;FvDWx: }m@n@=V] ^mu0z(;DdA ,N +9 ӆ DSQYjtȧV樚B~\ʚ +t8?WM~"Hl}0|Np?7r@!>AcNgP~u˕)6t{6 >A/AAM.\ !; Uc2{HEm%O?Ya-L +6&"YrnIӮOf xm͙՟ؕR6ׇ5{ +سtp-&4AK~-RO | k +cs\}F;L 2˸B}Jr`rm/5b0kM:rYQ\j5.1ib0\*?,Q|m?`Lyr5 0tA)2"w+Hɶ< LDLx$_֞R%~(rI pQv}AXX@T13/}?6PҞmlP½T..grz4"b~\;Pt y&6kE ^@u"!gbq0/\<L7W2!>@.r +=$WX;V2J2gȟ9tKi˻o<ӫZ˯Oh_/;GĀm^[ME{:k.gr#sFG {31&0@;=g4W]͉rfC_$'\sE-Tx2z+S""߬ spR;=<Jjm(hd@h9 kKCֳ|{Ek ڑ>5#~ŠED0X{HCFqY- +Diek6oCKExp1tw7AqY6!f0reC0!?)#Ql4iqBGT}qFMTGT͖Ā#tRĐIqFy#Q]="+0o%iBF#9HX݋R4̓6͵|;Q΢LuMnX ͱ@9'ѿ X\@b +s-R-pn< /:c"ڜADViznt>#^2Ɍ_ȰUYTSu COٮ +^2N›&۠A7կ+|m=,n^0gMm0zgD^k)~?& +-aQʜዲkd2jc u-[yxTgP<$JI`lky- ~ 3࠹k.QD9YwBzjbNL6-^C̆գ $DKcCgU>tX:L,#ʸTw8";=~[CkKsS/ l56=pㆂ.q<BqHfغh,Yg_~jCdcC^oW{s<}cfzX- Q_/Hb+瀜om{!/g*tWULK=UIrAH 1) ['pT/KwA;cǞF mQ 3}h"࠭FBjBH"ZL^ +A4/: +1QU ub]h^'~!j ]^Êɟ ]6Y ۉrJ C7_1 Va/$Q*y㏮YS0FC", {ij6+ٟZ1Dsi"gֶGr`T28-pgF-~lk?!+>!Ʀ~JHFBm;6^6A2%W"AIClαF"e-p $?Qn>hT'z-jI?a߆vQ| fEix05Ip"mXmШ8\=p+l^5 "Z< Šak@ 0YGXNT0 WN13Q2$÷Ù O} 7S @ !6WJX p텘&s-P-: +ŬȶG?+ʫJZ$6 +uP({n9T||0B) k-qkLڞ0$XC弳ZÖ3]*%7-w>*N5|`4nceڤ"o},u{b~3̋Lu_C(y +du=NXٞ#*(dLsDuk&0 9(iࢰ^p)|O ?B]+BnWHQ^E`oΩooC7FV;5lZ!$J@+%8z0>ɛ#%O((8HI9!Ӳ?~eIp֏/%9ЭyD؏JҺі֏XgIqDTwi%ck]Uĕ_Kh5PP>#,6zGl:X9Z3r"g&(c)~ru9J]-=Kޗv?Q1>v&>1!+<+3:A$m8;#=>w&yw~:d0/7y4^^ù^4|E N2>LTYaFZ[!YrFjnZp Oao!g@hZK'vrLYJv +kׄU1"X_*l3cKG^\J6WѯQWe#Zm.R j^kONY"lMh 68^9b=B QTc,ΜH/2u4s[6P( ]jk{Y +-皩^8O/ 3[g%"yb.Fg _;8V6I}Aem$!C{;DjA0^ 1~TL(`A9NV=ߡ5 K/~iztH]lBJjZC%--ZZ?"f~AF|.F)E >|}`J-ԕP|W\ݮ%#=3a\`.\Ac3^,l|"bD}'女xAO#ϸ30X.}D~a:k{qMŖG;$ա!;+Oo_Sn!dZ趭$ZRr*dcߥIFSnwKPZ3aL.ՃE H d'ċ&8\xҦS5S񿟞wD4- 1عt4AUO(orE3IOБ94xmT 1_m<&-pPi* J9qZd1SréǍ"ޖsJ>I0kC=w1]5 +@3P\rH~~KTN .vc<%s;Q@˭9-Q̤ԑʦ$b4Lɮo%H~Y b~ +ѩ'1[L5%"o'Og>pUS|N 2O7}ʄ5~RmICA @ CeH~w.lkCe~@DdMoTbĺb)j+)t{s;YRL;!-MXw#n0«.U[B2M*A@(>bmŏT8 PC>҇񽊞6BߒKO=Ϗއ[<Ӥ=.VQZDH7kOR[[4,շW}Kj6i:DUZF+F* Y*^C)@ŤnB¶QMPh;rTT0X"pKv{r Tl'CjXŹ `KD  fG2#.zV׭]DAVnF+eRkdQl*{QqiّP4N{% c g܆J@.OJKBkGЙx*֢I@iFMq]DR*D_j!;؄q{Fr +>՜wTZU5yZ#T( }-O| P$聈=)$8=Es+]Y'MVd0 +@sFiaَ*ћ,+|Քtbt]Q7ZX@wp=c񯷪kآHfѾ@R٦[W%M\0`a>a ? p@ NV\_M%*c"pQ z5ASHq"7, M%@$Q-!`u߭Y`IMV4HT": \+±ʾI@YH"XR=,oi[, ΰh@i/;7M$^ĥp.!%aŹH` +hřIt`^  +3-( &E)>1{ DCm1f~(c qGd7$S 릱  4m@ e6Y Ԇ["f.͙ޙQgoh&(YzgfJ:man` 4q5"E&ayQyk5Ml7ZltjxS22468>,I@iո9 ` $K#[壪?Bg*#pd2&p@-dbtCDㄽ }%R8r<qM'`KCV oa|rYk1_sZ(t>C5Nˣ1NNuu+mdYwv@٥Oތ" ݹvp7Z&{JNKfwz ETŦ.c +aPCda`.r_7FKOarR{Lk] J7V#0 \˙f8ܺ!HuZBAuNXH aQ𸁨ryQ\NI28.]yfivA&݋Cm9}1TN<}I=8#I?4U 8HcRbЪUQ3LBuh(6",M3 +рa2nh7ȟ3?$TI[otUtNnvfrH$m[rlr鴡D%HVuV:{zjbn.NCGQspEfӱW)>vRúĬ}|@ +]E.NJC%EuAQ?FVӻ8*ERe뻙K;3@IH䀟QŃbn<@DAqQh" F +W1 ++ryƓI1ll[x6B!xdR-^3z)c*#.ܸ:Gn6a5+fYV2e%R)3xQ|, LX|_E6.,: qUv>ut˸T\zIՇa㔷nRd`TVmI6IFM/L{%֠~h Dv :$N|= +U 4 {SZЏNZ2#3T;_ܥ[) Q.d uhFkss hNB˥̼JWLʁS@RkC)JΧr9J3Oz~4dE:JpgBR%Α#d!!РNV)HÛ_p~O?@ѣ:wT^aT> G#H2F&[Zex!c4i0*ָ`s/|U|SQfe'en"Iߣ +Y!h놀";< &Gъ:GUKQ➡_䐆sP%$D|?)%i>#B6\f ՗M W0qTNUiY u)vq^+mIe&ԡD56 9y{]ڄt1]HOR++\z/rB0eDU {]*Y}oH 9΅Mo8emBظ ˜BN X ix%ߙp< F)+E+ & 5&W켃wMn{aꖦ}2_p@,Әj3"^Vjmt7M9*ރ">lEN L^)o8L̖;?Ƹ0Ԇ5RKE HS&n!fu'1lأTw>)}ClU$}7(7EOn+j5]q->FH7Ȫ_K-F߫\|8ON[jV!;4Gu2y $"@#+qbY1j^Rp`Zu#WQo(}Q#c=!Š{h3@L2'`};b[u!JQ6OY|]_;hfFZv;L." E"]MNvBQ9L[T1`RBkc.Ueǯ,%j +Bܱ\!9kY͙'ؚ1_ra|H˴z"qUPSeF1g$d0x` D JqErEy嵩f,p1"С,s C@_A}Fe9JRWT| 696Dv~=C PxЖ/ nkӻ]HOE.."`Qk!g;-ga kLk>6҄Q*aOOh#%aea! Pt\R+mkj? A&^_IYt]'%dt;~{U6V8 +t BmOe4hcLD9ƸT-I|n A6]JşMdgJ0'}B+?NjV d"\ƾź'*zl-w4 uËMps.#Z*awwO#ojL 7VG cQE3☌.3 S5YKD;;ٟ8z${mqRքխV,b^L&!QU^ת5b࢒7g_jTeP?􆃨j$QxȮ:EڥL,[6MKKՑG( %@DgXj4JuWnE6Mde+n O_nX8}DA&w0$DBt{"fm=C=ۈ"\}NgA9>}<߀ }dzU3M&ΗhvO" hR;ؓ M_f 5̧6msQAmP*` +os[X& ="^ŊGtKYhg]BcyӅ}|I@aآ¤pY9|TPgL + +FTF7J T\aH d kB6?$ r(:tmxT& T^SĄw2 HUM Sb="}iusF2 \o~+9WdugDU/N(4JcS[uGǯU/w~A3ڥW/FK[-kAI(̨h#^wznDDx'\j#+Y)n9helHOLcs46[Z}7W+]3OuY/̭rDž29q!?2ehuJ&#K_WkڅD?Ӹl^r`?z1-W"嶿]waa'HXVfl3j|D +}ԏN ;zo953E[w?ѵM#"hʏrS\$:C +nOW:>O o+<8BԱ\aң0qr.auiUrYAj9;xK<xxXc'rS}718A| +}WhJw}{Ss#;R'`o͌"tn463Ol8Urd&sKX9. ~TӬ x*;?J!@k|Dڀ坒rSO?h mpF)m@W'Ա]`lϱ{ +P4?-@XU矾PK"dv+H!UE!)w-9;˜]E?%K. lfs5~ Pu@<X#}ş$Q%E_$r!,Jhf8uc3;1 +~ApwFmdF ^u.]Ðmq浥khZB.IfŎF>gŗҧYP]W;3m՚]8+L:tM:fJh`0)?vr_&uZ +!+!ݹu+yЫ;MRҬ͜HWx*63שtByX$jKd&mbSP5,bhtkXC އ +r 7GYgk3+7 " j=:!=ʲ[a=2P/iZZ/˞ *`z'` f](,b}8J?&y/G + +.Ň)mbkW`+,)k/ @\h9"CJ*N WH Ey=BX,-CQYK)PYTfE^L͞>ku"򖧡q⣪q5[R7?Pj/Yg[hD٭M@4 %Ad zZ|I-pC2YqIK8P/UGaQ=!P+5Xp\r2~`ϰ4C`]UR zX̫?a i5?;36#R}Ppݲ՞v%ۢɒ-^qrgl{K%ydx{2ߒ7(I厧\o[ΦJ "rƣqb$WDG&OϩN=L-kV>u%>pgV |TC]@pX㡣JgJFOS#$txi˼C(*QN,Q^79uf=q)u #a! +%kI:[LB۶Vx$+(M ]0h#v~'= }0U(^vGnfלac¤zV ~Қ cD +gjoZ +mө0gܙLU~0MEC{x >_􃳂vq4q: bUԘ9lTeh&L7335#͕>)>*XJzkЉ1ߑStQ Ooѕʙ;p~~F#m 1 2[$ 6w%ؒŶYάਤdһ\* 07B@*\vd2PW 'z{ LQ-߲(ɪԹ: @"%'^~ y"U_90<+ /nSI77QAWx:T| a=P=v+K5ʮp +b^@*T [oJ<b1j?8&(j\ iz!fuH*a҈>5e,6iKBxq=POw=; y5wak (/vGM߄7"S*Y)5r̴zֲ^ѷZ&tL+ڠc`Zn2 ⻛< +*%pEj { %.oe|6Ǚm-uwڳWv`ބwFe'?J rXL!4%/O@l0! !LF/s*Hgm d!=RQYŎ%ݝTIJc +25L*._;wi3M NR_!YHwAXC׬wc_jN3yRmqLkb5[2-Оc\ut93}_-qcko!Ϊ/Kq`h*t:. TүW*C n\һ\Lod`U[N#RAi[A<QUIoK5+Z23eX%?oon>_:%9*f^Uv^B0]AEcO|WzUy}^}SkqayxA3@sK\P4~Nm ˊ<(a)7 b^>2X6*XPKN.if>L`E[zU!e*6\ R"a+yF~ +'gy +G훐kK/v> OӬ1jl}-Ugsir$Fk@4hV ySI7pĹ>f qD6I93WR&&hP׭_oy3s@uVRV/QgqZ&(l:dX٦׽yï +(wiZ 08YߌA--rK +V-fQqֲOqEK]3y'Ʊ+JaRdb@"2T)2"Hl%qDE"\(mɭmY* #^J̤(ד3 !T'(WD!XT";ʂ`ҷ'`,hT䣧" g +eەt5yZ1C{4&4A~hyE'ZdbĂB@-lPQhC~ s"Ɖ//X5.J(t50L3ρ3ҡ7# dB`ӕuX* Yr]2!: RP٤w.Y@o&]H^wع\r{߯ Q.W*)I7ޭ܌`"In KHXlSb:) * ;dx9+e\:kk1eQluJ%ĆH"0.>>\L=1=<~n 0Pˍ|>)OƊY."oi`!_x.q<4nd=U婪[4^r督(*BOW:;!`|B5o5 b "2k)$##O +bkzBAWM'x3jvE:Da\kAt:4:*΍_f&qmxZH$QQZqOzԗ ~΍+ɝm% ++cfUSJ #iP5q P.-%z'8) 9K^~& 7։H5ezl *` E!1 N=uN6UL#LZU~)Ng;[ιm) }k۴ yg0+Y9eM^H"oZ6+<z] +&5Ue)/ef;I0kѪZi&O_ +1.LbU@$Wu%+Z|xmŰ!f4~R, +C\\o ,{MC8 lQ +ŕClv- ?Dd}t,xéN uM  WF+FY8N`=yb:+zWMH!78C_,0Sh  &Y2~4EκVrFgAiT%-Hd(g)X#@EHaڧۄٛiJxYK#46U]@iNb-nd,yXT, 0Tc$4pzpYimU-9oT  q +.P7G"EWSPS ?\x}}P骋ᆎ|ڐGkX]3`C+p!%vbfī;x!P.A\aAX*åRa$W|m"]1`^[4Zqǰ]VVSL oB +) +tbj@pi3r1!$&=WWH3f}Vid; +d' ?6Czib|ڍt'^zb[ǟ[VMD&NQ2@N3=zs}nۺ' rDmc8a9ZҪ͔ua{Ll9$ev?W:ek^$]B˕U2Ifnf?j}SIVQ0о4lJOrS`^~3Ҟl*,!.|s@ZKT ۼ20;%^ +qj9[׫X %h'L%Q4,$SǷkPjty\h6..pQdB2Z֚kBqD+XUE2!kzLonPėғ2FF,x^V`˟(T LtPX.DkI9>0^[6 E0ڄD-Aǀ֓ B;3+A48aTHPb(Υ= Flqf0%ݦK]=^J%QйX:MͩԞMݛIBZ4+z +d*—4?tBU` G"b8Rԍ?P=)8"#+1%h> 2ȼ@2FW)ala&LM(uYdM&D&S̷fUP%T?XU~"|ߞ=p(Y7voaGUPX%WmE9VbJ]=U5^S8NdItB)C]*řLd @$&F&fMb>$21hl 'Ȍ'tCC$wl\2{,``h SZ} , +V4A14 3VR+2QHlb2GӉOИBj\'ԉFX_1Y,#+h~\g|BuH'J#Iɼq%%h#(Š|AGE7BBTJt;CS~ +iǁ I0C+"$D6 + Lj)Uw2?B4Tu+(cP,S?##TRU-|Vݫ5VJD#3DT"jL[T#E2u>mFm^22㵚pqLBAft kh 9Q|ԂGyb>ҨԔ ک4jShHˆ܋ŝU3"%ZXͶw_;?4. TTH_1Y+!QiQq& HFSz2~YI 9Cu$ !ZQ{g$ ;ef, 94R#03p!L͙F ͇k"ݥJADO$m:%TWCfBu:b,~Qսzlp +Q79q>*U4RDpDB:F6Ҹ7,kqD^6PV/lA׈IPz_4Ψ–$QՐ*']~,*.RzJ%U)5a +=;B^/ͥb&;'RRRH;VHo>m`JJ͈EDj"+͐HMVЍݗ4L) +z}\^9'K;#XQ&1dZNLcRnXTL !h3Ed`NwFM&CT)jɻ/+1iF8ldS򃃖`E=䭡`gZ!3 A8OX9c$ůƤ1HtQJBg(Rw.f2R-#+v2!HzTCAFh ƒ]2@5.-O<⚑ڪPf&*V?Sf"eO`5<@4" ڐ(!!*Ri:4M*Mx=""11.zg4҇v\N+Lq +V z:> ? )CP D+ZUXIUшSuUQ}b!!4#fPJAV!B*AJ s΄0EV?ajĘ ?qWyBȂ " 0emL j|"*C߇ɋkU&^aR~)L&-qSM ")3r |+qCCBN(VAl5ph{e}m)F? RrIAJ 0N!P( A8r20޻ 4 DF$Ȅ,`"HD6*"hb"H8-HqHIτUg\4(b[˪7&hU11mM2?=sEj8WFb +G\QL}6,][VMC&}Ż$K7&䟌ïRq/Ei%ZIȳ%;G1]ShžYᱰeTdꌏj'T#T cq;UhզCٌLae>&1]U4 +z⻇Bafe5VlǷH⡑Eyz bS*))bbB?딒0')ߗgy8IFJa<egoo'&yOa]CE})&`myX Sd!sZXJd0.*J.XE!I9jY؋uXQCJS43Ҩ+ƋȧX{S/m4.-Gpj*\eJA52qO!3XϥhzU }NttamhGaPBǪ㥪&5Q_8urOnn.US7(X+^' *T-J5ZԈ͵(ӉК^feFJne Km'_ afߔ̋.b* =>"ڣFEJWgtD^JQOxm3B"&a˶6)gvߓ"8H c.ܮ9-b+u;'Y6/BLT+E/O|(WjQ4cf Q0^kiD$D꾔_&ME.ʦY^%W)RX*JqueSDk=-3ݎbME:Z5Sc"Sby,ZdECW'lXaTg#,OO%,EQTBATb'l2cgKZ*fnvFBed6I\#-FjTjmWi|/W\<<8`Hzj>߾ ^R$׃#|$^Nט# +zf숬h|hEdC@.D%IiɍmPYD-SQ6|TD +$SUJFȚ +Ez6+;RcB/U AGfp") +- "`%eo`)Ӄ)tR@R iK yWP gW˥@q vznl"[]0##hjᵶ^!'80FGA0G<S 9k`R{A$ =a(4B)rP4(VE+xj`,Ux#`NJAB*3<P~W ;v6*b HCUL^.چBd?iCv|iM +~Kz? +~>w_}P\G A]JCQ3:[ /5+ᜢ<@  @!3PPu!v%uuf U 1PbLjm 6;PHӀ(nb@Al” 8]AH`1^(TW(MlAB dFHchT1dpKԷttݺ`8 Ctr4H !qPXX  XVEv7KhmT-%'JVپhRyo +#+/Mt懕'6gII={w^{ԩyh+OiEɅW-ɛ3J)No E+pزN4v)_AV'K;*,\) pSB𦙄>fc  ΢7&W~~"ojJmb|2F?<$\|{T槃4j.ݮ-;A mH-ce`h•=*Ls]1rB *qSdmeAځlMs[ˣH) +4+ YæFL3⼩ظh +DZ62zh>MQ " +7[<$^t߲ H}TV8*sw5_ s\g=xqaB9dMށQ5̐t?f"":sT6,;tW5rvg}m5#ڲd/;Dxиp G'N37')liu27"#7JO1# +}%O&)`68t5WIL1yB|Hj{$f +ܴ:;hfjJA%z)#OD +nž19ͯ4rmH# 7/ЏQkQ5 XL=F'85*sHo?@[D[+*;~-{lwS`;`9qvQeHes+{C9+G),k! i$A~Cgb-XWoe0 '^{nnن^{XOv4pN%nK8\;[O5IС=@ja)u +lu@k'`J!elDϔ׏Wxp^xS_l$HT ,<+ڭ +=Qi. CkAW@\ _ m;:4qY]?l80퇅ǏЧ#{ɷtU-zzPK ġ' ;26JE 滿,V [\ +%4džMt_CMjT#O'&\B2}6ZpzfWN=]_H$8 P>|y*0='I+h\?GB +sNhv`& +D2rPj|߮s\U׵Vy77z)hWL˪5oҐ,#&ƭ6SWxpjBn=H.o$ѥ7EC2EV#hoX37HS 6h+}e#$M<|3)k,u_r!wc!0 Wfoolt@7 ح$Y^\Umzˆ+d]=n@|VsHgg lDTzeaA‚0B2๾W=|- ںf`x:HbVz=2 y0yl!T,P#$>:-NռO^G|Ao$8fɫ~o&*U+͝ FOP ;Z@BHS%f#jᲃ]PU>t[vu&? +Hcc +4D҉g^phlzzKS&|lŤ;ڣ| (X4>P +?ruz\ &dQ +1zHd6`U\L_&hHے7$d WDbD!@R@'g ~3ݽy^?{1}Ofh/# Do[L>{E-bWX= R3Tԡ`A^|Nc~L25$]y!NFS^}u}YR/cO5ŀ Bų^%sl2!aaW)7*`l>AVl~!z;vҨwI=Y3ws -8&FD{sDpڢ^ǐXH/Z9L(z/( z0!/ 94ƶsyU8& Cg?#dun[t3#7ah樼#|z8pA lC!.GHGJooWnJTSȘ83YpHʗ}M, ,V +zܢG㬥1#5h +浞iB`む*?|drdcɨFԩrǴ< n4o*W6AO^z9X@O!Nr{U +lMׯY`gv@-8Tgk ^N(uO'q'^zC9RӐW][mB#o ]VpT"c#:1[0f›c]?xm1g(2H @HxYԓ˄[]3؅ ƵPrUJZ} UJZJ682r q ]>&Ҹ XRXt?76KYEp"[|wǺʡ~҃- mc +$ߔ#0I>(jIT +z*p"www {`7Oʗi#gv;[=Jw BMRkvd eԔVj~X/|ظ?eO.xM…N_c9Z+ jk/v)86.4hՄPԑHj mRŐJ|:y0cj8i8YLR 9›CըOThe)ݬ>?.iU$]p $ڛ!ivV3 ?B*Dce*\,"*1Bag'_OuhZ*&^FdǪ0x#3/SL! TGϜ2ip;4qXC+boLB6L٫m~ʹ)C»0O7$o%5qH-w͟)zB[X|JiUS,jjwGț3MK< a$@ hQ*>btBcȤ{[VùfN76lcP5 +S? Qn(z 0zk.!#BboY!S(%s5wIlt,U;^^cΟDZsmF;"(;`!j1#ŖNÛ[|MμCـ1q|)έXךcM~Һݏ>R@28(MŇrXΪP4O7t& K/+a>W#!EN@(ms`Wio dz|8]j Q-͔ k*tdysEE!ktb |g~ƥ8|w{:UўŧdX{u!dfԌL~rpbo-I dTϕIqW,|8eU'W&:49l6[XiPps\՗Y_8:^Z\z|8۶H6|dn38nS$!>9"9vqb?!e̡aM4b{;9ýKgp!T{xs%[(?8)ẗ́$fX sNy@r;9slC0h}6h +F<) 05CQrR:b$$ + |0t'Y#k:x|R"jk23<0u |3O\֍&RX8 e(Kx>O]d-Se`*!=Ga4pce-u ADCt]1g~ +}#pAeHwUOϮ/+t]+%wcW\й/G7TLhE z߫:a `;9NڎJ6N"0tNjb b{$pC6*\ ïٲQr")9&x~9pVN'qx =a ܝ7*s# BN3zTN];' L 񃜈Lc2]Y^ZDf 'AHu??0'/~id$j' +[X|MH HenSn2 OmBIePP“^J&rD]͕DjsKTJMwQ`B-'&lNjɄIhA4LLDwuM340L(+LI'ZM&$vLFƄ=`RRd aRN&6I84E65㷄Ov 1Mp(lXg/Y03lWXaJΊS{xJ +[%Xuő*+^H%7~Ed]lJEba')i _6NYL+NFCc@`siq=1JN$E$bԢn>ΦIa y2 B;.oQ’P ))|(ٸ@EVVxû6"mii6dBr䆳'Bg7j"1F5Ldިx$B剂#"xpt4"HpDD8)!cC;ȈQgrT!*G=y9!!0av|(:dmt#l-!;׆4_:` L|i +@Zt- HHD ڈ&Rz:)1@;ȌR1Y4A:~+H_WH<FPONR`8:];C[!1p<2?S :]Uބ)(9!u \'=`G&~PeEj B 81ԏ%Gvo d=Ǐ Cu1o{ǐ Q>~mMLЇ97 qkMMֹ(|dI|lz&@7Hka=K{"ҹS=v0AnD"Oꁗ^AYrzH"ET 'Sf/uKy9 Ô{aE^%!erbC5!0Cu;`P=<:lǯ0k<5wlw;;d='y8܁kK6!Ui8m#CqB$֧h\Dž'ӾPQo(7uO!:S! ;*B:ұa7B:*7FACpL9 ~9&#JiVH=CigV!1hB,r ?+GWa-k#$r/e R84[)믅\X,?4_xKS?"z\#u qI֟db{!_0!fJbXDn"D[ 8#E"ă';CⰂ8r8TiU  G51o;_3hDT"ƶnݸ{cjD*PyC<oD wQ~h">#:7+7HqcPtnEy^ĀH%xs#2|mp=#Q {i4m4`#&\nĻ2-F=sVGZwil#>2kGt)&^kD dkx($Wc\>U5PK$L E҂Ԡn$ơF[ 'I: ()ۦ%4V z is&JiI$ i`J 뢱Ɣ%S;FVUb3h#FK<l%婐(KtӜF^3%oL71r`Rͨ& js4#ckfP&fdjf\t>s&_]h2[|m>PqtM\fn*C6)+e@T$Pjd[^2-IQTdfȠMNBZvsr2>oae1䤗qs҂c5:6c71|x1 + xHZC^1N +bb$m'1(~C /M2P0Fn'•D +Nz06xFC{0҉ctbX0X E0R z12'_uREq񋾈ڑ/6M{,N쪞OtbjSm^ 쓼[x ]49]|.I;ĽhC'~ N"I FY'p|."DtkrD + 5ɸn!.BvR@|'zГ$3{bHOl QhT?-ဂmr~;j(B5- +H wDϢ79 |h +pQ& .(FVQG9iRH2djH+v/)P‚M$ "z)6cB¦m/8VBCW Rf|]S5?[!/ti+s8D+V _>K@+Jbҩvdڀ*%YҪ۪M*qBǩHUJ/JǤX1;*ʊAQԳ/TK+CBŵѧV+JBO8Y"-JSafL) 8޾r/!7ʛ0y!<4dHAVc!J Jvnnŵ"RZ-\,(Q,WcY:*RaKj.#@PZ\ +~VvтO (-] VŞPQZZNō'uZH2(rvmFdMJg,fNq\K~ZhDB/3XFwbFZ.?]ŜKrnR PL.xyr9r&fŸh]:2F\g#KQ%cqFyv~{)pп #v]-ĕty- N`E^$UIH@tNI.\Iݥ>(^*|"F^M&Kqb=ѳ^<|/qi̾t!\Ks7/4ĪRJ`*0Q]˲ U*Shئލ))c 1A"!<*"+ 0m; +Aa 24r] +qbj171\ $Z1eSG1 + ZL?1Sp1T\g S1?p1rsm0t}Qi͏mVy[Hy|#sm>d/OgN=X I2Cdg+$CIe\Lהp2֠ayf˃:31yw<$ iM5L.mff*ֹCMLטtfvqavcynO=u}x!aGu4TԧC%4  IX!0brH3948K+f~f4OB #5M!Y5C\hj5|178l55cߚ׹A\7,Kx7q60?!ȅmŭ{MuȆGi6:6_ƪkv/8k1l5T,ו6ZD#NmPC0릁 lcU44nF%n|Bawrs|hg(7qF]7fvJ3=_ћn!R2e2\M6UMdHǏuBcNadᔶTXjIضU۵·C1T# hw[VP. qˆq sk& B1']F: ce©!=rz!:#./2!Btcq0rL I16]`;sTȯ9&(bu꜒ݗ +\Hsѡ0[p1:ϻz-QhU LgZtJC Nu eu' bc] - + ;cG+< +*ηcv +cddo`;ƁU*i*l?Uh0*8O;Ҩ*z +r5)h))V +M&hRw?һH݉(dD(5@NqֻwPb1'{q K=ug)Oxv@(X 7P67! @<ǠL7Q Kl&J1=/k4k24>kB DaEsL@}X/u|OyʏC\ϝ~ !GfpQ~?+#@矃)J?NJPP?DP%e"~$K@jE 1$ 7 0tH ;(7Q0U .:(qεj0`<Fp7m H:Xi"XkeiU>L&j|u*`ʠ\,>U|jr.4b:j&wp1JAPW[/@Jgt:am݌EpZFTE`ZOs9@)_Y ^*rn7RJ y:@i"ćmƣq+},2bç"uS]4a‡$4& W XTT$ muR Ri1pTWPTtJILMET^|&"ǃuYB3P 5]k"u*‘1@Ij a4 [ %BujM a!pT&Lsr8]O<ruԒ<]1 l BS NDP$T5k"~uAq<%ȭ?*8FR']EȊ.gaMS +!5ȦU\/!9s1E2A I{ۚ呙,)Ex6vW< BXBaK. nTu*"=! @`+Xځ%lFGYLؼr$i`^^)yʓjȍ ,Fv EU C>nlGE?i8P>4*a9 | sFZBd^CѾYhfL[BXYRN:Ky@jA4 }0 <Vcdù|-9Ue ,IƓ !H|]AVu. VS4HL3df #y15t'`=CyԠWR_hD5_"݀D2fJ GMׅW3 +ǪZ  =F. LzVk2(&.HW㋺x"R(r% 2sy+mOquz,1F%kb 5M0|_k1V+ƯsWШ~y &wXNJdcQ tUȝ+5/=T!@Ƙۃ^Е+gN/}0uq]mJ߹5?s=G]\@=|}>/Ti ηiՂ}bphgt*-u#L] +?0`4C`~o_AVy +fp춂$Zq!~VU: ++`\ +[`s$d ?KLxg@.|̫)K_jO @eAS6& +kZEJ +5Y4: P 8{HjrK%wN@~W]`)n %P4xT +2N-1/vQ.oy3`&&apH;T,<?* `Nyar;P҈0ju ~~K`y24CWd[=PPU͜Z*T$;Z5GBwmNk=c (:ZN:At@n +LDH슢Z *7Kb +0a2 w6A}LKrRyKJ/ +A֋HgQ G7A`eFt#u@yX 0/iZq-N~R=6.}Vg`jhyb }] +ŭ~@ U^eAh/ 8_qW]@o'q~plfbaLXdRdQ]J4vqenA䣣% )B F8CT#(H4n#V',Dժ VYA9&|2Ze>e)f}t$ZQP|~JA`P`aeGY*^$OldS'm9ƿGVkdM5-@򟁪& (^MsbA;JTK'Jna8EH$["# qbp# 4E³210@woznjm3cDy!\U\@>sGf6P +?Zԯ@ NI*cmؿ,NSmI>`nvIPߘ. -8@8%Ƨ)k8f@[2 G bc{Fa̝d"%0]J]X0Kr) +`uBMh0]Mp# sl|0(h'?3qG r&6ʢ`^u|6ޏ$K{"7xZgm!)Pi&_z]#=o$Hu=ѯ .Ű)BsR!S˜οba(z&_1PO,'YqXY~V ʿV\פ`n[$ `[5~C+_-)Ԕſgh roz26x%j8/lM~܇?8+Hȼqoc؅~]6澿' ̓~bՃEGL9{ۏ S<xY2$eE!ȕy|!]_W"2\Ul +k?lT^?5v!/•h|vJgg~#,򦈯-}<&w![m~e^|c%:Lz>iQwTz+$=Fon?*,~mIPͬ}lG)5ʷx,P%&%-wJ\ mۮ3Mem>?cE% }Bڌ5\꣄\B}nSC zA/03Tc(szPNG'bTИJ~.w煾bB/U _& _ ::}1vo!>mwY (@Ý΂m0Kd~%|ѹ_8QLtq|hG痒To*_3h"l`/٩t9u#?刚$l٥N_S<[{d%xe]W +?WfFOVz+t3rE%c#mu +xxF{@b)NB +BC#q6/%Q@f HKyX^ 』S R~V1OG^P-S&wܲh͊UoYh1E&j[#e^8>eM@Y mZ>CWG/ńu1R~i6yrw;_pR[KFI=0ı ĝy g T"MHa<7µwIOv@Ms8VS8Xn![/#m}^KK|C\MXnjDog<)5AͶ,~fZ;2 _O­k(߅:zG 7K3 +_k%2W$U>x[wrRN` +lw{% f&'́>A^N:B>5$V̡|ʬv5w42,w^[/3Y +lUJ +q_9"ctڼs, QwOq.ʺ(b޾ZxRN_Bv@@xvlF]"-m$O%'lgcQJHC=b' e}kt +ަNW!!p|+Q=r(头oWPEހCо`jEɳhp<7@U~ҘeoA>].Q&;X?.d@dˆcUЬ"ŬOuVtNHgz!bi#qJ1hRg!8J]S^ԒI{p&/pꌿ20{u#HkdN:FmDmVP~w|6M&J?=k'r׾Frv4u<8(whA=] +49'Quaf:ztȝz`1#^AvԱO:Ȇ(鴹0zzAa bx2e 8ȒTAWAg pq[C#k4x/Na,{8 +IQz9\9~hۤ },sWxtDvژHO\mMcd' +t>7#D0([]߄䚌lѢCH& +֮4udž~ZWd6]4,0t +݈Ua>@Vl11C3>!{DțuyW[zksn>Oqf3~7WEst`k);K<WL6/Ii.R[ٚsh4%5 hCR,#i7HT14*[)Lv9r.Oy23:(2Wt StHo̱rUP2dok(ݘ;xnYb,gO+/'yԯ_fS_KRrL2I&?H+}ˉ4_:{HnrOL_~bM+iGF/"' KljZUAsO4bg##UaF?S +aQ"?\'0s7{! +0H +`,j H>w!6KUdD\{1/r=D;\}9bA $gg9q۟!20t71$sȘE7PϘl}Oe{{+\3 eon.y[$3/ ,(ړ1/T +{m2s X`q a.-nI$o% R4- \ /X6 IL +8!+ aGeCv0?M*y7cW3رei8 ;f0\>? vj0ZH bPxI6ZC=ȰɘpNg R 5cAo$ƼQq eoj B̙ݨŚ>lzV`~Iea(2Pek|.HYtVOxX"2up2"1Ɵ)0M0aM(kev".\@c"z*.+>',VFn(OhWĝ'>i&ItI&DX|޸d=~n{>m!I\(u緻7 +endstream endobj 21 0 obj <>stream + ^(؈su&L{dIDc +vB#4 +=`=bJQ.=>u?ʅ  W(ĕ؛w"$(GrJn5=@iJ{g({_\6{n5K wCBKp*T&wCg k-y?7 fOs\qY ^y0i +DSٳ!&:W |Zbn. k³GNnܢ3s^.c#Nz [a.3w]pWPڽ<&,hFWonR9—N2{:%/<~M\.>XVn@RJB+\G?Ʒy/KSf]n#"xyOغoSBwط9xcz 3?|APXA{3pNi7uf߼D)r]a%Ddޟ*9FTFc5lH)G%#yP[gFH>^FoJ#]RE 7'~5; t*!oKٍk!J1_onfdОC5M a:xw]pM.v7Ѥ:wD'C媨 l`9dۍ #hwl俐:V[8f[j\݄М[>u`QB<,$XItun@.p箥LHFcJ嶂շoψvl]ٞx*?7 P*-fVBUealI$pp ֤2Xlܮ#l1j~bJo+w\NJ*IM,o/ ݦc9a=AD#7ۗ5ɯ;䀕z׶0G\e5!vdJC&:'OO\E$s7\ш~B±Zpmk_stXWvj=u+&t +Q; aXmoR[$A-*\bӞHJ] ӆ>1.Ba/n4iO:ť +wYXpE[SOЦ^/-00uRQ],t0<%ٿ4ʀa`7UD7D"JYjjH)v2[TIJ1$MA i0w ᾪN^z60't `8){SO, PTd$z2CmýȾAd% T<-cLyYލ]0ȅ\/chjC \gr:)65`?ĸ 6N5 łKpz~a㇎B&Gl'0ٍ!;d3#_{,' lH 쩶(ZysR{zY|%fl96Xa}+t +/Y4&%^rr]'h#9%x:T0\i?5`PkK\qq|f[!YhC|k4\yn/y2U_jk=$bk$/O!/0|tZulQTG@~W1&i0J9琙Y-E%6>n^|!kB%X&Vu,uhB!P5~WEryNb곘F9VTc}gH~=CV|Cei?2 k (Dhb +X+gK<`MFtu2}Ht'ۃ~#8XZt;Xbso?X_BI ?Eż:h"-+5hkIxՎw1}]=0bJXO{pĺ+6V p/xX5P H5YKG]Ԑl/_UX7]cvT¸Bqwmdiv *`` Wyt)T]0@£G8|Ge3{?/Xە%+ jìPʚ]ez '`o jݦ]ƴƬN@d 1ȳz*izW_{LĪIJm9=2:v Z-1y%UjTQ:pWköAU_DO6ĮUaDa\ ,Reve^ +X/5խW$c\"hOq/$kM.((g6PE/c4F,r|Fhlzg5uXmu+f֐$!2FEZ(P<$]Д-2)vR:1s:J ˓)`\v_iQsEKՏrťy44OLǜ Ɣ)2-+R3ͧi8C_WM84w•nCrñ%>8Ƿ2FoBC#K8wQnmQQwS5-EwmQ=}N80R+i6:$u )#]ΫoOpcxn+ +q8Rh&9䃱ux5`O!"Rǃt;ξX+CQ3,Ma<,ZZKqbE +"8)#NƖbs,|,BV5K֑M|l6Y]c2E,˶7(6p/hl-̡B,,gvt!جgW|Cy񳬛%0mA sxpp'E#E{t}:=5L rǍih-9uUs^g-h-i ¶.FdlMNTNz`bsgYi+ LkZvjma*y[/p2m`nycwn]QԽ=ۡba["K@H$<6ClY$E\+qi'" ${Q-?MܹK'sk #%}D$Ndt!kt UFO$8vZ^)OL+=nDclp.g!kI6|7\* +ocvxQ oy)y N孷% 4H9x"TYP/zg?Tv>b΂h2 \Co Տd}yoH KVDVX󦏤W5 +O$0@7f̻L+F}.K>,#X$6B: S`qL0ֈEzT*Mw/DE s%qG Eg%l-'|YYuT)mUb[³Z4Ml40l94,w8q9.@ 1f Bb<1-ZlY08OW5\ \_ͻ, ~r$H_1U +u&_lVv=(eB fdf104 uԘ#B6&_Zq7_B&Wѡ32d]Xyb"{W :1m`c$ + =;{1wvMoLdHY?q$mf>Ah7O߄y/'+?yxAt?,a)rO}2쓧Ya+]eJ!b~\^*?~Wc{[_o%|lȈP15D 8y"CDE2 cx+C*1 +\jL 0cq%D`>QS>ċG軶E2 7 + qj`KG`CAW5?@|?v|H'HL [.Dmmȅ"09=7%NE!/|p)bAV*YYa5?48aGQKK'.5< tTO.t~?ه +s]n=@9Aj퟽'F*<)h|lP>y#X:RmF +2.h! ؓT;ndaNj1gzB͖&2S,v$`K;HCKBFȦ'$\1Bt ]44lfy-{,ҭ +9_*ʮBhf|m%+/dse+0()wkGRdȐr$!R@ 0'× "UCO hXɂ=2m. 9!4]`PCvAq{5|M=*3不2 V&:`p(7EdhiQ_a~UwƘdȿI1"#lK%rl{'*1+o4ȃdD%6RxQI% 򙓛*t63zVoȅt4OmY3n9X,2uZW5,2xd?pI$A_#% l5o52WЃ鸚Z.3Gz;0]#Kcv|eG2}=\C%'J)l$NX's2Ms~rvP$؎~ɓǵH^^wH9FѠ$rnpX,%) ="H -"eHYV=+Y[ևf`N1L(W!7'V>nD*?:lf9gi]>48?+ ?}A4їVU/(нODTQ 5X;ROhAit1o cX[heO)z 'L iXظ=ނ%C>i28+QK#ȷ(cQ)F. Ce:i7iֽ3N |Pǰ OO]>lڧ"PB[6Pυ[^W@8}X]M6)l5! E~x\M (Ը՗QuէRQHwpt?~mNCIzTptƔFp:H k2$ .Ut;*̈z +Ar !jG,iAPB-D.$So˛hѰF` +Fu F ɧ/k!4${nR'($ڗFH{!Pu,5 Q\Hfe>| QT}PPA` +nZrYPQZ0 (܍pj. +qHNnHLԮ!-a짧 TӞ٠7/+:HJT >Ҩv& i⣸9>ӼFDZ]Gk$XͪFE /רkGj@ +R"0ciȉBPuUU3\2VkYhg"Sxaį ֦U!kEm*B ZS9ӥw%z[[ַ\gn~kxPz} S2]q&LQTbؖË"EҊ-@{2k{lmdË&@Wg=&iya/E=)G#9OS'mx %ڧMjGo!bm^F`ߵcKerb۴eEٶ7P[&MqPW06'Ɣ'NBз%r2󂯃N7r")2͝t(j40ʾoX~;.<ᴆF64wghw]ֺA\['S޽ъ +eR\ߒw1h&2t\{;&5wQSgW]MuҪc¼!.`u/G9"WTp5zug*cC V.x9-A}/={b@vaAXR_m N +h:$V0 0 +xJeAj&)X}:VPK$I/֜g>94`xSA:4HV{(pFU~Y}((48}C6bqqJыQ)l@*E2I ++-U?Sr!=S2`ʒʆ>z^)pkxāז|unn + |^]_"'fsZ] "ǭV~W\6F 6gi&.*x @D2܍(+0wt_O4gه*Xy/cRYn\HFAWvq8tO9[IPo B&~ NJϛzI;խ¤$>vLE]U +(qF9ݒ̵[fnd$-^`/WêI''#xlT ҂?r\b 0 WLˈGڶrϵw R)9)B)q@4ÊQcל81&\I͉s=HO*ZzX>fQK_KC4"+_Ȕ[|95[+֟5x]C28>f\fl>f4eF;V7 y8<-}Mɒ[Nkrћ?Է՝9 \u`Y厫9S{1t9h'ߗC2Mx+P4.f`a@F8o;xƛH>8s.^܌#S.'rY:eU~d{Z[Hi]^IHQϙ㥒׶tA챌ᚮTPt9v"8-Ow몁";ܢ60u,kҙ#9XF(&묰؅mn\`N2~\۪׭k@9yG{Z26vY7xNmΖ߶W{fFPZ٩=X۩͵m7$h%=^!b rv0xڝu7vQ-~ǎwfzO{4m2_>k4drwZ-/՗;5x"Zhk8K /8A^8N7i/aH*[b46p^<@ׄx I@y_8n%=*mMO__AHT|tIOQ>u൭Gڮ_KQDf?&9ZhlFk$AWco:vCjam/އ +f^H;x(>-,6O'NP/>BS:AΌ?Lp †(VuQ'%4,ͰR|DΏ*)烀>3tol 7Bjfɬ/]K`?{!Ϫ5j_~  N?ȐJ +RƤcl ++ fqP>7YfxYmΏ>bӦEp:toI*-: *p`~]#MЯ- - Ƽp +%~mq 2YǷ0?9k2>n"п4ǚ + püHFп~=[N"or?`F@i×nJρH F"~ :V6@*]*'@_p࿂3VhF 6mYnj*"`H9P @\0\sP*-`CS{S@.,eՆ-U4Ixe>ij!M\_<h$нj &J +7vp^,.pP6iqSy+p% o d?#ge4PeZ,n] +Pp} + aM,{+o?$lTӌ?(Y1vS<˔7NBO(UѰ=j^&Z'ʱ}~vͭg ~h\chb@'ϳMɰ5H̝5r'UؑAF̐Cw~F۞¬ _7bkc +5 @x}QA׮wmwWqQ7$"ȥD!!G5Mk.V,m!6! IXM Ma+5ݟ)ac%rF3F`O}2xe*~1gHps$B cXԠy`8֬Չ=M8UFA]!clb|s;}텯"-˴;)R+hF#z` !Q$nvD'z*=rSe<G}MF}dF7ٿdw"hMv10MY ~#!!BeXvٲze;+{&mޮXg+WHuu]3&W #bxDP;|cټ1wf.r8,:(&:5@W1cŒs>I"?`quV~젃?3b:!wO8aGpW $!#k#p(C`ݷs|}A Ia꼣ׇ2 {KK)M\D}G;bO7xhPRW&tCݞƼl;q l]4Yx +>;@Η6$$"^dC"ӺȲ!;z60wkOmUrp\<>g(xPv zɺ(PG 8^DŶ4}sAө xy ?{)Tl#pmF~bD qw=p@?V$lkΡ}<}Oc3g8U3lŮ;7๚0{"{fB7:: 'oo 'nfrv5Ǝ`DcG#E=eIZ GJ}if"MR*&̮$ +><x[:8k2:ới B#ֳ|rx^*C 2*Cyh@wt@Ĵ`tf`b/$ߗkeĹiU[%^FBӗVA_ &$9{ri9s0xh4ƲqrҪ[0u#TF~"LO׳l];g7t_ƏmR,( LJ ,F{!y+Dm'VF?墒ƚI7XaYPEEP~#q  I߄*H3qt_g4y4pNI5iZ#:ԯQ'#o> ` \/ +Mb4 + L@s]иe;v=ed*`+~$ THV50i-$ְ`]iG` U/m&Zax@~ ݯ5r>[M;yj+ʝU9hUC +HDz-V O*f +3.Lټ# N,~ HF!*/x6FtI>HO)mr}毭 +R8)4G 6^nC8y>#^-F=}']]93El La4P\ܩ} +$U}S_PBOHXzßxouh¡#!!,4jbaiqd_ݔصvr'$1XdE+"\C6k +QJ* $_KkpgjEVPKcW*~üΓW9=~'U +Q# ; JoTGhS(*#7vq^2ۨFsw_Gq +YCvX8@:/W}mYhPE|h|h&"i~oA{'1Dx]s 8{^ζA4,\-#f0@[;BeQ)wO<>oSH&_Gp'6$nl4k1Z}gA;q弁UQٹx|^SVY:4iSiGtt>XdYhYDФMdi6(zB?O +zmEƐ;x@N8|vz?gy~sXPof*E\o&}yc^̀%-Y]0 ;̙CD,$N>W18oqN?KFfϚٕHB5qruz[;2msL2嗼/gnm2lr[#i +j[vQelKmn˝-3l `- Pa˾}[Wc:w+׳mMY{ӗܗ{x;-ð!Pk#Yz>ow"ӸwFTͤ굓F ?}.zOތ۬ÈWuzi3.<)H߯!2~p>~uMW ŻoipeN~͟CxwQu6]H.4v$B;6>|FeǪI<.v/ceuĺ K`XZkDJe"JC{Aɨ(A4|#q~ާ|3mx:?_̸0{h2,F';ou4׍}m&c;zua\ ƅkah30Q;yz*&pS{~?-gl[f-ç&.h\%y;z{GD K@gf@6PB^3(sy#?kd1]]JG5q2g[$D),5fMѮnZ{ ?5q~ϭkqx5#ܮ#UE@:TMh9;[}"{,<}ηΞ{5rSF4" a]!;z<|[w:'aW2A\eH=d*^;RO"~.*LDeEDƚ@8/& 2Ф9<|@vϣ>z6خlpmmڬ]W+&,?~F7g&[8Eam$zXaAfaMYX7ECׁ.N#èףJ0z>B:v?&/~3ܗ[eٸZ.mX6ֺeRmG4 %k؝mkpw<D!L`km +|-ց$lӪ}R'؎VDE8޻~:vǑ~N8 ?H4}7t]GUļ9`<T),=ѦG +=K߻ 5xNϮylT%CFbq3}mJ^ik9h4Hg7w6DIUN~*oKBEsyW;#IױxXH@k,;`a@_:+'FeÓj,o^I8޻~cH. SoB;.ļ_'m͡u{>{'1$=9%m~3sv_RqEy=YO)s>}"Nb^q{|j=?)\W>Isyua\"~M9>,V12i$")9w'R'~+ +NŞ}~"N^JأL< ZX +N@=*oB BD<*Ģ74 ?Ѧ',F_)l7&&T}@ϣi8v~'1 Yz&RDY9xQmI`h>yѦvRM "LDZ?%=4Yha8޻^C(wl ʿ_xQ8߻󭳅4l!oYhy~!/gqH^Mҙ/o s&O$i5s]]3XG !M@D&_I2~n4M\=N$RUn;2~ߩue~]I`/b~zUI6i+}~oKtT-2]H7*4 dyBE]ɿoiFWhiG#yPd {~У3zpQ4}s=殭 >~Ҩ׎hO J@OvNNi}ZhCHXGϦ4nǑ'y~#?} PvE'5q( }>8o(g(GvR'ضY}e3f,- 6p;#wm?}GlWȻh:>Хi8aw6]{cHwC8ZFήe;Bwƅc_и2|k5Mas0ujtfnnL,Vy.rg q5Z`| XksyQhsgYbםquu];`?dY.R#}7`p 9w=Re&wb]Pwb H^smh}E%BS?Zßi4|G}hN*V+WAH9x[sygq컃(Ŀߣ. hm@N~jM2}cHcj@g y9:x~Q2zm^ϭ}"KJU>`h'x[>(Jf4 $xWD_(3}q\gA;yGQ 6tӰ pB݆p2~j]H2$w7tdTg߀>u/w6Fi;sIےra\ms(i +WY^ڗ{[v?}0gօ͗e.k2^k#q@5ypKfmkyW&sQ2~ls ]R庠qx&ƽXlue.7+/w^ womcQed7t̵}3y?~/,#5t a]$Om5+~O㞧)|o}8xO$'4@@`?=׍~5/zGfa6u˲s,kWsqtu^ߑ2hKP`/xje=zu6XV}r9ؘKw,;1i\E8&.i[2do}+6q_68WC:ϡL޽ѳgni\Ƹ]&Venl'˞El8޻g<{QY$G@oyhuz!Msx_д/uw׍?|grQ䣍@ {+O5{~Ocލ89e +O8`çFe4qҩN$mL\-ӷVwm3B61yn è{(̻ͮue<>e5zȥY/tgڼ duMӸ}ļ[G\וk4ov]Ty0 =tIo ~M`ܷ)5sݦl&Meb.z.E)hIXTKˊW`q +\F]x{˰CY(Fa\@Xl5slAqe-r_T1rsPI +A%.,bwx*Y1ōo@vdp=!-5`/vKe`X74yŠ,f&uyjzu A#Ef&F'CK@;mQ@d3AHiTˌUDB3#U'!u Lz{l@@Fiɡkˉ01RfB̏LH?x~~2u}B穢Jf-fKxC)RE*=[̍b>"<%e療J['*jAt? 6 +0ӫgG!DJ(\CA(HwI9}D(cM"&9)r&ȤVs1 +2q $ 'Y'Q P7@xKz̏,a +{ciƉX3u+'@R1 R1@%bY +i-@byB`(&erc(e,q6IDXJ(+r#|' +k1y&56o6V-0?:arK.rW.rk¨FJWӕI\i$xC7;X7?B=D~f1t{x-]`,QŭP%FLd~f 98d%JpZ  ()praI"$IJ-a\cXG-sh )qB,IIcF7rwv* u2)[xv;Q2 )AiIi$G$bMIX)]"Al@%>>6ݓ~Hb,ĀJܦSG 0>=dhi"pH B"!0 L" F"Wr6 LQe2 X:m\YqT9%|KbƐ+w+l d9'6'hEB"QEa-q=aasc]m XTe Z\xKtP$w(9 q$FĝE^md~ ;Fy〮gEN ` !)2 ( `ᤸ8Ӑ* B\?2ͰaBEI9cZ$,6,'p)Qj;Mq}nzp\ Psܗ7mƉĮ#gQO O@tJdܿɈ,CBhY]3%)~ْ?3ݐ=ht:NRԐBlMs32kZ$n(%;iHY! 4MO*u@9{ۂ2l/@kФv0-(XS0$X ʖ҆3NW'rK8A&Eer'*\+k:}0xN|3,4h@#_QH۱9 ()oЍ +ZȑO(DdFSTZ-+ ՠpN\uHdvJn&3zCFq',j4"%Ԉ))q Iѽ"dYuf#s4Ky +[eE={J%nG%[<QhpC>3-~l8#T`KJ"X@t:ZfքEq"K"NRn2-a̸7qq9L`Mm*/y + + פE%2n +ZP + rٶm[jwT|~RАPn&VlMU>QDqbe!s&dENT&vN(ab,ZjF,JT%l%}`{B)C& +Pn*Dސ%aA/TI3\YŚ2(ǎİ8 k#G7 4IkwÖ@ѭO\r n# 2`AYc8zh$}KI- Vȥ6h2vZUX86-tf M9`\0]Q JIR!dnލp'l1}d1S$boО BF91+1f[ +Ę wUxt6ؠXd3JY~"w0 3m +~Bw.2r@^tz8s¢;[*9q-VmCYi)'$EpIa+͵RFR@Mdwn}rQRc )Pcf#5^[46'7|‚^QQ,/hn,d(S";"ŦMH`NTTU[$ېh"]X/֤ՃUY]E>gfDEw;wd@-r ƌ4: *ΌMJ<%3IzqUl4Ԡmb̉Cܢ2irFDJ2J?DI +k=`aԀ 00"7N%d `+7d +ઍ@+K(+׭*( Ja`U3$} A(CF` q + ZH2:c:P>'2? +I^%D=D}*,̕ÓΒYkOa3E"ۀ(♁Tz1gLH^QNg&$&zU) Hs~&z(T +_bV&$>}N+}/H!@$?- 0@feACB-FGGL>"AҰ} Uf"ML;1~iwi&٥RD~'ݣgDe4nUE$KXb*i3n 6hSIKA3& + HȞjÓ8ߺTZ`Hi>$jH +LHU/.Sk}$Zh#| PHz +A>? +K6_V/񽐔Mf_ +e<( LکUI/ !|PJ`xGxY1:i@kBQ:vh+ O$k$Uo}8'ۄfW:7H1FX48 KJs]t 0_7<Z_Ff*Z=Afp +NHTŶQfЭs.X5Dۏ *T`v(/9h|Ά*̚N"L›>5^CJk IDZ5<@AȢB'aPEwhe7_Swx_O~b]E;z~G i +ޤx PDzqu@ 3#?'W;Ma cF}-c0>-49lI^I=#;(!RzJ%$eŃvێ وH8IHחT +%aO4*K2|k5bYZc蔓T5aW±2:3P9C` +#bMv!g];&Kd +}@IsଵXpP~)4lC2Q?]AEHԛRPuJSדUyVg,=(yC[׋4 :*kФ3IJ\6He"^̈}i?%2%@`]t׈|x==(2!A~ݧT[ FHSE^B@?֌K{DgacK*h7Hy?.M⮒'vQ"u O*Ff%z}`]٨~BD#O؝Bx6r.3X6kw:GUC6X1|mr.s9`Զ9ydcG ^HҞI;kH$ ( j`v݉5|@|,ouU : H0&i5AQJDG"5Mnu@TPĔk-vO65pƐnG ƥg3""Y"S"&ƛGQF5!, ! )R058Y;c6SbdUΐusz GHgŚU\+PY/Pٯfd <<@v"Aԣ`H\k +EPg-R zOC*~Xcrɕ:A *H`oyij>ݲdt~W/WﺓPvP 3V[־u?@g#0vfsomøg[S"*›f/sm옻5M!g0%G R*3&N'$ L +Y70 O'R?~uѶZ1i7o<6AyBRiU/˰gǾ2dl xܛ}.E:iհgȤ &ûnhwn6fЭ#uzm*]iTov `}.P&#λ~M`O%3)h?;("*{i9 PLk#M? +U ޵?V?P_6/pg _swwfJ߻H޳" o%NpT6`h7&QwAu}1n&wo@#u׏O+feT؁(Rk*cJAYJXo BP)-4 ص9<|]S, kRٟVS&,jISK9ء>,Z38kp'1ϟm!8vw ܧ铫cb8Yo{6rߧu>q46a+~o:wzbV.f56q 4sV z\ MTe*UŮDK +ICC2z$P?Te H?!JAEΌvI0vh_G0 1xpۗ:a\C +<NgА z J+ 7aANZ&%NZ{)眀I[xJcȤZk=SVBV9C[ xJH br˚\`D* x~re߶H@pgB7/Mn2<0CG] +yzʿzs8 ӈd)H}~)/^X<4)us u*gJK}|<7G&>en]кۭ vǚb^^?\(EG.yT,O܃鷵2ҩ`\J [Dp2Vƚ[l_ǧ+ Buu%Ao^KPg*uԇ~* ĵ`ĔU68yMb߀&6 +ٵkhl!L.}AHdcֲr]-Lfn<6nVy+Ƶ6p ..x +0geFSgvܲ<7źB +ZZU=4n)(кEJE`mH ^URF{!M{|xrˬߐvBdto%)1(G)WeQ:'1o!mt#޼G+ PPe)\#rT +԰.l;m<MNym.qaϴ7z%q`-$*U)AY)!Ku87yP^`T16) rd +RLF>~5Nخ@k)3;52Nai10x \~nE7p8+&o;adjxhrN8P$"e +[Ve/-\& B;N"쀉PR:2nY +32,R12bMcW@*'h1*#f)ܯ*Xut& 'Dq>:8(`4 Yy&\枫; <(E٥V4K`T5ˢu|++Gar8.U8 ]ִ^܂s,|mȈ$3bk}6RHU@Ť$M5rf:]hF WD:CUz6 "p;x- I* }O@d*IGpFU˦m%$l2%e0vj& >}[mh's0@K%U0xje2q[A8-9`HC R4Ām38bH@ + G#Y\/wF;$B1`vEF?2c@wАɲR򡌫閂mM.37TH_C5sUZ*?J;Yc;ǖFS`Z;H:#~?ӆm:k$!QYOR8 +1|l[¸ nAۤb[ x(/( +,+d n<0?s*"5u|7)a=Qzh9,; H)diVДHS5.n7"])lyp޽#λu4|pŬ1slϾ/rr˺}- !8/<|pf p ^:zZ>l<2h ?cx\-p$ "-}ze A ɹCrBp!89z)-h/-r4"k1,F4R%xJ +cPK6%pG@^JD4lMdDwVD@Kt _"0wiu6޺e.f]K{Mi<ͼ@Ar`θ1j,-BraO,Gq8 +C*;0)5!66AF2HݵJ2ER:|sNlx<';:me]!Z"B3I![A{(Da G_Zj+]"LJtFN T< b`Q#߭#2 ;!-EG=m.- +.lҏ$*$$rY6gQ1w)J+ZM{@( llC8X^ŴQM2 (AĔ@ G.)x6E hR }DM\HO\CKTҪx4Z@ukG1߆@'qmKt,Lg0E-o |l„D5NI(;\Gl0N6"6"&t ) X`rN!Rƺ!ef:  KLeG HV?ЮVwd6^<.ԙL/C]3x* ]Vށ7 SF:Q fQlqð/WB /6@ 0-v,H&m"tB b!Z٧rpk"מcV#m.;'KziUE:BRSnP'`hWAP6Iq>2EN Ƚf/MbgsaAǛW\l omUK +x `/vie/wjb2^L/(9eq 1I:dg`|-Ub&0UiUAKPR%TA%۠AN"tj9 ļe(ꉧ ~LH9 u+6d-Y,6 L49gܫR /x,Ñi z:] ^ʒ.ov6Fq Xʆd}ch>*Ykr#֬B "r Hl7`+w H(]A +@D$t ( ~iUc`xTLY2Rhܲ2o<&WD b|+ $0yf$S%7'VzH5QVkh2#\]~z1(t#ĕh~8SN0xQTP$CPk솱o (P~ +Hm+baY#XY\w-sR)n +mP ++rV Q  +΢uYdښwr  r h$tC4xDr[V^ǰ9$K:,kz,Zleej" 52HK`2BrѼ Ż^/Q1-cO-"T>8fxA\QqɃme ոD3 HYߌ-!1CZ>\(/؂VB!Bl':Laج ׂ lx \~tdōp + I(kWdTT<&Qt YwHQZ +~\vVa %խG k)c}Wl[p`0q)_Q!R*S%G`2jt,v .Dz) +Q ?rrԊG2 ʥeϴe|kA?!Ɛp66BT81c+O\q߅1(17DZ11+X%Zj[ 1ipB`sJa(FHXPB8A`Dm x&%dět|x Gz>f~PUa$T#gĢձ*9$-!-m y#  C/3b֒1*3a)W-,h YGDL)ɴ plk F![*cI%dRO44c18 Y<>;V6>@n2di\ØdB ,'w81N9G̊d[PID΀@lE#+}@\H&Ma Ós ʙQ%>YDZxc*Zq1 X30q0)Ȉ3"_MD!^ 43 k\j 8=^m\kcph046u+g6[)(5yN 8݌a:̾򢞓xb\1rk5@&6J\)꒽OR@Axɸ Alm d 6Af-CM&] I gc*Tpӎ06^Cܘb!W (" eLq =<0`F¹-N]=q#tÛh7 *j0DGt;-Jhc!Zzcl k "R0hM P<!.6ҡl˜@`CkKŽ!*1@g.'jx (>u-hZ&%n#ěh|` KMlC&u^6\J +_\1(U `bQI!G?ޑ@R8aQ-. 막ot].],i |``]q߀/ <* dQCkIS \CAPD@pO0!&b n+t1!ͰWM:B3=l8?' 0>؃!&CL0>RAjܢBZ4< p8f0̲#ð4:W5>dt8J(d/9ea $~43}L ϏdCFHBl$iȣ/^-2s<R3tI% ,h;vy\H0V1M. ߮*\?W4q 70&,?Db&p5q) Çb|G I1@>\fqX4l{Z +R)if+hu5H?i~.D1e ݥ&nXt񕱶 +(&xYpHz?).)6j4&>s!#>sĥ|Odk,-͑|ȧXF|:"qs 5Q!jBʮkX,k`Y gC)]AC\G' S>C\N9E2B//\fp³eYq6;RE-i(?GC3:6x.88F6NCOSRڭ$"nĥj~8ėbzQpoC0=)f(^5RC!,h&F~dQ*qj &N8qn&0hr%mpc)cQc^\Ͷ2W8BS`S,!4Ĺ~ #~CğhzxN %BoPQP10H̿X&=plunwn&H 07q|j>Dm$7YØa0fb8.Y/890!pvnphkaYDJa%-4:^lzw)bS|c1f"TD4hxr4 \fM&@*ZZC|9y$,P4Ap]KpÁx y2sNox +2L0)\rd=If^%2sJ $҅I!K20 }D齁cSlCH&Xlx7֢"xeMq=D

o8>܈GE8bc*:~NlX2|@d$xG)衑C1 aulzw Q"LE +6Llon&HdȣsF M>5#ehw` $.d![Q9Jp[Le69u69MۜԖSVZ[UWTWWTVT[WV[WWUVTږ[וՕUՔVMUW.,6,,*,)-,5.+66,)6,,*545.(mn΋ n/5;5:S454.4*5+).)+,,*45*,(94,) E_0"Ҷ!9w];.5u=B8z&R0pi4e\6sF DDyG%TDuDLߛGwx_ "CF0ot0pcOpV*MX6:e-rTʙ)5u~ѨqRȤtLb/&1B5zgQozU*44cg2MD~HSUMRDy6ENE&ͥcTJY+6 @ mRjc)&oB 3*."8uŐ-Wk,U7bI,} ʮ}"^GLi=F0 i⏱YPM&F o) +#OCɾN94 2  +IUwb 7v~GgZy(!]=Va&$ӫ +bd~8w_GO1;xH'XPG^ZL\%#W[82;砙vwV]ch!Ac kO@|* Oc}鵟^^h֕X5.k:I{>X BQjFeCTN0".ksYkw{zQ!$kN)&?iT1 +ģL [@6:i ZlY3>gWFmY#EI xs)[]^lRQ Y +=b[R+-GhcJq{ KA~?&/9߼>B҃AJs&t:yC@,L;)>m+a34X2,=7P+G0B}@4k jAo NH XL9:fqꜰ^Y>8)XTE*o喷3{p)IgL?Fv˷`b܌ܛyܫ} >S*q0sgt6 f|*"?Os֫\HOveO5@+Ii]!H*]&ۦcڝ mq;Vm;^6WYF'T"bO5&QK@;.P*QF&uCtޚYK\#("tz`5lFTTD*K7w/_-#ƕ l*~{Q. s +K Y< 03|m{-άWrZw$% NPk&({Re F0L?f@EϢqft H{g +DG`TmNĠIAcWR'@MR'(5y:\gU/ZsxNjm jǽ6Jem5}$Rb]>,~Oۆpx24 tee wBdAN,(@~ ͳgA[ug"2xz(osZ7Be +r#l>aa1tmtg=n:wVȾ $uCtk: 1^=Q ۣqV?Ξ"~jML}颧Ja^t]9>iuIgbG}pruJh?BB%ADH݀kbgBM BP~r)8QJ<vsU)k\Jٶcl +iE 3joP5PA|z(¯TmⲖQ. ?O]zI߅cSЬy [-F|%TNu^PBGxB۪ťrqys#dzh3@!|X PDڠfJw6(6ULkGjQIh]@~G ;iNiu@Lξu2-ً- yż>i +j"NGP$({& *ZgT]Ҵe$K53n L"drq:zQp<dgiE_ȅÓpzّ: o6.8v +H4T)$ƟY~PEVlL߻f0ԩ4|pY~%o0 %,*IɔІ$tAcܴ3m3;.z +JV Z^2&?L`ځ+09!H].̜ZgCӋYXdv(m KL>Fg+3^\J(b*k%z<@kCU +zL$ΰ݄*2ԇ05%Eu&k]n;>$4+J?(6 .͵b8͡G#+YX:.*5Q'aC[Ӻu2l~{Cԡ7s5LjǂI[Ь^sw\ףR$1+Uj]Ix I~, jhD(MNeeּn&P9DwQeaHS[Yh+y"J`[Գg: ][A}] 3w6u]ASad!YCT_Hi0vjN#zx%CnB9 dFZm/"G>}b$U[c J/&*FP-Nv} &[3: ̛û^hW($ϯmy8c:M`c5pw͠MvlKwƐH }uV inx| +L4~!JAK;˓wz0tj8`èL}1xk\y%Mڛ3gٸ2?7R#3q߉6EBm쇙Q40XR3ƻg|j#CfO:-<}(]-}9u VI=4pxgCy@bŰJ]B6"?:f?cd5XqÐI7vv [uk"M/"wHL"A!MAB@&$#4Yz%(XVʅfsQ%YY3* ʿ,G^_#8a=LBȼguk::N/ow]3^$(tvReH`;{&!B;WRn&  4qt_fol5{m93&L[ǐ@}>fc +Y48i)$}.%Vq$b1z 흁CMIÔԣKdoX꿈~}IüVD\tXm NL#=Ckݺ'_2EK4)4g<7KWkg LJ$v RJ`}!JA;Q ɔV݂5$1~k.$(}dՌljFD؋vqƣE7W;6~ G[,(̉5|]Z bZw8RڅtV<~'qazFo Cez/ܭ#r5escظ0}j[XS.Xz^2k DNӫgPB|=^CwDvO$]xwx׊ _8? j͵#T>57:z'2N"Q#=跏 + pPS1,g [@h`_}1b)(YxJ< fLP(*s'DDZcs$U@Ĵ*ZIbBL6!^p<: e>uȦ0rh-5`dfZE2m3 CS\R'(;%a 6un"gްdUDVB{4nomjîӪ?~>|ğ#/vg?-# e~@dt:;o>:nmnB /qiWV{yBaJr#,u 6хBVրϣ_3rURm ]ZMnay,hPBzFl$Ew̉n)}u6=O$'4YW6%'8:>KY;0.Oi5##j{ 9RT5s+!I!BSzC[QŚ'OSRQYG8ʝF~·e]A@z19lw!\ HRH}Pf`gr .#yEqܫoVFk*X7yU1,k'$Q3*:὚GwD:'7/PAY/ IgukJ?OfՖY4FS*,=)q/SJ7Asv?m9Tŷv@}8ۘ٧'qzL.ҦYL][Ͼma\`PYDѴ6^δ@>h+(1,$D&|zh+m^F^ϥ $Ֆc}"d\Lb;m浖̅tfZE6b9M&\@"xc2orN70q<O;7oTAԣ:iW^{X:8%HX7*L=Jd߲I[zV).VES%(uկz6V{/D!Z=6k*{Rnr w.t#3̾t@4%)9 19Ěz +MDTGhaTꭳrpNk69Kwo2쥇$tr!ك4zߚVfM9mj+vZ?8B]J *?f.Gg:k]8@g&$=Tl]~o'(;3OIþЃYa1{m􍷮7`Q+!@88Ç&uB{fԩ4\֕h].wM(y>AfPsXɵ$u>v>u۰ݽՁ6 '?v%F할rq +T\v DL#= +d'~To;yD3ܕJ+dzhXT8w b\rwQԣL2qKZ'K0E1|kE:3Ị_䗺XP\#㈸~%$~#QĽ߀&majt"o<H/!t|K/cWBIAI1wi?JP~J0<>sahc83Oߍ̱3|mn$>=GWg>n 3}DTP%uqܫCޛL9X>`1I͜aR>Kb'38!i&n$ĝcsF +(HhYy6ELG?;q~q2 =iy _ ANu<[`/qf3H23( W\vMv4&1* j\oIgeBBˆW4 ͺ .ujfĔpD=`OgU>iqvNۙA7G\㺬cY9O6#7ٶu7;* #^6sO&a3ms)b6&[ҙ8E5'a l]hm.1NK(IUkܙwML1+"tN]A\8@i IP (i ɾ>6eĔRZ# F_Hߨl +u$T(FbܸD<Dn9L@t ƅKXqR8B(5s ]cWSHDJ~D" Um&hOy'gj]nwh鋛,%T ?}&,wX5&/Vs216F\ڸpxO#k~@d^h +F!ʭҡɝʽö.2"*)q[x}MǢ]r}^mp7NG@Vؔx\@"?:zW&԰ afĸ:x׶YglJ``e$1}j2ex _\L Na +Vx9q_θYVưN>AG`r5Xg-xvN> "PeU~TN +E8޼nsHwzR*~tay +)&z+stt @4v MTNb¶;⾺p|_д63.%971T)]2_qK2 ֈCESgwB? ]6fo_9'WC`F`rE`"rxEUMDrxrP4kpz@1 @ 0B)G-C`00 Ăln@!C Zj|缡NApBq6av)tlnjVvvɈ18w8u'}`}ϹwNH3MדڜML6NŹ<~:T&j5Ƌ1Cl&6Tun!Q+FţHt)t4'p3GUKEF:]&HS(äѳxVѱ,9xGy='Q/*W)O ; 3@Y64|S^ʉfOIf_In_.0YDbwKrF}H1z o Fq&eY/&j!:y JQdoJt\ՏK\ѷD5X4@_Ĭ?0;^M˂ʼn9⎺W% 5vƽs= +m %߾Ga}s:\gO'}8KNJ-@Y_R?+8wRzS5ZNA_].kzT FG-ɼ&DC 9ɴxe<}ccepAM5'j>Yw&1 z;ؖo`6𷾔1?+;QDgȓpM9Ut^Q=ʰi&f[:] Zo1y G4KaTedscßu>k5K T 7[}-"p|Z܆gE5s{YD`jC +=/w!`KrDY_//jxQY3Kb5ͮJ}2_Y?k[4ǠKry #̢ÉB {0ι\ SL~ag}x8g%U~Vym>dcK%igͯWt:<ʟ՝᤹zhWa]\-=O-M)_/|cìbpO|X#}UEQjE7 Y?ޮHڗbt\\Ǐ+z_WnxgG0/cm>w>I졫|\*._EλZ8䈶 NM'KDړ➇q]:ӫ34KB1Jr0^7?RO|JfYӵm~Ӌ9|~|xMzeݜw[0qaa|ZO>)|S/{O*M"kZgk0VKusA染_"e ٺDG hkk1/yvz7G iW#??]4G!ar?/Ä2v!}_ NKw ;I¡rg`zJuwȭMD-!Ak :o7_o0][1:~>|.J@%e sv]h^6pd-ވR~'>zɯȮće>Q]]V`s/  TVˌzyOEŝ?Gs _4&9(>D'Ze彫 eyI Q~#I \r@ne& 6lC2_$w5ҐNaė\Óuil]|(:EnRry}0å0Ȯ=r,&skyV軤~|G6rQOK8{el_S0o6,#Vf/HXgsԏ + 7T?;0]z䘦?2 +m oO0tmd0?TyEsTn4|t3y8`cT{nn@$!f|/ +o:bгۙ39k"3?&P;F?=Jj;78ē0O6>³na[9qBC⎾uds@}}o=wC־R#smc-BpAfxjەGOkϺ=C}g9%i/$r? ϬGl1hXcjSWW|]gVfpQ +6)/Lh97rtVb)äLٴD7,h NcOCQ7ۅK+G|A n#)U +/.wW՞;}gѝ=YsLgش#oSqy> -#o2ۊލF6lXJK?S7`m>j)hl΍Dj۹@ayףʑk;y3pǣClz5PTKClO04p)!( @OXuG̚1HU^{1I3ct446PzܵpO+3m`L7-+ӰX[ʓ*l<:Cxg%E^fA"45Xa?__TONLhA ֧Cح[@Z ̄D/[4A[6.†W+B2aRU8(CVw߹pQ{pR4x(U8cb5Dà# S/80-p$0%*CA!u9|J)(]/ +: f*L*.}᣾p?$N +GhQOțt!M@=-Ct'`"i^kCH1D'<(:lphLXFVIc]<ν1a͛pX܆]46PӖ=! !aGC?PI + +s|@\#.I 1`Hh{zkPOSt_F/BsuSɩuzo](1 Dg*>Jؓ(MM-1döMY iQBbT B́dUfU`dܪlO%`j$#A$Ze ):Yo!R\D*nlhO1}۞W- CHƋ>SNVCay8y7$bTG8og5?¿_ +{>qѓn °7o3= 8p?@{4Zt:1$xU }b ;f_o!F+d6*.1Y ]wjY-LpEGPں0Ce}Po!ⷩE5>'c.GQvOeJV)"ha fTvSOK5$`+h +i@ @R$if{Ÿ'b(hy9| aZ.T x,jTn})v /i6!2\qg6-:V>e7HO!,IYrFlh|{wd& ~ yd9˽@k'dc=;,3e~M8r +|d[a9>ŏH +QVVU9氰+-ѫispD +H_dKJ]|MŞ @<5! LJ' L=g)YG*P|\µCA9f\h09Wޥ{KRL ӌTY5'=X8 Twa2A㿫 x9%cC덷+Aw`*~LܾOT-h(9&#M.Jl2,+L(*BNҚ jy||¡i|\9cJ-;ƹ3bfueP̨pҽ+>wgRpqRE^BQ;F96X+OpͿKpaJzc F ~HU栬?m`ģyR">nGW_4p M'k9Z@Ugvd芘 +y#M?_-̡uݠ ü󬂼) icFC23i`4LT6FhP8FKi)Skb#-ZSBeL7׸9^aD`_pr4AA^D"˂MM"I_K=Yh +c0+cj@l3 À=1D^91פʟKVs%m\ĉ0H^z"l>d&$ + +9172551d9-9ef3-4ed2-bbc1-3f2ea79f1f84336d1063-e4b3-4c2c-bf11-1cab41a000d958 646.252383fe3bec4-4b95-4332-9b12-a4179f8af2da84f846b4-97b6-42d5-897a-4f6e51d87d6435.46125-3/ 1!"<|y$ tF ֤ NҚX3D@Kg(H<ހ% X{(2`zQ& 9z9vLv$0DO"rx4&uY

6UrX6+R%Gg$e jk{TW%(WLωA ɐ?0(rYUH([HozV$ʓ@$}K?Jr۳nhC +kD q0?zfg)Oag`BPdjwR"Ϩ.\#欺?[|'5[d6SJjH|M4sga˺Y.v ϣ>Iy?&eIr̓,C}\}|Kvy6JhȕRvW"4r۲0UtM`\ l˖S}6]4P9l7QrRi7HLY,!4Ǹ2@6/˓X4WUbpЗ`f.=%7jB'=%iM yI좥.GS1ZcU*Ӓ u,Y,,KB`K}̥=zRc'n (v-HՑ,F2jQ7xsښl|ɺx3L2/|ʘPΛ&_I f9J 1%aC% N[-DB=*ٓs-k"L¾p/*"f96_K@ur`B)BRĚ32 ¤(H!:dc~NYie ^,OJt( +^)݌"VkJi3Ә!|X b)lq: ˕xlbITjT)δř,iƱWP3|~ǤNAgIk,?ZtiY.H߶NZ98/,vE)U+xZ7J;仿" )[IYŘ9xXfs +Y!(Mr Óv9?rJ=e3`k"<\!RZKFd9Cd +snwc,WKE4Tj@1tʔN/pBegYskM-YJ'X.0^쫀b#$mZ*ZB`I`6$D6DW ml5 45)RJ)P~{.6{n:h*b=rIG ,Z8,q,qgdc`vfTK(4fyæB$}ݜEeyLǴL{[x{QQ]*%ȳQy$m:pl/Ey]CkOr0[BÒ+㚳-,λ. ֝צbg{NB՘1K[~ 纤g.ٙ<\ZӞ>Zx~]-ぬWT^:Lϼ L\0]5~wW[uJPa xաB4vkIS2sµT_:7\IwQ_jo`T!8餋UsHi@Tp驴̼z>x*yNZ-~ntcI+2ru~K}P"|_yu>J>.VL/C'/_^ޯyRO%N]Э*R]Fu8ypHHT2ZkW|M;)R>`Tv]jFUu܌;ǒvb:j+9)TKܽ띆aL0U;USN^n.^۳?fo5v?E}T>m=Fk`H = q-%[o6:68mCGblxHކ̈ثLBp;A_Tu}3ĴoEX*"&s{-۬zO.[SEc|5GdTmgut6ML>SD|vfxq[+Yz.w{*wz._FGnxHgzj{j M8roGҼk櫛kv܌,?q}= %1/r785<$kEvLs|;S;U3kDceR2rٲoxHiR'n3&ںz;f_tV!_C5G|lcEoF{;fe|vQEq뜶ޞ;O[n~{2r!n%r]6I!9",,<@ @ fUph9@`Sx&"mSyHhS yq[2_y_:LZ׺d (PD(D$<H@ +yNHS=2 fRdI&(0,!;r:F34n.Sbs恐}6>|mk~>elϹ;!AA@,ģzӲM%m;We6}K=~e~{fxؖɊxmi?{/]uYjm=_]ۻwzjɖ̷oq15u3m#.% 7{_-Khy)8&h6E9)pT ` $(",\A *0( `ygɈ̻yxhfhݾzx|ܬ|vވx}n~~nxnlwwolk̘v!5dhmk<DMZX%eRBa@R*O$QPv y"DHE]S`$|_0PQDlM@BP.O}] +$r(0&C@P&($r})LDAJXyT*"h*Il(bIByJ%zC<<|J[*DMfC4P桰` @D$Xxf9 @e(,GTL0ea"d +y$Da L@@(b!5`&3y(&x2 6"i  +B`s.B$@bBBHHK$H̃$.C-KLM8l*D0l +HDTPD*OSyH ` +*`& hH0P`(P*ȳHH +QDH6Ohx(4'IhR&x(C8$tT\L0 L˛ +MŃ9pHHe Lt6RAD(40Ry6 +HKwyPDjV==ʣUv>'M-[Ҩ"ѻRtUi +-oS#OuOj((d30{D-!"%+V﮺$ +M tdQ@JԈz"*#{; +e(c\#_(%&8D@ +I(~ҋNV+hMUwl|?>DkmIOTGYKs)ڛ=?=Q*f<ũ>I&.HM>~$lYUeN&vu-RLE&#O%PzQgd%(/ʆ*悩LvN$MCzhU1Fu BᙨL$q4Ȓ̩ș-dW*7\~lQ%sd$&@ODO|#8Zݫ*|ԃD2uCDcBc% +hDY*h2B悁DvWU\1O(qQM]m#9ԟv@RLN20*Ҙ$KSV+Uud 2펨]" Bϳ <&bzܡHc@JʐCP40*Q$&,@2Q"$`"Zx&8H#$r$QTqP` H⩐CiI^ B9X4zЄI56!^YV~3G4~,QL;erL3[sa4&:so] @[%#dXKO<@ISqj;0'fE5SVUU|tے9If2 iLfo%a*|+ vÅ{ПF (ƭ6c JX1bzW7,Ƨg '0E؁+M{m~0@I,,c(-:pjW%a鍒;Tc|2*XQUst>ogWSxLp| +> g#(BL@&LӅiN8nA,~QC6z6! AiǯHwt,L%\TVf6߲>@ &V_Eٵ`fuvVZ F`Ǣ랈UP:cpEP|+wqG8TT2U6Y/\ +( 8҉ چI!ps2o#;b6P$L,DOCkTf MӌMn蒊fWl>^3*ʂAW?uc }e?iKJ`FVp&+.)+,M6T' ԁ`)>KᙩDU;N~3o(y30Jx? No/FE`vlzīpNn_@ ^a/ +}q ғ|ڳ 1x@gqlY|]l*I'&gfvMKy*QKu&~#,Öne0d4lOE[Ftb(y] +X8z*5a +h&[++iu +iU6=#.~Om.G׫vRME2q$r$h\'I+  (U/ r&⁄u͜NTu@-1#iuaCUlP.YM{a#\73'zڄ(H +#6;T!E܋%4{.nxu Dv7u>ɩI7)/dLlP22厌Q/v+˭ +A>+aPt+">uFE.rpWL +1@I4pJRx(7L75T +ݿ\#;)O J`TGMC8 ߲ꎸ<3ca 8-#x&B̲3V:NJFfw@(9F W>K]SiZޘ gP=/#"~>^ˍ\%10ʟ4 9U0M2?kA@ȤLT7MmujذnA1)X3glU8BSIY9 ++ǵc' >@ Oc1&]f<e0ǏcKa(dQ"IPC=_*| 0{IH Rg5X.V+P(:VMIqAd(klO]KhcՖ.o`xa8 }RClY R$x '_k +$Emq :e*!/=J^c3O~3u*F?bt{>ge?;}ųG.Kow*'J/r!hLD hjC5R_)DRUjL _eNܻ=4r*[h,۽1_i|lGތ/xaf@'$:5*pւ^[nЃ:G-Z[;|Sͽ1*^t>2=Ac-2'w ew[?Uׄvi[͐L TТUAGR_fJfUnlU/ oD/7r߆|BPᕥk9MᦡbnLd*?qJ;ԑv!Y٨u1Y'h7DwSo +&=@9Y8owS3K_f,Xf>!n vZ.]E!y-dE4M4K`o"uT"'v (x oZƣFWK9(Sd{2! $Iˎ$@( " ]ӇoRШؘLvCdsO:68"uH9g:N6}"UQi|} ')'.p[6"SD]ç15PT?D8xdO𠺒WmN*Bx'34W 3 +8 ?d7qhU19-dļ|| G;8*ݸSƮ!(+.A Lh=yB$LƌDgBvbzis|~Ը=NG1Ԝ}ݟ^` AR {H WR47䴰^ew+'2m}|RGG?B 9eOhڝTb]Sc8 +endstream endobj 22 0 obj <>stream +&>VXmMv-.bH[&?At`OlM>6q~yć۱ʄE!kz^n$=:y9vӴZʂ#']\F#c!bBBUIP? qb> lR8ױU;Y +`G$_ht8:PE]ckLPsPv|zo 1R{w`AԕvD<)KsUT\g8e&5 f}y$Ig I+_uGg6`^ĆaLUxǵWWkD 8QJV~"Nm'FO7i5lƣ\⟊9VGpUOeǩB+A@#Xk`{jvi"yet{lHRz=:[c6`US8]=4BH4+1JF38]Luh Z[Po|(urc628 +n frO%haj +^"Gie5D6+U M.$;*&'6A$)~ו3APEjG`^Ҍ@Y͙h]kXd}YprIYAx$mҙjW&F/x +1|_|3$6͵HT:4{{e7ήZ]%F5zzZX_ 1q J.Z<8k'E;[/F#Lj'!4~7)&h@#6dQ1WL Z]}W3N1VM'$b>j@%aY[uGꟵ*$Xu##o%}@$)2>Ԫ Hm<Žۿ2ʒOQ23z̪2A0C'}=*W%l;`^ QNIH?K$m/+u}++Lb LYpEɟNw:<Γ; )g1-nmYo򮁗AAN$4i@É\4'jhnFڙSw-YƩ>7 4,8$jȖ51bE|+pdnt‚ LP&Dk=,[oIs"0EIcbgC +]w׼p&p5J3Jɔ`qȏq4^hfԎYˉtɂft&{'zr +zDbH+e`JŐ-HU +fm0Ady>C+t}ʈy'Zcx UACiP*D&cs/>`/~DX5@˵"N8aOt*1"m`hyZ Р4tBYgX#SC$vR\}q0A9o_=Eo4m㶕<!5{ڼ [=D@I0@B-L# hͮPV&‚b:?_D]v%-p"\b(W:lJ7]o57؅\HG id`>kٴ2+?aTmȢIc >y2*PObjcO抝H5)2B>bGi6m]YZjl, 3օqh)zb{SGGxXnxؿW +k"O ]K_;sER(2BRn. ק;|߄#wZ8*_5)B{-\++1KgNULu잇;/oF``{l5qxD.s nY0qޱ2V>TE3bW*|=d.nTO*;B ʞlلzrJϣWAeUn_K +ѫNo1ŧq6m +|,Psy؀>c&a3fA񇚘s B4KŷJO'l07~Ȋˢ6Ԍt=U!ușȳ겠w{ N\iTGltQCs lFҾmiINsʔDpPRykKh͸Fh +6 +F.s%kePxݘX Q`s3Sg")T~V Zo;#x<ˣ#*+7BK9[D83yX { Ή' VʅN׃P}?Cc=ot/OUsjРHX,oZMB|d:5T*E0OfB,z%\M^c,@i{^?=Ei?2s'̎G7+ `]Ř8[ +6tqL:׃8jG0e͖x2s8#+莹8#]fV/@j$xC +ȻZ>M:KY;ܳUt'>F hj[sۛtU hymOLZc)c\*9hO^M /{TG7ꜻY;uZƧ ҳKz ,QRN>!1UUc^RE{i t%b]^4uqEփJ;$e8fn际7ـ^**0V);E`B0-]Ssc/Ꙉ5"2'].V<;BH/4Q2 e 8 ѧVą9fLVֽk>Ǧ +sn( 6²%4"j> C׶j#ԁ&L"VQ!5T uPF⁆7d+}І^bYǁU*qa>YYt7Xj뜮]-V"7`}8WZȠ m_򯏰]ϚQ cUwd+ߚNwl052 АaEMVYKzu`-)>=$ m,M?xl.e4bS3Y*@d0 j9W~F0s#0Vzep+ώ@SW (n)wdc/qHIoR`a~FT; swi7Hug4Ԓwp1j-l +n_~t|;[ezEpIsduȿ*{D_-NLZkk4$KwJfF.8>&Nݢ =ʡ+RJM+\[?U5H,ZT=rw`}+ǎGUW-h̯v6VEUe/'y +}i exx+`*x^%ϭa]ȤL0y)(_sP \O1'}J'z&|-|U, Aʃ9BJcUڢM`>BCC7X\evd8i&} Ys>$Ί\p/02&m;Lكj}?L!}wxԢvi.5'lcSmޱlF9T[T!PSe xLNvĶ^2gY1>#mfMރ)59rRM><þMrDiUiqv^,'oLq=' h +4$8+sAR& w|A.L;8L |n;Tj!LĒ}.NPT\8lt$`),>q/HZ.rcM:<[A]!;֔|#* +rI+DA 9: Q:hDI,i׬H)FT%5*3v˰X۞q,r<|1@lgu0GUo|SR :_Nb%..uLr>Z9Mꁙ<;rWH~a!=re#'+ò&^7[x=QHf.1b,UՐ)ģ ޹Υs Ɔ}rN(z<ߖ3˾6cHpBdfd^5E_κ?ӜPT1Mp~@ f7׾()M;݋aQ9J*R hGA#6`#L ӀGh &h']]TVzX->]J7 ~=dA:Pu^va |/e9xL^U[;])ܻ4$(jTG /t:)U +$Nj*-'I{w`Ul3 n}߅i'ct&.d`C\(_t.71\qRBd:hvR膤.̾isXIP(@iurd^Kܬ<&#e%H1k\RaM{NXMz(!tb dovp^W3̓h 4RLj!y-q- Ț)z>="/$;mp/XiȥO[`:fij X 'V[,6=E/$2жj&^]%2M>e/J3gYuWP[u;̨V$ɸ ˆ_o@{Y\3)M5OCfAWA]-I;yȑwXP H*7<2vm3i]=܉,T[VRE u +LC)n3]}:[@(o29)Or\-3&i2iés?fg9G{?#] sǽr? 8?aҤJ榉QC۝"y+ sZ%S3[{H&sȇѕ,j;-`Lʀ& +w  sC=|w:j24q!_.6cIpb8e70^M +rWr"YJ_H3 D\Y%dN+kI`,mR11zlLJRU5L[+BMo>k\v + '^tf~z<fI!+S8~APCU.NP-m +%i4Lr,wb\v:lEɊ2|5GMI%Qd%!0kʭ &X΃eDl[nS+ pvGNP/ה@2 _mIGbmfNJ~ymmzEirNƮ'('2~nEo4 L 5)˅,G-D-Yw?"q:%?#5TbCN_b1&. aˎ+;A =36F[>̀kAt(.JacQfq.f'v2x{,b󱭋XN2TM:K%m`7" M&s)-o=|, F3P0k:81#h^D-`G~[{4=SFXOOFT`O;YwoYd\ +etyh|:7˽J +D)87o8_1U$^czImkB.$>DMplÆX2R +i9}9JҮ,"_H3;pah(9`bCˆ{ 7NnU"BE|X^iBdġf+Ih#yINlz~K%ZB'`_pg]Kj4X.ǒ8dtdvBd#ywFq2d,+fԆji2P̡@fAoq'`cTIF:P4zzƖښ b{R(VNY$h6u8Ks&|7-Rah^0"}HE3Y1~ ?|!j6=)tZR +KD49 |u6 +^= g\/CSP"CG , E<2T`ԙ\e4Qq YRr`۟~c9aI5G// '94gUIQq@qU\I*~4$|ATCJ#{#;E"H/ T 覀zYilZ,byTNS3"D8}NWþ^dEi zZtZ8[[#>q2#cK28aQ_8"@(G +"JIlG)&/f M,>qpz BT좔@ %>^eK0wjw_{,#qʖqxHS\9G@If4) E4!;j0Ξ{tùѷ) +w2{VPؠ_6)  +)UPҏAWO0۳yir{]L9a7 p fa^@bԴP^w K%#|p܋g)+2VЃ$,Ѳ@N g0ZYK/j){hNRzw2 4F-x/ e<+8$.>GS}MKCb5l{D)26!RhoÉso8uGL 5W{i-̭ӡdBŸ$i1Jeҗ [ =yIMDQ!^o9v~PFRkwP)'WYQ?RpҘ撪{||7]//} j/#óش\'WpAI]H3pB8c&3Xb;-l|ݦ (Z$DHhhM>ŚtȦ7\D߸<5]'Pk72*[py㌷KSS +,R8̄ @N} B.Gː+RV+xUF^BSÙe +oR|*r i81 K@ JN;upaQ6"%1I% ud YkΪp`'f8lWs>[wDڭ\j0?#4ckm=W8=^)r+*T $c`+u%d@ R³|a1g ybxS3jiw D+PcVy4)p6!,H9UMކ$#_ /n&54beF'h8<ҏ{ؗuī骬0@-PHwnojW*'vCF q!F7?, aǕe* ؈,e:pC)2ssQ̋MhW6HNEȌ#dqMzǿ)^0@{L~ ]ȡ!WY&Vk}«۰g <6Y(8^P^AEǺpHć,B,KfH!H ~Xd!fx qYQ%']ԧlPN^c@Ug̐1hcw.LJzu9C7؍uck2~,Qlsw'7+y;SαPl;? 0x? U1V*mRZ`_ʎ:?l䚞"7Ix@y=FJ=Mx'SlVHBA:) 8S/zgO] +S axitlj;1G`3xA8h +lmo96H|zY9}`C嚐EtfbxUEᆖ 6;Jclu轕2N߁'c:>ɳ)>S0T^$& Rkxʀf,~`Bq_΋!FzCA7~͒C.,+jX?!rh&>NhpH㝝֘8 Osd(BcK6䀜-҆ 4UJ!D HGk&% +0*J b4z2LR'0!4'~p Q+5!>J3`bL@#$K&6IOΆ1jI?2hQ2t GKsu$<2Ad~Jĩ|J-mK#\m %)~  ez̖@c⽀"(B6,ȺF=#[tEHo+>'pnc 7U?6$fV)vi! ޢG1sƎv&E4J踒:Ġ+\ sE% E 1_:Dj)r3GVQSF$D,.ZTJ"e]jpZu˜+ qiq#dACaDY +;1 tvJl>_NqVJ[U۩\)N`F 0w:zNXgY͗:}{r6t'@ +Nwjzk;]Sj:e9٦z&[eZsunpmd>Fӝ4. n̗(H 04]ΖLz?ari_v;p@W u>0&NSBj`RP| :d ({.Pv T9-`O!Wd=|tVNWW; jdKf4|G}tr._bvZXS^ rn\ ,p0gyJSFox"\^6['pZhU0_4xt>!:I-a9329Fo0r]p% +ġ8R7Nv =ۮ`Nmmn7J H/4!DL +vp#P׳)%Bګp)32`<Ν.7@22 ;+ 1W磃!| oHbhD +x:?WiD9p(Q`>UKHCXVd,cVPJh^[.T"0%" ᰠ a{T5 +a|b+Axfrdp`Ef5Wx-BBeJ5`!*1P ʘ!Lb dq,dSȤN/Y-4PJbJ`kS(5"I/U`J/R)1 HH{NHuT6H=DAltBYBH[Xj%\4;b2<@ٺˆ4!*¨pl0ѕBLfN;B 'Qa@`, (uWS>%#}Nɧ j|LىIKc4Vhz  M +e*ʔq<%S3ej0ARmLR`NJd+paΒXV>܏[l X  * +XRCp݊-aVv6\qwRnD%vѸ&#N4I8̉BŮ3EaXf0^y~6fNQX5 q䒚'Pq@F .hT0<P `4 >pH +jMl&O$Dh"T]OPZ]!G"Jʧ-q8@"ym5OYmh8hb5V="yָ4D*ХҢќs=<z? \HXvJS/MFl}ۘ{=EJ} +#Ɣ/_Eá +.EDzGʗr`}s%2 %YneS2:ӎ O'˿nCy$1/FG|| ^pS5oBV&GS$I[AA^8H`;JHA=R2)h,Ē`5 IHyha{xB g_Nq;&(8#K{ $lzdå['YCR,~Y 6^p  슅 lG-ւk 1gD)tDiXUh.ıb5Ej!t)~MAf +DĥJb +IBRd1 +`<wScg +bPjT(\ l"B(*^v! \IA8]#W #J&P(%NB$H,%p<1ENGȄH&xP%DB>h8thH%p^TCN XĭBj +Qp9 C1BQNB?+ D]IA&B D2B4pW"ZbDeDPb,!.:!c!g*҅Tr\:Z2cPC +!npPP4hAP!bw +Mj +qS +kAыQ'!Ktʏv"'FPq.2!+p 1%?G $I(f P4|0X@ +M5=! +Op#ȡ6n%n(۽ FJj +e _4\Ȩ:AB ` iJGp4M0Bp"" i[ _UH"$," P$0+8zF*540 ÄF8" =yGHx8a>$ /g& & 81D|G"`3`aB+XFZ+Ut H!@$tY^K7˽Qnp`tb A'-2K>2"N2=nq)m E3;IO +ES]rH3㱕 )Ѐ2Mx2c#]N֢F@S*<"&RK(Xfiie@Č+mD,&1Rtl.,a!Ka@Qa0(pp`a0a8 0h8&VZ0cRdVybHjcC0QE9!xe>,TFdi #AԂWzIe]@|Ѭ%0 ] ʐ&)h2Oa#IϲWd0  a!!0=0T(c#G 0D40]^E`$_Kke0 %aaFFl8" C1 ÛB®a0 CpRl (sp@xa`<r m/%BZQD +a58p Y"9E&sD%f*dY<˄aHB2Jo`s9f +# ' +mN{>:߯ڜJ@N96Rfs*mN%,y<2Wu{jN;e[/їnvsX}Z˹N pҝN7۝֣O֣N~У;rY/s؉QK^2в!`2_Fq0|9t\M\'p +Wt \6,vd7rN-=>2FZ4|P;1/0&f2LF{d`=;aNݹn ٹs)rnvh촛t tFz~=pf/۽^r +,F_vtNv` [ :Yv*_2%j۝]ћћ׈d7 L~=i:b2Em8ݹs|}r*KLITejX$Pۮv\[rSj.NSRv{.ZW}csy9gsf-s6eDSGbEҎpA.<^4͋* + +6(._8>yf4 lKWbٝ&We`pPXRJ/N6GgNd 'T$-NPheNI`iT8;NC!qe`@:\S|ti1TмjQV0ff!E6BAi>i+p@2ִbeJ"0Prt e`F""IH,jfTe~adF%0hYK:CA[}<@g 9Sh%7TDL U +Q4ij04PEsĨa>͢T $KX[ { yF7)XP |`P /Ѣ%eG DmٔF#x + 'ʬpDXdyH p Щzg9ϓjU궪XuZJ6NAIVzSzzu +jRn0Á*kd'TPM!hj\r!:[`L3ʂ4ΊVJ Oר`,x|%F2}SmxEak3Pو2JƛXlaDe"IG3PnK/zI°ڈP(6""y*B" ~V B1̅K.٥6 Py?,hDqx<8M%th۱ыPdsp*rZ+۲-/X +FypȬ Y01NI2>]d[J20jLL=}8ذZ,&HࣩԥA8 ap،#}u m:HlNR VUS?(N!\"(uJHFũɰ1 +5ڌ[:IAi`JYc)-a +$|K@iQ6YQ2]$_PE4 +2jG=^ TWu,ژ:Y<ƒ]ʀpڨ6ӉRe˜1f1Km.'dZ Jd֓Uih8$dt8X.#&gq|m\d|vy# +ӈ('QaЬvX((3Y$eG&gzQs5Gh,cT;H*3bT̊LeV| <0y`OkhYʬx V]V@&P4NHѐ2FB8JZ3@)x&,1_ z)Т@\ "-$gq!a Ɛ@ 4Hptn 3$dPg +PQ@ȴCTe@*1w4VL&~$nm-eVK +Հ<8P' ܈J@:"~Vw]jc)U/H 1f'JwNt*4Ǔ: +r+mJTBQXdd7ژ!Ad<?s$}(̑b_Q虾svMIVmvL|6=nبѸ4?• 7樹*Qe~Ied*@R*z4bX(([IfU';C!KbN Yt4N„Nxzgnש M$h RA[B{ @lrYZ-J4HWw:Wo66TY3C9  Ƃ 0>Er21,- V! +s`+CX-,FkʍH^/LRz)` kc)Ը@,^f^f e)Ԧ2GȓaEA7"zN枬M#2> 6"FdVI>p..Hʈ6o\ {ir}dd^WDwژ7Tp܏(O<"׃>LژcY<އiL: c%B$|0K6bjgaM2d{r*e, P\iLF†X}#r6 +eyh%TEá8@q>TIzKOBeH%GbVm't:@^Ī%]$qOnP8*᠒ 7ݐ1*uZMuy% IHTő[A]SS),LlFn# IFKBU@*8d%lh 'k`,p/b7jldTAUy|5 Pɫ U(T6xoxg{xnA +j™E.l O{.kr: z):R&IF]z`*`q2Q" c%;h&MAQ*y8O 4%ca +N7+-KI'"Nزenq4T2GLTܮ<^4`DS`̜NC 83T^AfqGB2Kh1T.P)$y[ +*P^F-NOWRPxRL49pJb5:HpmD!2"cqM&2[9E[$yхJ)Ӳ @PZ@B`%rWpTp"%̘"Vql_'7_q\kk9ϖS]Zwk?=sw=狱6m+kۛ/u6o% ڌ\|IP)ח9 ho_mvݯ{7l{5p`zg~bL~giA^m+:c7:Jd}y[99--kOs;I0}s;kbk}ׂ_g1nqXo0yպ v9{׽7v ?tc b5$oU{57von~{ֽޝ1w0Ҫw|o|myXŘ[NO_; |)ϝcR;qSVcm}z0pw1}s}jwպSk/{oyl+=?Ǐ=czqw{ko$hf{;w״m4״c^W\ͿnPTkou1׏jε#Zl0Mvs iD",wPRwrK6[7t\+޻HDvSȹ|0)[soܷ< Zns])ƗV[>us]--nk+;]@`}2\ !˰Dsl4kHB +eMYwˎp1$ce"FdVǑpp9 + +ߕt#Kc4lP!H !*Ƃ{#2l.`{r:i/u@`W׫D;1BU&+/MR&,T\ +qVndxѓVQb?&BR.@Eᰊ\{"NTs;N"? [g-G8ÊBA q=^"ЦãCwT_8`X(PIj۰5r0džVJqX*ȧf$l` g@˂@'19N`x_2P.rC:eh8z<'(l@sy)b9S sO.Mp2L \ !EfsYq\ +'DLf"Z0X\ )V*v22-{!-m,fb1\OOgU4o ˵HNFS;UF"DԱ2$|XtXLZfXϞX6mtLjd \/iՊzpy 6/$CA I;$ZPqy&lAF%2qؒ6 +@ؽ.D!(ǣe3g*Ya3csGp"@6G4& Q0B&*k.H8uQ.b<Ƨ:"Q4z:PY~H rAB#|_V.:6d')R#i\xH55yX~d瑱*p=ggUpټh869&˧VvV~ t ?A)4,c۲o^ >uLƔk]TeVmq'+#LcastDEJI.kOHaFfҞ$ ;lw< c$[ iCh<~W$ c/;x5#@}@)ؘ#AAsY!6RJRցlE0]=,9!!.s*LBKb'G"aR`.a\*Kcw8j-kJl*w׹hJp,`㨨NeW^2cʒAe)k5<^O"@w 8<;X0 <eE(cDHI ȔKy]IF" `1 Iαz VEe힄 haPq\VB:pVB8lD;l&#T"L:XyѰrR +C~y!^/`><-iy|Րm+lBWih#DtFڠ1*eE1![Uϲy=K@2b^fՆ!GL 8,B$V"G%ng4-sZyx'3Y+slKy^d FaÛ K +&JLxvX %J`q5Ƀ#Y XUD QfrTuHev`~D{/Z%@=U6zHsC٠9>9Y6?rbc[9ms,? /o*yŅSr9)+h8Pz:K=,Ve.(8?FrYuU8mQ+)V +5PeՍ ^y<jNE:6ftP@kѾ2:o'd14th8HKK`P2wI+a+Bb0FօJNTB  +[AEv c-nC-MAvxhN,xWVI°6._Y|] +(_Yy,"]f,_q6jnת"J$Ɇc*viW>«PQ$TR>+Uγ2 amReu0P cP4 q!\AM79BUbm"A)]hqjB acA6k+ʋ0*a+/EraJLfTVփ2*/4M6e-lֹڻ{snksmjiU_گƚA|)bsŵ^֙nͿ>5|wZs\s_-W{+q~_~3 }מgwӿĻުy5^s8w[۫\_o8gkנ}\=[[7ֆ՜ַYoz/θw)ﶕZ]1Y[|S5m{t_mնjmmΗkʭozx_\wϳο3wֻy|cA|u֙W+jqۮ7work/֠o|mq׏N|i͚זf3`ks|w&_? _̹ŽIPq\gkmǙ/~w{3u6ݷVLv)TƳ\!D33 $ rt" m>(HjGTMв"j#$Y<=ݰ:5Gf +o3X i4ND0L X\ԛnbB o%O^usqU4_{Ht9U +%~a?$ 5Y@Vw&2V?gR6H2˦)Ѻ4V@G$kq57(Ds 1,+BGZD\䍇z6}Dk좊s4<tЇYl6ggF)^HG7YLd`ֲ"z8kBI%S԰J"qW#.xSft5TuYe*)y."%$6 ̗k&JW'$g#vӭ,PK@<5A פSiP*LRwQO$4kJ̙-FO:9Wa&-x|_=_rY2ΚMf Rz0^'i֨.h}^4]}M 'ȕ0yPdhun*ƹe.o u".ˆϓvTawp7ާ`p >zl.xhoçG´7QigjZ"18]:\ħ.7R7ԓe:+'ГHdę1+\^`J#%i'`vK+f \ؐ7$>Uノig0oS~I܈tܯ}vF9dkl՘YA*`%^Do`@iWԈ-Je#@tXs5a"[l0ZϹd7-Ɂg&cbMwLD''kZdk}iiLlM}B:0׃ଚjx! C=[Z9bo.tPrt}pȢ +\5Pe!/f@F;eQCt1=Cիrv"9?M[ul"gY.?UFqp9BVwqCCdY4UR=qȽ>ɥ^mEI,Ȧ{p鱢*I> s'aK1?=W%=̋lLmbaZ.U'Y?:јi'>i菆ߢ `dTIE +Jūq`€-9#fU/2yu.1Ο[DHy7M#^q&5*-pl*O*J%pX9Ԑ +*{*$ZDmAIlzEL|N} n4emg9/3DBk4ւێ,ꈍ=3瘢kKdu`n-ı~ԊtTyVUG1aY;}_'myjгR)|7@ZTA>XbH$ƚsiL"rXqd}pnX#pif`l&y'W +=P#`M=蠹P +J`(ZB2*$EԆx>X+,7^ーt0׫'%A3][n +'o"9#ɐ;ʕ!sٕ2I!e>JpHaZ$1V_]LƭQ 5.>ÝNQWY[;DL'H>bR!"B7% ydV1 ( ~Mp\qW&j:@YL?Q4юq\J<'X!+ wa ~"<xUkè8qK`[io'>cjbNvqQΕ X, + bh,HM +lemC'Bbh*,"q3JxWfȘs6`;0zȉhHqqrSfqu=y`ؑp+b{HElNI]tvwH6 Wys4PydoC2 /γnOW˨m+ `ھVұ]]N17W7vqKu aHFDKFI>䄪-Y]61sDFkw-t2,yѮIp˱5U{= +z)B2g?t +{%#BO@"a'䌒u̠fp|L 4`n,EGr\d'sVT0%ʂ)m$o~ߨ'ajsF<73ď8jU O4Nⷖbsxobp0ƈbB +ԫTjT8Ӕ3%z Y3` + [pxHP2Ep]XAGHUjOX:~ϔ%4E^q&JX]d58kl]!B'@tȃ} +Ч 'ZѽټN8PXpƀqe1'DV6Tl[S:Nm֍}?j B6Fb 7ǐ!p=᱁\ʩo .ĖjN\KڪRx+EԞ!v;L .RR{ )/K 5$"Ue`5PZXZWnz p_KC| "yV +$dG94R ; +H4Ǽr`䑸;ov\H̠݃^0 +䋺?Ѓn={🱞ϙxφr6D m0aنJ mN!y[U v3ن Na  2 ϡl(2#в ;1"jِ wޫl(E`S8w+SWX|QގGF7' @WHU#%e3)Z=b-p=d XaAfu6UFJwƽ!̊#^b6ZèI?eS^ߵzN0_p?t&M[A |xjz,y*T?m*O⽓T 9MH B`7WE9]gvx6oeԢ8;9CR^^j%Q(J <`ozM{ߢת^RtQhmÒ#k72tݮWrrCD{M8Sgh?H3Kzl(On8蝯`\Nu_!߈;N@}A~/k,`cVυ7lآm2e7L)F`xY dL?fN +y*C>cilw1.SZ=R(]reK¦kXi&SalbmACmAb>ػ?;:'Noیd)Λض7]o߅ +n +bL@Eۯ":G?q#]^X\%1az1;8AI۴p4s!qRjbESeN 0BN!PwUӔi7Fm'8:w^Fi~I*(p0na!#&}N7~ n os!=#l$򭻔EEc(IY + mam7gn{ȏ"4ZZkީ`.? Q>frFu?Ѩ}zxKET;u GmEc~ +z c;.b@0^cS0Iý{H7B@L,t&Z^)uڢv+KMf~D> Dꈃ$ +7ܚGXJm#¬Ysc_cJXGKR*+6[o&W:wuRGcOu}!۠1'KTS*@{ 9Z3yXHIĉ^3P6VLϼr1݉S?FD'K#_dUoDcWЃ&ꈩ][boңaIQ6j:c&/U,^a*-A)pJs!V͏%RnC }Kqǃ4Zd;jI/>nlMpe;Eǃt5!yz) 3W]^^$Eg@P} ?@^ \OA"vܣ,VMӫ4z!#@%L^ >qȩ[_G/dW_,[$G~FͩVdvӛ_εj/rLiĵ<;%D·AGb"[~4oTo&K&0=OA: +@l x.g ;K +Fe=Kj'#.,)]D5Ƭkvx*K*"/ph{!#H%zh,J0o gm[)ki;HcgNt]ݱ~~Ty($H[~L|[QQM=b[$mZbn><}RlvF%0w\d8תZwr.c| Z<2ZP&m"T(G/#z/?ե%ƈ"8P[e +X_9J.-Li8TZdxW vt]ϝyeB ՘lc\l-^C&dNǩĤ2Y|Ԙ6YoPI,7cRT]rXHm-R`1$DL*w/ ϧyg>*2/`ugijI﫨@e߉ĴY!YpכL# + Kl^1GAlubbؒMDC( Qݎ)w}`y@Q6א;Y- wLkcN-xόfZzvf&Fy4R *#>u)X @wi2fRp] dԌjJӟRsV0x.? +z"[w4?V{LFMN?,ZR7;H@IOj}{$lSembqVtbJ5 puHurz#rYaRM$މZe %GŐ5OI!ɁjlFzet= F54?]'UT~3um;N^ [cCCDW>&TIpIɭM `N@=5`}"wJۃ!.Bsƽuuά#^Y^Q=CW836u*0DQ:eYz)WU:ģ 03!~gi,}Fv-6bYi ĨLGw $bmKx}q)D*=JnbMB}q!6B!(_rp{X—oH[PST1V.И| +*MX\\ypTk><0 \ *ofvhLz-ВPb{: 1! xڡU}LQ/e0b\P *N0#~zk3%¬pUoNlqِ"y1*yd5cF4Mxde!ᳩBxAX0{u><[m}Uz=V[R6UX}P :_:U؉ҪXV?(|-y~XT@Dr}{h'H/ Zo*BDhXJ.H&%߇-Y)ỿn~4t 4'="fp@<=~"v5 ^@aq)Χ1ai/7mfF5:gɺnvCbhj0|_>y4&43X)J7}jI;3p>^2g0Q܂YLEbgp?mH %"A&8A^+'^H0m)1 g;Wxp ]j V3ovh +-2bU+ 0Z QꝂd)e X(Q`#ָeF⫝·>):Pc|*ջ=9gik*W%_fF@Zk32*k(ŝ)fًP'3=dm` ojo~F4r0," *7d"xMuN T}܄Wg"Uc"و' 4Qk3PLn˴ f7őV/#5"kWdh•Nr&r6%TtV%5fQڽ?T9U0Bے`hjcn\FsBܥ{P#J<\d"C&+ +68(bqT*p-<\QtDB<^_A %( +g X3V_ + Ex\/i0QS-lG@raƨJu^K9F\34z͊4oCA"̇e{?6OQ_]A<`Ep'D!:VEVwϹ^Yg bB\> dnˢ\Д[+WY|>S@];ސL r (IJ8]T wE!ru˺GӲ,*+q:Ǭg-c\x"CkSΥu 9S 1u2b1v "#q>i{ђ~;᥶Om _(ߎJ%)s=rDW6$A'"H_$:e" u 1dϮmݨX{F>[TZྔLo/XA,}'hglfef{eXYy 52gjs|=۟fE$/L^gȓ=Rғ|C0x!6O|..pV4#o~^(zQd.(;}!7!TF\Ss$X0yGhPq f/&>SFtiSէ5J|•)w"l`_P.J-%Ȥ-ODR]׿Xsx +7!{q2dŮ#QL/`K櫛b!ruk&[TEed]^&&+77P{nIV/%}ͱ?_S!}(УV~M\҃E2D0BL0I"(g4y8Td[`9Yc ɱ$13aH'3T|+%yΣU &N|Zs-HH&/D.zحޙKnwHfeUyG0O[P`unjy3I`T7y('2N%fgsf)Ba2can)Y w WQW=p?乗9ȹx ŧ38ZFiDV D k啌Ʋ-zv ^b c"*|'c*ہXPu>"BDySm>X䖦5 yj5+< LAWfs } D2)^hXRnyWDt ^Q12픺q50R +[Eצ sx +{ɨ'p 9|}F8 M(׷P\t6.}}U>ۯX6 7_ 4I)_@\0/Y"lɥ+V|iTH#8VIj>2=S,I!ȭZ`e}#HCpIAϱSԂiAFpٱ3VXY2>Ţm^50&5Z;U/j XN_X"˛#W3g:?ÂiWq.jX` l]y\jB2X@_Zx9SAN< ~}TE d]P@;+D' /Xr0b=1kțH!d_[* 5,k E}Z~b2~7ud7j^HC^pF8ڨN3&R%7LF}p&KCP€'cDN6#}&?̬7lEI3J ^#Dj[:cS .dHW zokYk`G&ו ɜ_:5;HeasV2:X2)#!/ .I<1zzUY*٣Px8Oa'gC4aXE T$wOY(,xs}MjZι~A?/ka|'/h=:3gӞȤ  5Ohp!XPS@vNˡ37ȓȁZ#, !Ayш)0˫ L<N [P|,_mHѺ`ְ?5Wk:#<1.IN- +N!a4eQj6C:ߙYX:L)87ju +*Q2Uh;% +7n]hP)*.>q~^'w9I0I]H38= ;Q7%3h^5iMXRe 5r@9fv%9E$UĜQ4Bȍv8hۏ*p^T2QfHsuh`&- +_a%~H"FP#ܾ\ (% f~9!^Fnp3[vDh-/8Dc|ZUһSm{ "LY{qQFo\ +i`7B%44a@2E Og]m[ y ƫ`3QF@D;df/_ݣβFЅ TEWe$+WQ:讼LqoA"ӆr׷>M*&d@~-#?#oh䤍[ g-tqV#=v C̯ AZFh2)$}48R4d2+X8mT"-uL^Zk+Fжp7'ɥ?'0݁Oi+Ѫd/sZDqXW~FˋBnz +F{&aK`VT0& E+N\@=,st` TAzN`ODWyPcn@v4Gd$A  I 7wA:GJP1ꂂn"M\\i' CE@uV_=HD/Td3±i!ͥ]ێ0!-xLKm +eapSNhT ajYxm0Ӵdk@~CfP1<ŀpFr_q7B*pzQ&[N;`ƫCPʻa8" +aꑫNX{+X>3ԡ6~j䫫Vi)[ +wr" 609[Zi_Ax}fBAS7d-(?uq)'I܀ZUdq.~Ǧ }5a:lF84#2ъzfg|QΏhk Q(V!,0rP'bAM~3@F;.E`#[aK@q+@  8tIڏڀRt沃=pK&*WITM90@`@PFV,@壡p^wT\&{h/ M!|:QOn;2y_m|q2-,;C 3;Ƴ*˹3C#O,U:4ix\TxvX+?I`"*Χ3LB@yV{E?Lp/gsEA5a͘޳ccr3ZT=Kt?[7j.sWy rYq`!%U jל7=B9mrY#IJ!ɀ1RP +/}E98})Չ$$6Cquf2"l<iVv)؛YF"lf[v\@߅`*♪ `VApAM&g5!/?CÏILglxLm*&2S3EXq ]O +W!H.&|VQat[/Y +F N{iĀbM))qieOm:q֬i;ĽvRɴy1Vmc }"g i ^eaY:%xD<%1/9K*`0v+8^[B ˧Kw6#i"zpNN13x>hhkU;Ao' Buued+Ars5(cWㅿSY bK(eް9{<koxYM2.#Š؀\*d Ζsn0; z^,'EtNՁc/1h^ k Q/~L٘ݓY_ uTc͍_)W17ļ{RCoմ6u'y"h JQ,+> P\4Mۜ\4-*J`l;CK63.Mޱ!)m*3f48tdI9U%< 2uDҊ1q /g>s +9T%d?pkp)Xد +T/ M%TF})$|~lƻp;'q<$2A ۾HV +>m^('_.\ +NO{$uVRCDr6=+MՂƴ1-ڊ/?{Rz2Q}x +,w+V8B'E>GP&0u 5Jut_r a٢oB$5We0Za(0^Jq*)Nb|.)j !Z0# +Һv{9"r`4#m'H֢RyZKII$m;DqjXa6s:vi[QR[ܻMaJ&P kjMMd&,yC^`ȵ4 Y!sU^J %&_W<&∀#JH?aqxNjBlnND.2qOڢN|;\Qk04 #-wuieN׳Bܘay%X HinKmC#TGL +)8i-e8ʉWd}d`V)1xG )3R]wLP9EhLU$^6qBO!tIe >DtƤ4[Uhu\HfvnBU'c 2$Pj'}j`Sxv*Q9YP%u R* #l6%wXb7hab{s vszM' +F|eB:=xC.##&m_W:v2@` +`ٌo +^ ߗ`\3N!7p" o{MhA,$?a3%+h͓(+Bۻ?+]DKZ"RGJZ1k5D6d.%üMغ<7b7Owt `ݠeJl·[icEUɇ^.o78g,ƁŜF/>-XpLY,L ~)c:c÷|\W_t||`(K 8[#tΙF \륻jaZ[&)L=K190  \ y椔2`W/$ #ƈq ~BlzOiX]B>Q^\MXtiP=E'wyM/^*qdњ0% 'BRMIBGK;a3Zْ% F*ippbP4"afdᠠ5ԩ?F³p*MЂWɃlaU\BwV&8rx ?ńHnp1:XR,(tәε^wǡ*jxء4yc:R~L9ۖf<*Jv'f ӹIf*2%?$Pt?bPAB)j"b#O-"XƧf1֟D[*B$*lzC(^P#]V`%*$1(Fyw_yc(+1 LXVIf^(Rm,=1EXUgAYPE*jְp]xg[0"ꜚf6t"yT:qBpzeXMgtE3HrkN\$Ҩ9p#PDLpN>Kd+SQ̩2BgtMY\q"Z]U$8đ&USEd$PP6CaEMX @lJ}!%s y4 ifJ0S^08|D!xh3@P-ѴhUXORTJ3wZ1-CLN ti~83788a{z?]&NBE` ^ qHW4;y t<XBrSAౘj'L9!dBӭJ#AB&m*D@1XptFl2Xl!qN&foTL!.c +$M()BaG9,= ﮍsDL +{Ҽkj`͍p6S>-11^ M_ M64J(9<^kb"J& L$nDЄQJe}W %1t+2v-CAh@ +h:j:*"4tpn =3fu,SɄS S!)||Q +14t֙*#ܗӨ,kHVO) e+/%%VMj!\zXė``p02QIFy ʄgl("w*(4 wN7\\4a&#Ж:6#SUP\E +$qb +&?")X%V!䟡Bc*JI2/ +$UШ_bPJ%5 )UT>ZP* R< m9BfմQ- Nj"BIdśQDI *)u:JUb˄aރs!dhLCR2UFTOX +Qį3;e\.*3{P'+d ROW1{ OT#Yc843XGfYN2d A`(ҼtQfB('CtPBV#;2MY5ܙ(Z$'^]u8%ez26(K̈́x?d$ú9L[ 1-DJhHq'jLU#z'> .y!*9t`%Cͦf0C,EުQ,+&:/$e[TzFTag”Oh[DQ*\桑XPx9/L*rRK{HTt0%Eȉ/…89.XHɗ]]GmHJr*!kFXsp52R}{ #)T4èD|>5Dop-:L0ȓV"| 8(ŷ\!wL wƪEЌH5~F5CYJD8]M-58jAv +e|Ua%!$/QVib0)5 <|DP@8af3 (.j8"v4nDi q! +@8`K7\'Q3Rg2`u~!vJ݄gS$ 31cpCFג4p6%J+ e;87e5 +"f9L@DRE]u09?CB fEn>T":R1}:1E$O`нF {oխD`yyfop 4\cuO@`0 |6b(AS[$g͑0t7R3X#=!h C BAiQf %DWVr")9怤3%#4Bc vIz}Ո9xEA`6EAf0XN5)1agSX781H;3BFF\Mj(ȇfBlԇB +#7x|8"Yǜ cP$BY2pCR8ن}2Cr* t2[Y81^D&")SuU\M~"R{MSEeXŴܦٺ6;!YVS\EMW<u48ܪBc!һa0&DgaaĒMTyFaPVYujg8N*ю.8C +\|uLt\U%E=[37J 23h4 ;Rb"KWȰfYN'5N*8iq3Ԑ9=E!jqĢLdU|0 'AKN*rMpQh_s%;…'2ƽPx(\dT|4zWh,[_'f,޴&fʥx/=E,-E \LSa!6_7-c +p4"$b&vrUC'C̨c42ڊĵ鳐d;`Mg$0# 1LYV$ FXnP@: uZmB: \@x= LiQ%A#o⇵4,KK35\"d!U7i>hFaRAQ7"P bP9KRB0\ S#?QD2y +*#:6hdMѳ҅Qg76}P%2B7 it0@np@.1O +BWn=EKHU:{4Xq&J)t{#2LMGdAFBF,G$mF(,̳P))0te}FԈ4%ǘR%VuDwXGAP`[7V/-4]b^ HH(Hy *96-*9&!¥͂IH@=2"bRA?"f1a6eO }"(iT.TK,Qh`" rKXZd:п *~4S*q%@ "X0>(U"alj7cl።ѤY5w[!'u3(6J5tT3)E4>SAk:}S?J)/L +`Pw +UQ +- eB; TӑtZjp@qiˁ,JL *_D^qZuyuSÚpXR ޝ Btmp fD  X}.,pHT\djk1 ڔ_ `#(?06! "p>\PΠ+xd "HZ@Z`)"~ +k6[8@_x ǂb& ' +wNR (+u@5]5ud>I1iAJ!b9T " NՄ8 ЉFάaY3@W;9opP? .QTBҐGNB/t"/TP'Z($Tp%2[iP4d2M$ip d04nvԫllא^ }65)(&.=T[AU9-. +4CPUqOC BB[j.0񤨢~R.{(z'E*4jWUBcLEsA-RHN DrɁfPB4eLGNNrr&%Dl J"ͪ$@iH@  |Bm8#1 +A3EK59A $h9?uŦ&np)͇1J*:zp(LꄔX[ +>^I +;TXx$TQ EZ f8Engҩl-De@_1BIɊ 52N +%N3쎈L,y"l @pH2l,XELU +141dh%a"bخpR4ZevK[x( F5EIF4ԂF(@"B#%wDe>M&Э7vr=zb>˓b!~:%1D=ń)z +} E"46uXv78xs@:!%JPS Cc.oo7:cj!=reQ$=ؽB5NT2TUH v?UZP{Q'Zc# xJEr6[f$ꗹD+ºvpaS5kL +G2yt榋Tpc#W +U+!i9 +~3fp Z`O9G_9?3 "K;{@Qh$*`Ay!R¸&k6tLp4bIἡ"L^{!v,/)YYJ5@sF1E=ÉoNe(Ȳ$,.\=`=1L63oMSx&Cv@!%ttM%\*US<{=;#Qk{]A&bQMA䕟`G819D; EuѨ(]ԩ /N;j8B5䱥JdIdih8>s=" +Q(@t#*W)9C-)Wn*6L%F8!J4U"#t:1spɌ !3o`%o#l858KVS`ȑ:0aPf,Z| $$\큋T0 -!48Ζ!}^$\ CdуY'9ņGl( ѪGQ +6P4zЙ$HLtd l ~zp%DbAb 01v,ZJ%`# +UTHCyrAΆK b%C +2CA聘#AFF_% + +a$*+Q>o T5; C߆kѢA4أsP)ZEكYhF ظ֞@]᝘ "wélV#z0S3'YA' };8qYv6(LĊ!ŎZ uqE$o`^[1FRՍf*DIJ|=("yB +bT9梛bƐV.0Z((IjdJhh D+3kA3At!+a=; +ꞥi#68pyT⥡,$ĭỷ +#(`ͤ`8CXL1d\K7_^4aip@H$(;'LPIbp kdSJwBFw lb@@ض#g + +O (3xl Rlp R?m*"y@\x`3DVHDqڪyYӸ4S0G$FX$LPE贝q̪isQ=\'ΞB?u(ch=(P8Ij_*0a\A Ԣx?+"L 8&_&֒8;cKڃaDQa|zʈI%:d,^^v0Q*ً#AL #A*\%A HF.-w*36x858h*Cbp&޲0߄K4u487#'PQKW5`,*! 2M[]w+vpY%Y!8p*!(xPiP퀤Au ̆oMpB Z:(;V;>0߁L:Wcz3ˈ(zPS R8M[ӫ>vP"+f^q$*ݧ yp>h$@8pqdt#F R4s +YJ(ѩ8։>l!./eqgBCح +ZO2 @-/6ڢªJJ! 2(m\؃`} ̬@3AA5P- X iAW'%4}C+k&>( _(NAm+G͈LUIIJ%T sGQ9Kؘ"Up)A]UwD}*<90)DLn. 1=p 0 I_Q^_JJ M%DNvНZ1BцG9H7 \>r 6lP)J԰A‖:Ȇ73~ +%U!KzUh%C2!z`!;s`?Dx&fh +֪C-^ݔ,A6V?$j4.p_k8~JT +/ X*1CT Ӡ4,P^PpSbc1a-PVVQPDŽV2"ӆIb[(Xe2_P-τebZV\dd!݊6Gȇ0Zg1{i5NְT!Bz&((4kINLdUuWdbu ((qMvy abxRjpA2U؆G5o EaMc&"`;leVpj8%LHH84T 9FQSkwLe?Iڰ8#x40BHjB0j*Q p)h$BU_6b/qr'a4iHDh$bU#=DV$DZJrwܹPUNb>[O::UPJ?a҃ۼHUPBNJg%rLuMFM` ǡ~Ț + ᗼT=^p&Lipr5ipSEuࠗ "B}64TdqUmOF}A ͌ȼ:xƭ ihԭa78֞4H!64>RaU4ް#)h MȩOχj>ݯ48DgG&Rg0!>(Nt3%\*" Lʁ!! w_<aQE/zuDT$"0&A7Vp<ܥs-d6hkdHJ,䅃%u,:D0[g}4%̂'v$307p'U1hvl[{h-4jã=Ȩd )i#B?ZNAaia_Dt!dU3=뭠=5KWvB=::EBG ]LySSBs=$"ؔ`H|5[KRECƦzA|⪈ :TŮ NONъg o-=0lKȆ%CaN=-;ڠO Kμ$|m$`u M:ɞ̦ Qаt‹9禉Zn'!yVqqm&7'&ZRZ5LPnm$0NlGW1K uY:Yi+mXidj~Pjލ n*^_6-̲DJEXHi6ڃ,AH avbuA7*6.&bz=HB&xExyq S]-b4ABatobRW +z̆㠙 eI*c5u%x\E+DŽ^Ț1(K.ƐiIL rfBNFڵZIY) e6I6o8S4ף{xjsODX1:2,bXA MI*y6A +٘ZHk/ܭ/litl| +ԃ&D¥H|± '[CEȨY)4!V*ŠvD,l\U Ә?t|W߄U +T"XNe1\vxSɦx6I1 i*<5 %8*KR T*PCN#,%k WHPVy-j +Cw7PGH*DHjBc˕8%md O1*$A C cF$I;F2FȤKɺD&~ѵ*|T98rG (:Wb\J:Jȃh\x,Jt;_fjTYY2ibu#E a25# ^t&r,DBr07I'AIWm~k?-#QGSGQbk_2gA^d* SPa ֊2H2e8!Vʝl,ɠ}0w1$O5`ӏN&.*4sXƒ}"|Eȴϩ׮q<)͗ZnB2RVHohNvJB I dό` #-eZ Q*Rogpx~ +L YC3ĺcj/.'mPd4IGt1MQA/}x)dߺIٸ eH@he݉XFyc/#Y o-كDᆚ EY(>N'!I(kn\zgݥMr!DUڰ=шΉyodXðkz=;+E=%PJt6 +i˰x[1+,iJ`J 'pkN61F,xd[Ff&!vTɍK&y"Sb +`Aܫ,WNl*#3L |Q݈r%CFq@-7i{wAg*y'.g8deԸENA!"1*7D R+jQPH'(K>3jU'oguǕ`3B}Qˀ:3,Ԥrh<9.̅, q؊n)VvbqC/lGvR d( +.6BA!y"[vGHd9LS5t1M>`j.zҤ0c+2B 6D+'G.IPs3OeDIVTUNoeUbT_eazc %I +xf<%2Q@Uz9ݽ h#$ߴ@ɘ²gG +(6`*z8SaqSK~9 $p51J##(ua[(H$bU:m:DEIAoYcDd<ό55)1G[%dBnn|&~*Ekll7rM{-GaY͐:K?P̣+{2Bh,KnKlĀьy%kD{ +P

K qQ`NO3<®i gѵBe$eƏb']R_Q(Hef~H:461$.D2X^<L!(rc<:TO@c M=BiПRZi@ ol_@UtNSGC.@`cEsy2as 8LkX %6uٿ=h=uUɧ;ZP,{pz_cutePi3pfwS'5y+ubvтSn. )K +P`xM0uF4ErR_{cqG;3bR<8߾@/R{So| WN8jZ΋cl]^dTvx1ET/Y!?&5KHBUFt.f l5iY}l*ԑH7zP:s#ONMXb\MFC\ +k~iN9뇺o?8, A`rk: WG#oP:~25S83Hp$Q:KjN]u!fHMf~rEɍH +o(a7"B2TAӵ,JZգq%@8R,)ԉĊRd(C@`j꺢oGٝlP?$8yu*Jpɶf "5lF1˄r bg8 +x-qhB!̏T:P/ctP::m1cY:v6j sGH٭cc+ 7~UTS=~<4ƥCPЁՊHKk(@<^)`e+ 8uy5MޫSAx\G7g6RK:>U15D%|H +2"WUsI|SxdQGJQz>,FؙBWUCv?l+y<ʸqi?۴$x#e>\'A[.J "fhhVF~ iuNbnͰWkQ|&MYk"MѢɟjK%+mɽFdef Xn(J+)bB&Z렶+fd1g1r)Șǘ j;%j#9 +t>,5B"V+;&\F[J0Qdɹޏ̔/* '׍;|$2(4[ 3oAs#7U)u.JSh{(?e8 r?d3kV'29᪈ivcrNڅd4Dqn[+_{Zb/3J+"@ ǭ%7 +!;^*Z:x.KtZ]l} KzUs.!1Legs+A=ӄ|NvC$(TiG9VpDS01*=v!"pE)ϻX"IFp`+0+8X#W=a;Ճ"đį[w'Ce,$uo6%t sIexW3G9'L8*Ehp}c#m _nYو+-xނaHI(bgG\oɨ<0cSsBX6TJ4lq"^w[S#37}#EH69rkxuʞJnE)bW޼ן̧mw}D$^j5SY8͎eI^}PM"gGd#6WE Ɖ>!X.MdWt^Mx*KdC +4 Ѐ4$Wf]ipx>,R$dI&0?8!U> cك SmdJς([ƛ>ݼiUOI <ܘ.i*n<6aI ]M44?nY+!?}7L x C|1!7iڟ{ԐNAD,Z`QD*޸eE̫onII ̉lbUБU#f!;xQQ's_3ỬuV@II*v__XP(È䋡J9˥[0P! LSV`2U$MpZUH>LXO!X_ɐ8Ų B/˙U/o=b)Aؑ0*Sh}8a=)N|e@ +dppXRmA=RnRĻRw=CNx`MkHm`Ockg/]1(40U}y]#xfpuZj񄆩L!.o7%-sSCmY 4@_ 6h$<`CYaWF G8Ou6'x.:} #ގuqcVe}ݒmjuY=iD_KR [ܩsLwiG y:e`a1)^jgmiA<WP*ܗ/$EpwPu5 ѝ47x }:Ѱz.Y|jsz8Nq l +Ba9-Rk/zA!iW ԜPFq RSJ=X圩gu93G-4FBAI-g,[ ReY яN܄A4K8e3JeLbo&:1<'0V,=Yx]O'l,D2CgLiGHR?Jmr Em/~%d܊TNr5O9p+-hR| 7aDs"0ˢ_?Y$1,IP>Vq2L&Gio`|YfĦHmP4 +DSUB2-ZqzINIl6[$^]F +(_,OPw<0PuhSi&,(2'eC!*X@i&OpGlưۢ7<2%?IU1N?]JGvt#yL7Ψ(muv1 rF$r _ ?xWFaA4t w%dOcy: ?zJN[4:Mj57 8"moVGHJ28[~ۘ0掩MS}cكnFݜ:H 1ǰ#Sf8ߏ dV# j:#Pb8 es b;&5xda1E$ B,qAULdTҠB+ɕ2IO".I05#JPA@4E9fNWp{Z]tݐ>pOe_&ꁙ#6DГ<7ݽSf$0Jcu2G?EmhnmE" G!ӐSwOs5j͑ec| dC2{O@^RqZ$u _ }^R1tfeXw ㉟vY Фj +[Okg/Yؤ{l؆!كD.eJe{Lyjߌ[_uHM.ĈF/Esy%_Fw?'فqJrͺ x(6K]k) +$81|F3 5  -ؽ|@aF0k̛x/!۩'ut൪/Sk#!c\>Ilǖ_)Huj^y_Z1dB_&׬b ǂCz(Pq_h;g̴ 28/YqDadc\\{x))" ++rMr`'ʠ(7v rULH2KqπjipDzo qPp0,asScȯ~c9o,ǪT)ևBRPKN I`@|~#rhX^ j֐Հ]hA숈dH.$A z3<71AgՈIIll|09J)htsr>B[ٝ+yQkZfUmX?=j 1oRKz* *,,GV< +ƽ*#bBYqU 9G ho*@J2壖6c.6zBPGx/H(WCCf knG_4GNjNMmN=U)FT}O +#|`e@:&Խ]tJ@ ] ={050PV JKx!uV:/C364{[\CQ zH$v+ V;KPz ^,]X"4 ^db/6f_Yoz $b +endstream endobj 23 0 obj <>stream +`jjJ+>F`D q\ԹEŁʩۻ`D h6)n,J%GEIFDuӌ'+[4Tia 9Ed>5I|y@:xGʒ<|N=C,o4J,_\'ވ1 oA`EEe Qf1!Q:_ dvZyW~ It+[Jj{v>zitmq+MSH >D3 I#-W -brzˆY1H/VmޣרH8a2iz1au +2$:51 yjs"ч+FDj)?-| iUH ʨZ$(S(']l8O <;؛)VFl2iEn/?c@1vm:*Ԧj+تo$I kc'p]O%@pB֛1BNv[ tgh(K~qcC֕˽g4 >p@X>~2]GhnizGW++|\ҡI(kW"8`iCX6ę<ٞ$Y+J{ +vE + E G"|%Wo"%ҢV"@:=@LWH&7`DvyGt ?hs@z s oAxWӃvt+q0#S! ]Ekp&#Vj52Dg[Ѓr9W(f'%ꤚ"1-8LqG80.(bZquB]R]ߦ1~Թz_wj7{ċb$grBNfkj"sAVj#O>8l=NnʙQF=KWʝxBKL-_Тf.xvL]*ϑI.ZP^1Tg^w1lNZ1QXQ*{ +yeJ +{PlQ%)d~W1a@Iʯ<1kw/=D9/\sBLZ|j1~TkPrf|JRaOl@rxueį".s*4B],6VnڔfߦP5 C(󍤮BMj[֢O-4JR=擦lWxcqc֣Xǽ,ʿOUk?%7 )mrhZYx܉:7qEX)^B?mǠm +sm3G`Q<T~[I*n'bgadc"~="z"z1"}ףׂE ɂT:20Qd6y^W@Rt`u*aT4#`4h^(.%_d2"A@Ӎoꉀ%6v*Et=zyR%2!r +ho;^IPڽbKn{-"^SH$ +;/_O^v/)]xy/Mf|KjJNk82z +TVѫ愼/(@StK ?6SՖ+&Fk\wive\fN-Snp;go op!xDKb9]Z +8w?4TJ[5E |-m~dQSB~ +ӶfAq*C9) 4GTͤ'|D 7+/Bn >4u!SAXCˏ~N77cHr~1fՒr*=,&4EU- i=Y@&q1YXcΎcW$>zRc Ɥ@({0'Fws]|hҺ\O3J4> p{y ̉T.#hKi{b 'Xl ȑw$WO),ՀHF"n^D?A͑| NpF!LvR +71:|=-o)qHljBe#j>Xs|mT-t:a!{(l^]-*ϸu wTʺ^ipW ^=llꬄ 1F?*JS%GJ"LKZuԓZӋf lV8byn]{ItH*2޵ ] P6ҨDŋ.\K^y |B%UzQ<3&Y!&e޽,#Տ@F-RXH7S[q3@lah?؅q,Ewwi&TӏަԚPӟ;RFAM&/J3T qe8rgٓ}Ur.g` "2T-=ZƐo \G~o%`U< F-ޙT'u[2̀W$!gWڰ'IN)?8';ϭ$|r↖hWfܹ]2!X?$%pq[+Ig'I@mT$#UzF>\fZ|wya"x;Pp-mHh^in9l%g + nP&NGMGFI9yC#d*\@Vbl{=fpC:0ܚg I1w9(CkԴ&Y:T +]bsp /]$_`2J%3&gz3m` ^ŌHNcLg`s %͸] j>=]JHfM$~v&x[pfêS&WKOuL$x垝ft͜-.=wM]֑oH&$7sР]m+imN7i@f:-lo[m1 3|3H2&Rc*D>:=9Q܀Qf3ϣ k9)p>YTrٓӵ2 DʚT襈wo*Q'铇AT^>"AZ84_ Htt:j +d}ɺ+%gAO,uP̴QwǪb Uɐr[LJGxpu~p%M +"u@/(hc`lKkGÂah +"2"jwO n_6>YvAΪ6K{H^DMk%dzwYVe+,#ḤUч*=0*SpKMDS2({lwpM8Qi=t7G1d1cUۯ zHV:d`;/2a 062]A}RW!#XUrcg\%};K% Kgy;Il[h" {_Älw嵔P(1` ]=,$o j_mo`>&:;y8dsd EtD٠-/>s('~=#!DQ6UW6*A'0Đ+خLhhDdA34)abvs/ : >t%c8Y`8m|Cdq%oFCc6D_̞D5T~yi O\o웎oKbm͂^ ~7"swRQ*ł 4$B,moGDJ-0."!Q}T)Gz4%9\!OGG_C)PW8j5IWhIk +=^^',%H,LDQlעp3+EH!f-P܊&؅6_͊eOxчm3aJJĒa,1DZS-N~iPq[ };SsC99XNP

%u)_ʻ8hQCĞr5gĹ @ ftׁ܁4O5%Mf$<^Zc߅mf,&{ZYxkXjSY-$ϳK4 !,XmɃ*PrFSXWK}^%>V" K;͊F{(@=jxxmBИԟqIt*?[;6(x2›v7UyA94XY U5r]›ܱOsԴ}7@VT*N$nAEff~3њ9S\q˝cY#TC}Eb @=%zߎ( +T#YQkHtfZ@T1OWr^{yvrzqW; +:sdVv:Ж%G8fM?m<}$ 0Y$'C9jဠhڂƿ+b) n&4ik=&IzU ċL\gc($!\ B¨[;Ici>j_<渪*@!Ҝ|ny;¾ʽy*L3@{~ճ]\ړR9N)SlJ*3r+#{EXR/.: enHUvN-6+ +gAꓓcN]ag[٢Oj ߎW;ؕ1ᔶ R0JK"\ﵚϒN@ ?߲ h'FUg؋5rvbz'6"C9өpGceGucTAO1-*ց[ kםABA[An;FY1{qmWJk R%@|%o QV?n±*16x o{6Wtl +z!6ؾJshqǽ_}͙#%b۰{M pwђll툭0JgD뼔kDȏ 5S' 7 +R!oh  Iza@kk4'Nru1ڎy񢚏46IIF-yeM/<\Ivh#dV04{VYJb"'h-&/(ڙ+_"-V'>*@/c9HՀ „;AOZQniZWGᲐ5@-O̝,ه`U]N`cҗG7 : WjҚ=+0K~W Z vf"iX3CL/O"V+FƎjqL!y]2brf/*C /j3ͤ"Lє#Q_"ip?#PUBI$fϒѬ(MU O{V"35}#ʓ3Yi +)Eםt"/t}őV59g!fNv󆮌&bfk]AJKA?q̊ nW*D=6@\ '[*;NOKs8PT PP I 6u+Z!afRw!TbB/jWoW ȂKWZ+ᴞ6%o;2!}v,|~J)`Wu٥(FncV`'-.AQc~n/{4*Em [ R^Na LNLJ@>TUPTD;@ Vvs2a\ɐj AQ bm5Ք?a[L=1@c,)Ѵc)Q+ff/ ޒҬ?azqU0hP۵W?KSl6L$)7UoݯvoۄW;肽aV<) ;(KL0{~(E0D4y}"ׄCMLb(zM",ɜ DS) 𒵚ӗ%WHAxDx5=ZNΉD&f?*PtR'C ,35#~F"za +e"rq08w=^NtmȩP$A0$)FN/EGO4g>dWCOMޥN*eRAČn z'҆maБŧg֨+`ۦ:y4d⾂=J6J%qJ_؆HAx#?Z.KJA D`@P W.V]|s{6(Z.6A-7̛9irp0%P37Ija|Th-FKRd/97rL6@jrM(7)AA*w@tJ'T=]]TY0dt+h0zG;W,];}qmZ 7́} rF)8'Oh)dTB"}˵DrCtL,@n#H,)k:ihh)o}t7"2#R"XȓP `a,%xɀ'KD[# &>1roƵQL/{VEtE8ΔS-d4*yNl*TBc`5MmSKՙ;RO }}P O,nR+B|TpWۏS^22ҋԴdUBVn10_Qf,bn4!?=nMgDŽjR+3]/Y1ewLФ_ DD،dI1kTr4%xᆁ,v>F|ʢuuղ=( Yؘ3+s.h}10 v8tN2l9LVR<^iXqmbOPS7Q/䆶Kbd}H-mmR-(5"ZtESjn& 3Ȋw Eg0c|Xǡ Zk#c"yq!@ӽv'% !,9j5A0^&3W^j!r ̶~EN{\ j'L7+SgVxtC9);Z#gE|R 2pc5‚/:|o(h'9P$ ŨhrJ !}ȕ, y=Qa2sw]?D]Ȁ$'EEϹDpIlXPYFs]ydzSu~rӧr|0jnH I*vMit$b#׿%w-,Pk#X{,ra:g~fU{pC7buٓYՇeJ~*)tZ&jvS\͠b8Io5!A*Co4!$cF㭄7~f jIٖ>~qEjZUe$zPm efv(ZV5Z' fD(^ʽ4 '5]Z077{̑"&NE7M,2Fܫpu}>pSoV.f~#HF CG p֢ߎ +gJ]fU \+ozY`߳tc +\ғgfv_A'MGL(ɬEdB ++<01( ('XgRJS?:؜(#` €hmuocE-jyce5}1Iu]|zLV+uKH#w2呵WuZ|{W["Eh9-BK[`P.C( 6SXlC1,Vn@q +W6w*+7,Ap+oX#(2r +唐8T;Q)36Tt{*LE3,/܂򴮐FS,y/t"R!|g; ^õLvF^(|gkGbarF_J?]~:&v_r8?Cf/dA*h843n!ZDs8S{"*0_)E إ^y t8͇,!H{V`6OV)V+dӁ qL\M q܁4A@ CT_2L˫ \bITs3Y|~/R6;ʿULNR5Ha\iqG8# C":j|l4Ƈd4ǃvىζGIcLmec ltk@ZAT:I6ǿh-dQҪVܬrI\M" x&K {@7L~: S~Kшf7K'ǹ3MO!aXxtHE:K#*DxNyĵ!Y1G? rR? SAkXjOjbf猟 +rZI+!,ȓ# pd_MЙ vXE=!\gC:}!HkE.gУz d5VNzr1KزEӆ3KΥwCJ'O at<fhGhEU +%q!(bX3OrE1*޲3UB9R=s Y$6ZVNAlSC\R+m#UbM>V{0 _gD"ꩳGIPSJ=1/a2q9G{ЙfXa{/A3׎\|)W=m +,I6s8k͡w;xZX+~:$B[AYJժVϟuLmBVOpyX|S 'rq(m[9gWJʾ$6 #`ӏō`9B:8*iAEC[ViAim[FbP>s:= g[y1CpZd7eiGFe-EL,`/ӥ8Q+Q]hZN:Y:m +V2"ąԚe.[CRMf Lb4FDC?$ ,2]r01Cy,KVI^- Or]{v4^G12 +r0ԱKX!8bk], x_܉Iw*bA}!m'Ev]OlARܧ6QׁO|M.,]lEu,;<[b=͗}@ g6idX@9]SCfg\[6 /IYxȞw3{NeO;ZǸ+3y\MU/*,=3gIZ8{~ +? ct;՛{/< ? A +KWխN+ŨUc'z M:f?ףcѽU˾({9UϪ/}&*b[1 ~l!r۝= ^uTRx(ܓ8&U7hJ#sHk`R>ĩdbZ֓b^̯Y6)Pa  +j3U`&=2v1%A=#'Nދ<,I(VGh:KG ] pe#)]6)0/eKڛJp8Um;qcwQ͖ܫC25ί L[{c 7RQ.dWYcτ4ҝ8U$;bYZM8ؓ{ubqtHV\3j#"4%XiF(3 $pȨT*8N:;-*41g~pL!gzb]^{Ӵ= +Xi)8ddp:0Iyz=)v@=Ɛ( LLݮ +(| +}vhAǃ} ^N  v1-\W\x ^b>lboͽvLvdvEf+]Tw"vm(`0ټJM*%N@Cи&;8&<ܮȥRDt]AkCۥf'\k3" zBtmHM D" cgPjy©߄/1ELS^d/dxei[1eASԽλŀZLf^N 0H!%ՉV,88x*=XR'd#ZSJ툴F̴O?ko$bƢE7=oOWjb{e-2$v*z7iBPRCt*e7wEl!ëuoae. Yq>mV3r)L +k18+zIX\D +5m3n2P6aw9B;|]ZB9B}&8u_WJyoU6g„q*]Xlj@KfyصPޠ$ql~Ljl $XP&Ys럝<_t&q|v(yЮ'AհoՔ/cq.+{Їȹ$ ZC٠I{A/`+ś(b2(pѯ$ +ګȽk-sVU$5g\='L- # _sBTuO٥ʾA2=y=UcbN]tOIy)@Kn[ +5a/ǿ :1j>:VdN*kpvP1 +d/}G۪! Z=}(w)@! ZV˶}z33ac{8fzUw4a1ʰ$HV"7a(S5zhS)IoΖ"ARR70N=ƺ?kV1+9I(#=AmrQ{3}l%׌AI$W};5}[\.-9dҸ/Zf@XIi{͗[-.0ˠl<ϻ[T +F>',/d'0viVS:Y3PvdܳVh0$^Q,>0f| +tޕG<'_OZ'AR/)TkB*?t;%ìy-C̵x `>mx tŠFNƵr5]8]}VRF= k [iKv?J Bf +R +N?M,]@Z[v Qp^ީa\}1@O*c-A/ӽdF6CWyQeEՉ+nCXlR=RX:۞g[u=[UW󕾅 QvXKuf׃lI8*pwک~hLt%xj?bh& Pie;h]VbG#M'"Ua$u. 7lST3G̛:wt Gx=FLqbb(Le'DXPꁳ41.vrKI7Dmqܤ.#N '{ r_ٮ-@ow{Ѯ"UqΤ}q:D>NSm||D;d/849_ l]hdV'|P.* w/S<J9kU/pCpݓ7&$li6Ҽ(7%~}BoZ:8xV{͐Dg7M L` H4MrW|{>\ZBtB˫)B9@XRuP%*n'ȮXWĒYO;`ިu+K_|Ţ6(> B{ny9ZHSm"D<'ljb3}co^IuERJ$ Pk0@'mnv%fB4#]Z`ñ"/u3] J2K]22dG3 +T;>2ɕ In&IIM "JQ~J*@8pJ觔,E9d$+tUcقnS&bҊ#tH$M|㤍lWHN΅f_[z " Zayˋw׫CbiSsG?441mO&}C+3De}0i?ʤƼ^5 q$EUCGyg5^ᯆp ^tt87CģcqI}$|1o H*o*VɮWNPWBf^dFϿY12!unG+]IQ=J3[JxI~7=̲S]ɦŔIDWd\bYKvۅ>zHJa7nԲ.{r8ov{1TIu+sנ=\lzU~M]z^9}Y?D$ɺ'ϕ~וr. Vk2ds(KN@(d=$gdLz&V6grz9i6*2| {dgO esSFeGT%ùܤ,/;AM+_61'RճEM$k'FSa!5e29B85>ochf}3bTɟgaHī% Fts9Hi೗$]%Ă}J> ovT} +4-OȞc{]eXrv 9a_1w*DB#UQ^? +9,VX%^"Bٷ=6sSg2d:IT@,Hn||?L˯ +wIgO㆘Kқ BIMz)E~a:>]|9VN}ud7(6у<`Wp"`Yb,&> _;h$!tO L ĜkG>*ăr0s=Rl"VV*Tu#*<,נA{ړVzċwvAoe\UPQZ_d;li @Y*8U L38piuJzlvd`(#zI }fon.¢k4lDے%N⍚aUQ".; <:Nv`CeT8¸rJq3WLJ;TpXV,)Nz;xM1 ݾ}(涛҃,MU>Vb"'g\{8n(xk.9`#RfH@Wv_#_)7 +6]A>,I(X )[LDA,FE#8힖]xU:J̮),6xNj5_sԐ#ИJٌo G o7I\l9ܢ`%"hY@1lcd4FO 3II \hXE<}OsEAPJ,P4f~k4+ \D(a!jôZᝍ(^鬬ɰֹrsl0D>f380LN}pYBE+jwcK0onnMg429OӋRn#j')!!bՍ,%Ja 1C̝P؏… +ssM(C:ėOϺd*eXO,-TZ~8k (9`ů@˰x~C*|IF.d- С {ٖ'Z9%G/#Fogo"a3`wNh>|mB'B o`ei*wn*CtQ#UP[yNuLQk#\h$)RHau +jf>1Ev_$J/I?"ʅnpr-4Z`iS㤦+aY$OdwH>2w=gțv'Idkֲƴ Q1?=q`p!xz19M!e8zG<gJT6%SwtͅT`mMK@yqdA]Ng1Xt7!>V&nei^Jq&F$#d 0Cύ3739,7Q2hypT|A8Ή<0 s=6܆c,OB![J{YF`y3A" e|mEci(I>A%F?SQLJ7nT_ mB.E=l/fb^;͊do>S+en&z_ ؃A|(X\iŇtWN"ky^2w*E)Zc4RC4)-y3 ;yڌ%,g49/>H^:c-~<@(BN2ͭek׾@$>H( 5N1@I SI)XʞwɻUN@k,L+,(?J>^~rHf:}X>VxacX +sa6$+.萋jע|pES'a\v[:w?0YԪ"x%1uh]G4vLhbQآA4(U [v/&Ŵt1"/STDU @dH}#Jw`]n~h!s01PzAmj LQ=s:'C*Eas.˝`YNkqcurB E F-(rI) >%4-`SgeOfa`zE!RB728LOQ'h}5<=DK&}vpPEPFٖ n +)Lji<7/c 8HHTM '?*.7T>@٭5ޘ!Kz$ksUnܓ9/ + )],O?xV+ m6T7(B}̖>Ϩy֥A,Y:2叨ѳU¦fs4ZgT1p6P F#&x`˸o(L;[7[4 @){uxa\3.Z{b +rd:\8,!0Ui-`JHOCC"6F1De`polNhfV<"8AYN6&d|-߃rT)Wn#sepkGx={WI$ JJݑwv%E|=Q܋pSl.L$z$8(;c#Jx  -BI]8Ϧav`ŨV,IY%\V-6JJ'!LȊD@QPݛe dY=Q9_L PEB<: %K;q!>X%؊LfŦ$gsqHL^S 3T2~\>E |$Cതuw)4vD1kaxŅy6}G׽.. +\? [G'Lo59q-'ꤧ'jH 8W9]t8W^10 kO_hAƚvfחD>t3Q*V0yIi{ M?2Z-nk2.GJ'rJ::[E.*@7H{yb*-@Sy7)`ta0b~L~/@;Bc +HsuNLzHW҂W"LJ=6j{H1RzW +> +fRә=g&1X\s}g8E3X#IE-xyI% +mhw# Ҋ> ~fRn +0FA1Adn7Hb+76K9zɨ_*5͗xӝ  +endstream endobj 69 0 obj [/ICCBased 72 0 R] endobj 72 0 obj <>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 61 0 obj <>stream +Hˮ$ zD]>F  yFb/z8TU 1NWQ EI~[Ƿ-lo>S(s9iu`-m򴸗1B3 [ȩ[nm+1ZcOqVb(sN=1Y2pf*R)ƾO^\15`d=^Gؤ`^ôʌX|UM[ ܶKʡǎ޵ Bm=#tLЫ+ď]mfAm5fcM=R3翸-s~q7W~{K!p7| gL\qnzU>%֎mw>0_ߕ^J9\?mi`-\J ] +&æԉ2<80UM1#^9Z&~45S3R N\v B] +,<GTCP2Y Pmt_-b +ls[Ci}ϸLH Marc``er07K8&ⁿlQjzuC!&SݠvJwU +S<]{ΊsĈID{U_?ϟ^VlmϺu~7hIaǷ?m}rO2gz41EmUm!.bƇE!/ b-KI+{,p6;WmBrxۣ~u`faك*SVP4J!8]7?!8vJ=!xj]>86:ƒ=Ns%2fsQW~Ӄ=]:0tb?xS5\sHǖ퐏o4Z|e6"-ң}vKVSZ\rW\"pϹ N>^[)t:u;ĵm}t_wb_ϊX^O y12x4 YL&ܓK +'7f]$wQX{:0=V!E-mǵ^PO \`O [M .Xx_0d f{%axn@2yF=8)뎙:(+U Y5T-" ӌւ0o8O|lK.*c"5 I7 d<@tqC>d# >[,+,\dj,OUpQg]Qri퓲5:NQ~9rZ}N`1Tw:2+/b8dRpj'NAj^pDTIny+9~PʋɊ%+B%XS JJJJ]|xxxD=#i +>dA&z0KRFYV,g4MAS^hOh:XJTᅮx<;W AzP KniƘ( XH)6T쏁OwIe/,:.ы}S +pt/N9Os^~֓6!=k +[*aڗ`5?Л{Q7@o1 + r.cŋ9$ŧC8:k2ݔ>4hMg(h@D%Љ2KnzãSf$u_P3j:_hG(z+ˀy1&V"(;W^6W+NHGu +0W]IYN5"Rx#>*6L-UR] +utL3mpW#aaO` ,W>>=6+²'ؕyF\Ӥz2Hxx,==.32IGOQ(LcH:OoYc?%l(/)Q<i$mUt +G'(Pa'b[m3veܫ,0`A;Q8+0Kr"B3|,gۢqQk>}*6уof+?,dI;|\YJz`JXS!e :rDDɏz5~I U2|#`kH&)C ۔6XW#H!:J%8&.z;{x;eBRߐH"!QN-C.l`P +F;:k)b/؊`Hc ,VR4SHtCN/ۓ +zkkvl([kKiυJGVl3RU r9?\<ʊ[~o? CF̬VΥ0*=}{1ޙX̾Kg'ZlNnQT Y3M[GYvhP=Z ڽ.r=$>m=q7"3<)@s^+LсN41E]a[1%J-eޓmNhC>a]+/jBLOVsb>f^ [dQg޿G~`,Ǖ3=8 Rc߁vчPpK6@cG:bJP:[jf#{ݸyAW}XU[Ŕ5ط5M~4%8l>zao0t B=b/ckXP`뿺q?]324bum +"'.7KS_"Wq6qշ}巉RB ZyT%;]qӫW3ٮ;$b +s?2} MJl ň|߮obCbwC*?y0j)>/\ 8UC/WgչP}~ Y%f{jyv|hHBS$1`$~lXپ@gΊۣ!,*W jT)45Ф1Մgҵ0SB&㲺g@ z+ZwhP?5PEGG uJPVB0%D| kG)4qf 6>cOhH3#_"?>+xNkL!ul<&D Em50U&Z9- +gL"B,`` LM]=ݦvk. z>@xhbd +#3czaCwPIiFuA u$Ԓ*>;e{Uh` j㾦(X؂9>>2*:5 ::uo&+jg:Wp/N[ֵ[s>:h6v{)0Gr_2j7BգZiax3V+xp\JOOM*G,U Uvk}&D`ߦzԓ? S,JAeSbU\% XwyRP?):m cJ`3Tg?qڱz,`-F5ZAi(X'~[Ծ"a>0 E+ZB,+],\urfrҦY:h\/m8a[vÜr:4B޼!5p깮^kZV_,2*3~Xk!>M%!?$ \j>a,.<:)p?NRnuA-z44ef;%Qe ҭkL-FFAQ!A^l$.ج,] p-PMcB[FݺXgJ:$}KBUSLJ<- Gt=]-B1Ɏέ Y~Ø G‚ I"<=>22`ЬeB5Rk/4U2Ӡ5ql1ĹVY?]]I1'XYnEǂ1,p~iJ0R&H(Di_<@VJˠ>V]a[W|S@V(V{k丵tn') ޒCC'Ðɐ?eE| =bj#.%(ۻ#2 D]DJXw-|r '.(^Yi f-Ě8Mj@H[Ԝ~hcv[;鮎1UG%ṅQF m9kK5 hpzy SǾIԹi2V'(,^)Ȉ u|l oDCB;f7tu;P< @=zH\7^kB\}j PY=q߾|3WxG yO{/(kilҿNO]&bWPTm8=:f݇ER}(e{>uhO#1+gg9n_{XK\n7;'B R`}Bik{*JJbʤyh)ۓ(jyF@ćFNpxLW,j3 ~*d1e~"z .FcڦcD_—/ΏWn~]'S&VORX(,9b{(1p0S| vVjsń{c'<fTc'kVJWXR:bGc_..2{inÈ^ʾ +{pGmsz}c}m= +endstream endobj 62 0 obj <> endobj 65 0 obj <>/ExtGState<>>>/Subtype/Form>>stream +/CS0 cs 0 0 0 scn +/GS0 gs +q 1 0 0 1 469.3965 229.0468 cm +0 0 m +0.265 6.747 79.142 11.406 148.236 7.672 c +164.593 6.788 214.003 4.117 214.188 -0.959 c +214.385 -6.307 159.905 -13.703 107.408 -13.425 c +56.998 -13.157 -0.227 -5.768 0 0 c +f +Q + +endstream endobj 66 0 obj <>/ExtGState<>>>/Subtype/Form>>stream +/CS0 cs 0 0 0 scn +/GS0 gs +q 1 0 0 1 577.8242 119.1698 cm +0 0 m +-4.358 5.713 12.555 15.082 76.06 15.597 c +135.072 16.075 134.396 17.479 161.399 18.35 c +185.488 19.127 231.423 11.842 228.018 6.544 c +226.015 3.429 212.372 -6.035 149.139 -6.945 c +145.414 -6.999 113.762 -14.039 48.62 -10.188 c +41.661 -9.776 2.807 -3.678 0 0 c +f +Q + +endstream endobj 75 0 obj <> endobj 54 0 obj <> endobj 53 0 obj [/ICCBased 72 0 R] endobj 74 0 obj <> endobj 64 0 obj <> endobj 76 0 obj <> endobj 77 0 obj <>stream +H|{|Wϼd3;JbSuMnբ%i% $]-Dq[B\B%!(f$YZ5>Zf23I>v<99Ϲ}s3 w?udqINVxpYT̴d}zχ@5&ilbq\ cf)Y ܝ7.6z}U> \WNWԺ>bW+4j'k})|&4!?St}L5=hOf3VÏxZqahJ}⍂Zdm#w>~h٪u!H歶nNvOaug}_ '}觟 >/FƗ1cnj7>.>!qĤI$O6=e̯5;5̙; /Xdi+VZf:lظis۶Zvܵ[ۓ`>R|%'O_(x +\VyqvjѾ|lrd0!X$LRt!C" /ԍՃĎPq8_LćsܚTAqg76AA~%O }* FH_HRJOKn"d*w=H9I!+Q4&8888׸ʸä21S$ښ67EbUM +W*͔JW(JU( +eWcRV=ͽQsybe%Rdqs;z8z9:N9j>u].9we2!R)[ +[)+ŅbqX!>e`AHmP{N1p +BPZPv3$K4P*nJwg ~}9vr'^G1NN3(063_S[(FFQ@15L%[bxSb9S,&KPN?ѕSpT:G:ՆQ|]Ii4E}~p_GxVjn*utWu倳y/0 +mC'!.ř%!bBGL۴Nh tu_xl ɫ/<՞Z_7y~zD߽ +~ӻ~ +{U?k .Y uzӭl\F" -`6b')99X { #y8RA&F 8 *Q|c,u\C%߰c!0HD& 1 I)dLt |YE؂TC<ʢդ"$'֣.b!-ܴ6&LٴHO9/v% vS|KN B*ԘSjJ>cO) *tj(ԊNjMd :K/^D2D<]2H2]` +!TNtQ%]G1-ntel [22`+*V5b[!gFmfl a[62:NvlgAu:bfGX1;ʎ+a');ΰcV.K2YکUn"*Qբ(2Q#jFNbW5Vn{vfw7=yvϞs D@ 1{/v;6M6^\1LI2c;Ƙ zB;o~/v>_sSȜ6gYSlΙ悹h.抹js9u9ǝ`h$[V6VmjlU[ͦs"$.|r1|/%+z#{=z`6f,mkZ6Ggpy`veg9vg]h".K2ܮÑ"2rԮO[mFɾg߷ȱȉȩȑHbڏ6;.{v~j?9S"W\S9r5N\388;,[G'cöG;ag]v{ao}8`Cqɵ8s6\/:q.%r\Wj\kqV򢴖67JM򣾎o97[msǹs8%Cy8ecɍk-q(h0`uj>@  9u ;uzP!*B% U [KSi՝P2  jB-ȑv܈ >Ʌ|o-_C.Ԇ:PA}h 4 0ނa*L0f, s[ ;'/xCxQ<Bt.+3Cu +)&C\btE]IWUtN#b$誺N)JrD%S +UJTP*QUFT7AETjQ:jSK>5ԈsԄLg,jN-7Ԓ^V"6GmԞ:G|gߣԉ:SJݨ;ԋzSK? }?Hg"]t~?o뷓KzK+ 2(x;L 3`6 A4PFiD/oi$;Ccx@Ыz^ބ|Xa ,eV@ +VX `=l&.>f}wWV6;x' %]K ɐLGpCচf9jZb\5jC\jڭ}jR}~+ 4[:BuFWuuS݆p=y݌weS~E(GP (WbFf=%*z^e{\(^+/uzx}o7NJbű۱qD+$$[1C'g\.[^~ WOA~4 'IpZ87 pQ8\. A,9(!9,GrBNJ"9-g99/\r%§d?)tN9EisqUnծq.ˮn7pC7Mt`⷗Ns. Z}->0.#%/eӕ=MܛX" ĖA vbČZE*hlSjiER i5QUDj7Jb(<'@`;hC##%rT8 ;v",$vwYx7祈ZIEt6bnN OEDɓne7ҭFn[խVwcnMdR'ƹnmjfi^7ܺn=vunwt;n3@0{'<)O{z%0jƬ5:i֛P(l+d;e4or4Q1m;fI7 3Ӽk2,*=7R%Kş߮mpgXh;?rN(H#xϿ§-ԝIUԃ"8*bwp/|.)F ySRo + r u4'31,'ji(z15??8s%3͖H9"?_XM39-EíΤ!e1|f/[X)zN9pIuDL&7b +_/P_#④o?Heh>guxPNXg0M=Q[JʹpQ5 NSTy"v<}HYؑ]6w-sx0rgT'WlyJJQq`ЗtQk_p.YxBtzc_NC*OM4vst/bDG:ZuJ~sQ1P8geɝ򄼦*VU oڤ.jn=,٥ ѻIZDgcq=.Th_ܞ;r/C83@3sdbDh! 1#l]qN!r*x}(0NN3PEr̓'uyCaʩH5^MR+FA12q~j UɪmX!/t=r?t W8DEsh0RlUʬ xC2Tq %sVFDROZ,"i hP%eX- yߤ<.?nitJ?"v≦tca]Tʡ[\s"P'p31+՞AW62H|E8{Am_Kfk-d,fIGVC{D?GXp9dC1O{y vp:o;vFXz`VbHy6u܇_ i R$KcgRJ 6"$;yU6?L*9XVu]rXf uhSUbb6ik=4S;c>YQLf8KԛL"_-U/gL + xz&vQ y*fh}r;b3IDq^J_K~>'rT*ǔfsWٴyccCϭ{vmݚ5Trʣ|bEyYieO-- +\Nݦ*2ZU*c˖Z>6 D-6fbK +KɷC,4>-c:buk#k$50WlZ$غC7X$lcpLxVDui4{bQlMY̢G[J%m-M)Lo5x㜄ݑ3);%ȷ2QZ㍋ƋˏɈWZ;{b1"] n@XĢDj5#6%} }f742Vn =*Qs`':&;'<':ÓH|5t)B*q)瓒CؗODB$ #+c42-J829P3k@HFk4\!I( 綃8cZaI*~xSe%md$erٕ$RMMJ'x.?o 6:OHtyzT14^L%_|y ҩ.Yr*,MUQjÑGա>32f2[uK}=VR:<eGkxQ/i{Lt~/.KGHNMOV4 2AE ^7dUHe]gwUKAVr -EbkSLMM&Fsy7u*(\@̤x?`Smv%٫xLgsJ/rf9AEgYi~̔㗔_[hrGC_o*qN웓mhvv&$lԐ%h2BK{EԫþrP.Qa<zVjHþxXl] v/`5`k& ۯ&"vONb y߳MiG|~7Y;";d~e Z#H"A@;3&#r9됟c.ԇϯühl@k?ɛDc!'/b1nׁ9=S0ubxn`tQ$KzāyPŴA ٤^q#Ĕѫ}e:j;G?CR6~aš0cH]ܢ2ZN6oa;&5 9N~ۯ᧕>W + !?އ4FM| +ǐ Z$Keu47 +4pw%={ wf#Ԩ&ߨDwپ'oc~^L|r_AZVe=-h J\dpq㒚qY41"&@r"L .a9AX 89., 7syky!>Ώr]`sq[ddq6~DR8rʱ^ ]|G^e>cvݘ}v[Կ=e?S~Nc_?.3/w>\y̱+#=ǃ7mi-y65 ^+8ژZezXq126g]_wqt<{Jc?h\{q7ìrYM-v Vh&S_/4dɥY"בYM^1vk;Oےe)vgfOd~+݇oϝo5]V,F{.c,qcsԝrCgtLrwe;,t7$WHfeOgu +_&fs7.^~T`cå8~9PKal!R]*v瓖sBm|Q&e^P$Uy F[I߁!6\~qQKLrRO}b|I +y!d~vSZ&H0~昊=!{£л`~+~:6cޗ^cw'Lպ*]2?ϬdvZǏP3e2\r,!g=@ܯ YESU0Y󑋼Cq}o˖l@oHFo:2<rJoH=? +9 ٶ2hrYskJotd唡me:>vW#" #j?/J<_) 8qQhGTzKy@C+6z#+$y.YmDer 2RCY98zWok2׍e l +nYF}aC np췵R1ژ?B<)wugJn5n%.Vݝq\h-Υ\(M`2Cmzh9YcHVۇIw6;GTԾ`GsGߖj<72 ;]gk*dq#[)䬎U=f6y>!%fVm1geYlz&bBƟq"Ϊs#Cɩ-":*E}JQj6ۙȍ28-9AO9{ cgHwRij3QDE 9Ge" )Eѐ[oC=BTt +YNv{냴N~3l5c -3rV2 cUš]e8a{8ѣ~Q_{8wz7o8w~> +;ơ%q=OWx"Sېw!'(cΣ?{c(}_Gײ-cuxAޛ^K֔vk֬ u%区5Q3;uץp=Iэ[wjQkRq3rMzjމt />d'<kH玓1_9sdkޠ>ܽ'= #q#̱;lt5Iָ9@).(eu0"j@+BA0ХȐK[f`#e^YBR 9A33 UAVEsZ_s{ss{ػzţh>}*}i4"ti>enn4K5UUZm[ +{jseK/a_IsTkcKܺYlo}_llU;lES'#(ޜ20x\udyxݙa=|T? }NԧE[uLon}HMRVIUKgMkQθv +%î1sX7C7ݕMwn~\.]嘕?Du/ܙ\әy^f5zӭy,guVF'zH;;2s>_OP-g:Rj;kngw85z6j_XwRێa/KuW6az+[oƩK$V Zǎ;d.?~*KiNǪ~aޗp&t;%0QG#;(De+6Ç;#ϴe(alL_D?wvzrwm'FLSGx3n@K5iءwalBn!,?gJ:(z%8 K2%>r oQ d td~u[NˣήnsKN:lQ;%EdmfT?%h{eE.: ud -qA0ؠw;ó>[N \oy%ec~p ++#*!_|`:bC̳:[۸R.VV)2߈?vNsMGɟ :+2s<<_E_)tfQ q4>UӦI+e÷Kw3KpaےgԹF[ѮW.כMT+Z=ZK"dc-1_6wAŖHƠ=R qK'qI\N'&Sgeec6=+bNb?4ѻN/i~HŞz)3H]%/S0ߕ.,XZ}3MszϿabkYz=u .j]۰Ybd{O<H~h ƹ}Y];};y.A(i{JmʠS+)ȷ`SF޳}[BKG7B^F=I + rzXV&/.КR#Q68/(Ww+{eQyb}LssE3K:YXS<[Aɺc t̓2t2ށx]j40.צCwٱjm۴Z{ W^;c~e\!~| uSFI`?wߴ-Gգ1\Y=ݼrǔEfjޠF4({ +^NYӠ p+p[kWEW-4vDž͔Y۳y31Xgj|U;S͆4f $˲Asf-:f%Z!g>5l[JY`Õ0S-[4a g` ݏ\O;/i,U56ۀM IxAJ9W\'z2#8>sZ16QۍOPC?~BA (^0sKA;θтt8]Nk9J>>iEkh8J(P^!%:vt$<ɀߡ@Ƨ{p]zE"`)5(P;U//`ЭJiYOaYfA)tl4Gu/s*i 1M,4GcLq"mO7*h)wc)|wݤܟjRq?fY.s8okXj̸{LyijMdyE> =aS79Lc_2N}aS[+O,Q0,:{ӯRd_=^ED38p>jG0wwwo/oCܽ]u΋I̳y2o=95C۔kpuqދ*y9?؆p]gŻ3 #⻎ +qOgY؍+ꬷ(Qm77Y~(e?16sWo[&un+F@ClD}ZzC'fxx{0s-^ʿfY}݉ܳ3ܿkԗSM+ŭer2tlno1/7sEBP( +BP( +BP( +BP( +BP( +BP( +BP( +BP( +Ba:Ä(A=`=?@W' +W d&1:Y.&v,N1a2>1~m=3x"dP!szEPIQU?Y=/e'u-]W9x-Gb"k#-II:N˫rJ{AeW;h9C QKQBL*kѶ* CoUᤨ%4Hc4 +FmF扳R;HcwNnGBN,Lc;p TvDMtPvڃhx^eCEmD$-k>[Eӂv{"vv9(*61P)lUf9*7s>'SxwUC~LQ*8F!k}DL +l+<~߸kTeN}b}9eROKɒRJ%[,VB4i w'b8~_'M%sN/ :M6k…rFH&%A&$H4]i]ŀXA%(Jզ.֦i餩~`Oަ{҇TK={~hrc@ȧM~Mp|V~65h yP¨6k_CiBS"m"QCGڮƯf<?%>J@L6o|[bvoĈÿCC7A“g^d)f"~@feN`6g +[TJ_hoR0VQ~p'8όM~ ކHAqTg\vFbܯC|KznȘr5/q"j +Qۄ.o6 f>icYHʧtS| mh [NWa +ԏTRtW;Z}oSh}hu,6`Vw>WDw(İƓMC?4vm;5Ơq+ϑ,?Kyձ*%]+G c.a))n/44e/lngp +Kn1!Z*M!(G֕vMU9lcs?n{GўVfq3x_a8= ͱ+wԼBӵ ʬZ?D@ 9&Ih +9< +~2CgqdAdAdAd5DV>)"""" &x3 2HHHHi""""&l6  aka5DDDDTQQQMX ,K ҄aia0A0A L&L?" j + +y%! + +F*@*@*@*ԧ>L@222زfxCu@8 & DDDDN9999Mv=eSK$}k䵿B'#qgIH;i#Cm@CyH$@ABl3ƢblZ4󹏸݋EwͬD;{W y](@Aٯkݏ^k7f=е]}=B I7E i{sOCP7ۄ }Srͺn}(-@8z $ze( ,ikcK.1/](|% p'%Š2| z[_߭ٯXݔ ?;.>MKu?y+?*3{J.X TtEo*`k"}n=?GpBDCb!xE\4xG-=Eq_]Hq*[1%b8#-^GsAz鵑o{|Wzj䱴ˎ'*#I1twhĚK, U]a8M#ae K>PaأZ\8v)C\t\ѫ\T׭e !:f=Hv0 +騀E*XN$BzxӉ՝9{!ٯaŴv/1@7-X eCPƙy%3u²rx(sK_uD`[Uռ/0'C=:(v$ C .LfHNHl9i.?a^DZ%FILnCֽ}]Zt`jp=︬~B>K`atґKT5ṿ6İoC +y8Ól'k<=n&j{u;+:B!t*/zru+#R ^gdz£i=cX\&^sQ=“8 ?eFl&|/M.7|"$tآ +y\j7[" _xU~EnCЅhJ +4 Wqy«o҄S Mnذ,{Ok6eR,NXEe"<7$(DӜh1) v z~ 0b +endstream endobj 63 0 obj <> endobj 73 0 obj [/ICCBased 72 0 R] endobj 59 0 obj <>stream +H\TKn[1 %~Di[*(.z& w9)twU>qvD/r_/R&RH??CA*B2dWVhLA͌{= gu#tyN.C&VFMIߋM訢'O5-q)bݥM|0J]JTQ__zՅPKv֗Jӫߥ nUa< 4#ߖ赴:gCqpt|t1na_Jy,-m6A9m 1uh$Ƙᨆv"Mp ގbfXPWmw@AUDIbdh~&Wb.A]HW()ˣXJASAUr\z +#{9@dJca4\9tB6(Ps6ȡȖŠ`x@Tj@ Z7!SY +ʘ;W<o:hja> {a-_wpAҦ< H zYC},֨_K(xcu v,Z z+:ԑ*b5ao Kǭ.("A!$T~&ۀxom=/w/ +endstream endobj 60 0 obj <> endobj 78 0 obj [/ICCBased 72 0 R] endobj 57 0 obj <>stream +HwVu6PprqVr)[ +endstream endobj 58 0 obj <> endobj 79 0 obj [/ICCBased 72 0 R] endobj 55 0 obj <>stream +H\nSA ~ƞ-PłUHx{/Uxlsru҇WcR?^ok\_'$C>c}xGa[`^EǛR)Pr=+|#6PdWSP+{2.Wz +p;N +{OKI d[]+iŕy +wF'#M,cN4`l4U|Ë(k?{Fڃ>cg,X +endstream endobj 56 0 obj <> endobj 80 0 obj [/ICCBased 72 0 R] endobj 51 0 obj <>stream +H\TKT1 sN$[jF,844ܞr> T*WJkwC$>5|_rQ/XL2T9()BS >"gK@r$D8IԹJ?Dnf=6Ȓy}N¹! ?⤳R .ΥB`o٥՜I-:ϰ tv0[TIpP $YDjigPw_a1̢y,IǬq ~ %7:294w$GEdI08Hǹ6nqeN 3ftH%ۚ 6[`!.轸o g% X&Yu*i JT] +7eœBd{Y H;kr yjB/k[,BpMp[Eu  +qG+<`ub5%נneq+5e4·Y> endobj 81 0 obj [/ICCBased 72 0 R] endobj 40 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 41 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 42 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 43 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 44 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 90 0 obj <>stream +H\RAn! ^\z>=RRFK6p=gxѦ ,ׁKQUn+(#!qriTA)Zխ ʱ>}=:n>lKo'<xZ[ eĜQyCUF +9I v6յG$f/x-tv*kw{z~_྾ > endobj 92 0 obj [/ICCBased 72 0 R] endobj 88 0 obj <>stream +HDQN0 +q:JAVhŁ pzceypl7xξO;,ۻǺ?ܫQF}@>HF$z ڏLDtb +s╢?fZc(xJlMUp&Ufa: 4x".LZv.xJ>nb&c#M zn/Lɷ[dg!)-N&͡R9mu5逞Bnc˩Wx^O_ +endstream endobj 89 0 obj <> endobj 93 0 obj [/ICCBased 72 0 R] endobj 86 0 obj <>stream +HTSn0 +fH}mvSQ=@6PrCkLrf(zQϑ9gp8 qe ΤTF9I7Zcro2Rn- +D"@gLMJsn/kÏ x +z6~+eT%VS[ WZ'$!Wiɜ=rݽ5݂iY#0 DEnn߭qV> endobj 94 0 obj [/ICCBased 72 0 R] endobj 84 0 obj <>stream +HdTK0 EiLՠt,: +QXq'U)j!'ұɺ6>S6K? #e›F6 +endstream endobj 85 0 obj <> endobj 95 0 obj [/ICCBased 72 0 R] endobj 82 0 obj <>stream +HlUInT1qKXE(bZ "Ey~G(Iˮ+%_8{)p,_9Wl}elJ"bJTz>?x PJr<2i4U5) +Si56Z S~~= +<~ eyDa#אU䶼Ѷ5| M)I>Ng5#Jt`oX+j,m ::Y+Q8x lctǼe΄R(7* ,ze"%KN+u$6?sMKESH{R Vs A\XP:_QC(W^bo0'8kt8@塈à'mYCL\•2|Ws!J;{'"kϵOnv~Ir|{Zc1]*N>j<tof랦xJeP2M$CB^˸BfN6+XZ8|Kujhg=QPז6%1|̍k'ūvŃ1'Wz<).# +ِoqQ%WPf+ +D] '@$TΨ-hn +P3e8Rc}`fp'=ǿG+n3 +endstream endobj 83 0 obj <> endobj 96 0 obj [/ICCBased 72 0 R] endobj 29 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 30 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>/XObject<>>>/TrimBox[0.0 0.0 720.0 720.0]/Type/Page>> endobj 31 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 32 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 39 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 107 0 obj <>stream +HUj1 )XZz +%mO)@޾3 P(IJ4son/)~x1X37_5O|ǧ)w'%AO > endobj 109 0 obj [/ICCBased 72 0 R] endobj 105 0 obj <>stream +HRA +0+n6&ݳQ<)|@PO +6z ĪCcĦ]D1l6jlߴ@Y0pO +endstream endobj 106 0 obj <> endobj 110 0 obj [/ICCBased 72 0 R] endobj 103 0 obj <>stream +HQIN1 }AFhāHHH{V:]Uvrzt6A0~i{#o@ǀ^KA_q>ZQ2h{+$L`B]0p;FIbjE %&DZ!b -@8aV^e3xOs6l"wU&2{^+RVC +hIj):)rzfvM,ogsU邳7 $PY!侅}y-GSŭL.2k/s\27^Q +endstream endobj 104 0 obj <> endobj 111 0 obj [/ICCBased 72 0 R] endobj 99 0 obj <>stream +Hˎ$ Ek,4liQcC As5-ـ^JFD2%yÇ᫯?-jcyGn=XKǰK4[yae{}Vdc﷞Y 9N>PpRiGU'ڑ= a3Zi51_VBp"+$ixZ@G|zwjrCv`Ya'\($o`tW|\JS-*DcT%r|֛Xqsx<&k|wvOtIP*|+3NE)6<'§r_)zU\>.y]CQ7}i,>J _w|-;Z@襨ŦzɧS:[$?K^w:T6:=b}R*.sĠ?v%w>|)\a]ו vß7cNL0Q"GX @xrIXF62iEk&ZUL{=Kzy2V:m݌KҳjsI. MZyDrWs*ߪW!TިU&B#(L?nͣ7 c4騤]̈}{8m;[ʥAOuy߅'aa㔢8fJ{i(3K̓Ţvw 7NC0oXX-[H! +\WTMns*Y 41 )姄HJ.ڵ (_bſHF؋f{sy 2Mpэ ,"|S9ͳ 4IORu/AVO{LEL"U[$g[8F #EmCV!ǗsiЃS:0b >BҫxAWhoty+RE1 7vK[e>p H\aEw4z'Ș~ D4)GM.?CUowahLxHu ǥf >mߜowZYܨBؤ"%a .G\\ZDD×T[%NO6=eͲw%3]r +,{8%woS`,i`. zKyM!!KOex E2ʣ?[ZxU<<:ZfSf֒?~guza:sݨSj.ӾKt)-$vЧ[ 47I5N}/ +'CL~$~ar WS)➚k<~MV0I@uT)W7ʐćA4߯R"PHf``qb/q4!"Hݘ|$^):4 23"1fʶ@!#RƽĮXPe|_R. p1"իZfOEdQgH38@GGG͡3Lj98:Sv>$T^˪Ch >Bm%FSFPJ$M^4WeOD^G9Wpb~&HB2$͞,x 3YjPs_̸{+,UZM<^9ҘX +0&ϙGLԚr5Y;(h*޴v}j5aɞ39,"i!#E)ue<\GBqb=yAc$vG E"-8dM^jK!:_[AYѽ;Рxoc $- 0b:]P+95mMANpv5?/c *JrZDl1ΗXY'dPid tSDBw_)&܎py?A kثud"vL:s?ېL0HU[KK73-8.XKhW֮Ih:L?u\Ь6^(.WfJwf$ +P  }N)i{ + +4 +{q2TdcfLm29/'VD6=z%Gd-ĥ,,.j?Q-M0_*բ>* _82f47o7y%joc#ZI39?6rcV(!'wT-VڟN+g8ʰbE1^?1P7o{ݲ)#folr{x'Zo"Y-$9va"e^vZnt]3924"L'B^#TAK]F +@&a2x罵݂84B׌ɵYr nʹFhDh r%z M-t`m۩VZO^VBH0 >3pFφ^.g Fo_7YDQUXϕ[8~_<rŧ8P@5̕0Y@ULRR+ӨSJֽt tCAaMRNS* f-퍝=o [}a`-O"uO*Oz%W7*(H+7~sOО[yFJOI[= % +wғPqSK8g=hsl'۳_fc5&B\ړz^y +~Z8Z3_ 4'y -Ŏ@ڭ4H#4C ȩp*/V7k4nw|'b0Fomv(ŊH=#+2*{ִM<ʿ +J}O +mx4B!$gRKlr3U6tȱQ"jV]r @xOoT۠}A)rоPwЬ"#UfJPCR:b_#'3c/|n/!힙;* nJ*IYev"+:@o\CBg_ Ey[oAI:[OP1l$qsPIj&e R`Wz~짠UFNLG,inx }`PCIIpne!*nC5|R @{W9]My +>6QX_Dk&JwJ" ng3:Z'jzuV +zS=a o6C%֗90ciD8{_M|hƢyf"J:jy`E I+CŽܨAINaͶK1!%;OJ\FWIx4zj`HiBjibrS ru2H uy̋`{VOKe$E&7i C7=#.D3l܋gߢlma-<)a #aчZzQRgQ~/lSHϷa(yDv ~ Ql}XHT:̫p~ȏ^C=t44B?}R.9#uM/F'5J @-mL3N}Q\JƠ=5hjRW4%QW&$B( ^Qe&/83@j ??W,$7z`![q%m+<+kw39V1GzhӄgG~,(ZB!*<=W^|cΰJF Tqˈ) ^<8FPs&30mP +$acq{7МдxNXIf\Q7ZBd_*L<Ɵzfl/b% a=Pb5%4٨gI[|rC=Uj'A*qA4U#C~KurlݐPݢ$r31t*&8V/L[SPUN'騮Q^.#GWhY{s.73 C8d`]l*F4*CR98) +*iԗذg] B_ݶ,Lû[zPR^bK_46>J +{֚*oL10[<~o_;YUl#䑻NR=йkCoLoA~]ɜJRk)S8OW[Z*;3`쉄ەs;=P) Ǔd"E{s]  o–HZZNTӽ0m`;P\|8,bZ:Ӗr4ݧƗxEShȌ|\QU #nAr;DeKNnbݝ ̇ncjLXj΢Ñ͹Q1=s*W^λʏcڈ\I‡uўPhbd+WkDHuJtnq | `vX(\o1x6j|3~.Ѻ +cF*'ћڕCOd Sw*H^ ŬH('y KQBX\$leǬ)@Dgh_e*^.j1tQ)Qs$?*}wV|&[ ~<>nD9.e4xIc:R?`7~J^~;/z܈-mF?c1/"3ݱN CNwUM1:}fwC!JDo#:< ?lw˄rs;}z6YN֦uZϦe1T$%⾓/R/! Xbܘ5Q\̽ޮol ߶Ǎv?.`#X +endstream endobj 100 0 obj <> endobj 101 0 obj <>/ExtGState<>>>/Subtype/Form>>stream +/CS0 cs 0 0 0 scn +/GS0 gs +q 1 0 0 1 181.1084 222.6855 cm +0 0 m +0.277 7.074 82.987 11.96 155.437 8.044 c +172.589 7.117 224.399 4.317 224.595 -1.006 c +224.8 -6.614 167.674 -14.369 112.626 -14.077 c +59.767 -13.797 -0.237 -6.048 0 0 c +f +Q + +endstream endobj 102 0 obj <>/ExtGState<>>>/Subtype/Form>>stream +/CS0 cs 0 0 0 scn +/GS0 gs +q 1 0 0 1 294.8037 107.4706 cm +0 0 m +-4.57 5.99 13.165 15.813 79.755 16.354 c +141.635 16.855 140.925 18.327 169.24 19.241 c +194.499 20.056 242.666 12.417 239.095 6.861 c +236.995 3.595 222.689 -6.328 156.384 -7.283 c +152.479 -7.339 119.289 -14.721 50.982 -10.684 c +43.685 -10.252 2.943 -3.857 0 0 c +f +Q + +endstream endobj 114 0 obj <> endobj 113 0 obj <> endobj 112 0 obj [/ICCBased 72 0 R] endobj 97 0 obj <>stream +H\RKN0 @8l)zb"`CzvU2Nf`#Dd<5#ѡpۨɰ˔=oCbp Sci6T[ߡ[y; nvRqrYE'/br=0IސR;8 BJ:m _CvWxH^W} +endstream endobj 98 0 obj <> endobj 115 0 obj [/ICCBased 72 0 R] endobj 5 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 25 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 26 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 27 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 28 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/TrimBox[0.0 0.0 64.0 64.0]/Type/Page>> endobj 124 0 obj <>stream +H\RN0 +um'JAX!.,KL +VmUy2cX_Br=WUhYI%π>~…tLye+Js9d5vK)qNi8F0T +B1(Na*l꣞Tr: "(Na ǛDٽe­G@6i,QA>.R.gǖ5qWF CYsnXdP ĶSS8nł$ݴ gBˢj#hoxۧahʹ4nL+}Ip qt<_|f2 +endstream endobj 125 0 obj <> endobj 126 0 obj [/ICCBased 72 0 R] endobj 122 0 obj <>stream +HTRKn0 Hm]E=CK:4m3ҌHї']N/'$T=y:$-]'NRnUy^%Kl*7$lu\ZUq>29Hn!,VE[:bsfE+{c 2nw[:tpӡ|̈t +\@ZFM,&B2f(q¢4v[눜gj=> +B>o&1p&P±kz4I$vTƿIJL1k4uv971H eJW1, ;*e~W: +endstream endobj 123 0 obj <> endobj 127 0 obj [/ICCBased 72 0 R] endobj 120 0 obj <>stream +HLPA0 (Rre*l4FcKwx)s=!N v;eq( \,O^龜άPN>'ԕҠͲUt#E}8`QfjE7nY8lhQ&Rɘc)RS;mb[AP +endstream endobj 121 0 obj <> endobj 128 0 obj [/ICCBased 72 0 R] endobj 118 0 obj <>stream +H4PIN1 ^^iqpa%QTUe ><w,ino *8A=yuٓ5tgq` +7"l +XZE,Uǹ{FlI38q>MYsiI,W]N1.Y>9R{G"vhnLbO ֓|.kW`Ҕ? +endstream endobj 119 0 obj <> endobj 129 0 obj [/ICCBased 72 0 R] endobj 116 0 obj <>stream +HLAN0 E>/Pױ %6I#Ke|z`̱?0֙s}K_J pCqP)PⲔC6 (c)c_"]m)J@`2= דD*IP2l7 n#j Ov#K6FX:1Kr5IܺeF=%tZ`;z:*Ew]׋^\^_]% +endstream endobj 117 0 obj <> endobj 130 0 obj [/ICCBased 72 0 R] endobj 34 0 obj [33 0 R] endobj 131 0 obj <> endobj xref +0 132 +0000000004 65535 f +0000000016 00000 n +0000000147 00000 n +0000043120 00000 n +0000000000 00000 f +0000321573 00000 n +0000000000 00000 f +0000000000 00000 f +0000046204 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000046277 00000 n +0000046495 00000 n +0000047905 00000 n +0000113494 00000 n +0000179083 00000 n +0000244672 00000 n +0000000000 00000 f +0000321952 00000 n +0000322332 00000 n +0000322712 00000 n +0000323095 00000 n +0000308348 00000 n +0000308719 00000 n +0000309187 00000 n +0000309565 00000 n +0000046017 00000 n +0000325716 00000 n +0000043194 00000 n +0000043287 00000 n +0000043381 00000 n +0000043475 00000 n +0000309945 00000 n +0000303534 00000 n +0000303905 00000 n +0000304276 00000 n +0000304654 00000 n +0000305032 00000 n +0000043576 00000 n +0000043954 00000 n +0000044341 00000 n +0000044667 00000 n +0000045053 00000 n +0000045528 00000 n +0000302841 00000 n +0000303437 00000 n +0000281615 00000 n +0000281502 00000 n +0000302214 00000 n +0000302744 00000 n +0000302020 00000 n +0000302117 00000 n +0000301225 00000 n +0000301923 00000 n +0000272690 00000 n +0000280387 00000 n +0000301067 00000 n +0000281713 00000 n +0000280449 00000 n +0000280900 00000 n +0000045858 00000 n +0000045955 00000 n +0000270006 00000 n +0000046088 00000 n +0000046119 00000 n +0000270041 00000 n +0000301190 00000 n +0000281650 00000 n +0000281439 00000 n +0000281943 00000 n +0000282196 00000 n +0000301985 00000 n +0000302179 00000 n +0000302806 00000 n +0000303499 00000 n +0000307464 00000 n +0000308251 00000 n +0000306784 00000 n +0000307367 00000 n +0000306233 00000 n +0000306687 00000 n +0000305812 00000 n +0000306136 00000 n +0000305406 00000 n +0000305715 00000 n +0000305777 00000 n +0000306198 00000 n +0000306749 00000 n +0000307429 00000 n +0000308313 00000 n +0000321098 00000 n +0000321474 00000 n +0000312136 00000 n +0000319880 00000 n +0000319944 00000 n +0000320391 00000 n +0000311665 00000 n +0000312036 00000 n +0000311322 00000 n +0000311565 00000 n +0000310325 00000 n +0000311222 00000 n +0000311286 00000 n +0000311629 00000 n +0000312100 00000 n +0000321062 00000 n +0000320998 00000 n +0000320934 00000 n +0000321537 00000 n +0000325291 00000 n +0000325616 00000 n +0000324908 00000 n +0000325191 00000 n +0000324532 00000 n +0000324808 00000 n +0000323986 00000 n +0000324432 00000 n +0000323474 00000 n +0000323886 00000 n +0000323950 00000 n +0000324496 00000 n +0000324872 00000 n +0000325255 00000 n +0000325680 00000 n +0000325741 00000 n +trailer +<<278610F25082F442A3D7EA5F5F65EB40>]>> +startxref +325935 +%%EOF diff --git a/messenger/assets/illustrations.ai b/messenger/assets/illustrations.ai new file mode 100644 index 0000000..6cf2b4f --- /dev/null +++ b/messenger/assets/illustrations.ai @@ -0,0 +1,1958 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[5 0 R 27 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + illustrations + + + Adobe Illustrator CC 23.0 (Windows) + 2020-08-13T20:07:11+06:30 + 2020-08-31T00:38:36+05:30 + 2020-08-31T00:38:36+05:30 + + + + 256 + 116 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAdAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8Aigufzvk/Ni58h695/vNC 1OaRjYXMkswtbguaxCIRlQiyr9jalfh67Yq9K/5UZ/zkN/5dSf8A5GXX/NWKu/5UZ/zkN/5dSf8A 5GXX/NWKu/5UZ/zkN/5dSf8A5GXX/NWKpZ5n/Kz/AJyF8ueXNT8wn8zp7lNHtZr+S39W5HNLZDK6 jlyWvFDQEUPfFXsP5G+d9T86/ljpGv6qF/SUolgu3QBVke3laL1OIoBzC8iBtXptirPMVdirsVdi rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVSPzh5rs/Lelm7mHq3EhKWtuDQu9PwVe5x V4V5g85a7rMjy6jesIDuLdWKQKPALWn0nfFWWeS9Y0m40i1t7e9gluogxaFJFMi/ETXiDXFWe6R5 hmidYLx+cLGgmc/EnzJ6j54qyjFXif8Azlb5H0fVvy6uPNDA2+ueW/Tmsb2LZykkyI0TkUPH4uS/ ysNupqqzz8oNd1LXvyx8t6vqcvr6hdWMbXM56yOvwF2/ym41Pvirzz/nG786T5qhvPKOuXHLzDpL SfVJpDVrq0RqAkndpIujdytDv8WKq3/OR350/wCELWz8raJPx8yayyetMh+K0s2fiz17SS7qngKt sQuKvQ/zY/8AJWecv+2HqX/UHJirCv8AnE//AMklpH/Ge8/6iXxV6/irsVdirsVdirsVdirsVdir sVdirsVdirsVdirsVdirsVdirsVdirw383tSlufNr2hJ9KwijjRe1ZFErN9PMD6MVYz+X/kA+fNd vTfzyQaFpTKkixUDyytX4ASCB9k8j4U8a4q9C8yf84/+V5NOeXy162l6xbrztJBNI6PIoqA/MsVq f2lIp19sVS7yBr11rXl2Oa8/3utpGtbs0pWSOm5HiVYV98Ves6HM82lW7v8AaClPoRioP3DFWB/8 5I/+SS80/wDGCH/qJixVF/kD/wCSa8p/8wK/8SbFXyB5Q8m+aDo2t/mR5TuJE1vyhqpkkhTcm2Kl nkUftcN/UU7MhPhuqt83+VfNc+laN+ZvmyZzqvm/Vg9tAw40tVUMklD9lW2Ea9kAPcUVfa35sf8A krPOX/bD1L/qDkxVhX/OJ/8A5JLSP+M95/1Evir1/FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXgH5o/8AKd6n/wA8P+oePFU9/wCcepYo7HzIZHVB9f6sQP2T45GUwOZpkIk8 g9cW9s2NFnjY+AdT/HIjNA8pD5sjikOheceWfy817QYdQikMV2L2+mvI2genFJQgCt6gT4vhPSoy xregaNbzW+mwwzJwlXlyWoNKuSNwSMVYn+eHl/VfMH5UeZNJ0qE3OoXFsGt7dd2kMMqSlFHdmCEK O5xV4V+W/wDzlZ5V8neSNJ8r6toepHUNIhNrcGMQ8eSO3aR0YHxBG2KplpH/ADlp+TujRTxaP5Mu dNium53UdpbWMCytSnKQRuoY07nFWtW/5yy/JvWIbeDV/JlzqMNowa0iu7axnWJgKAxrI7BDQdsV UPOv/OXvk/zB5P1zQLDQtSF7rFhc2EDS+gEV7qJoQx4O7HjzrQDfpir1j/nG/wAuax5e/J/RLDV7 drS+f17lraQcZESed5Iw6ndWKMCVO46HfFXpmKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV 2KuxV2KuxVi/nHV9Qs5beG1lMSupZytKk1p1znO3dblxSjGB4bDt+zNNCYJkLeda0PXvPrU4Ek8o HOZgCzFRQVY7mgAGcxl1eaQ3nI/EvQafT4xdRHyQQAHTMMlzXYqrQ3d3B/cTSRf6jFf1HLIZZx+k ke4sJY4y5gFCR+evNllesY9TmkWOQn05mMqkBj8J512zdY+0c8T9RPv3cSfZ+CQ3iPhs9/zuHiXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqwvz7/vZa/8AGNv+ JZyPtH/eQ9zvuyPpl72Eap/ur/Zfwzmpcnf4OqAyDe7FXYqxy6/3qm/12/Wc2LEcn07nor567FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWF+ff97LX/AIxt/wAS zkfaP+8h7nfdkfTL3sI1T/dX+y/hnNS5O/wdUBkG92KuxVjl1/vVN/rt+s5sWI5Pp3PRXz12KuxV 2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KsL8+/72Wv8Axjb/AIln I+0f95D3O+7I+mXvYRqn+6v9l/DOalyd/g6oDIN7sVdirHLr/eqb/Xb9ZzYsRyfTueivnrsVdirs VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVSDzN5eudUeGW3kRXiBUr JUAgmuxAOaTtbsyepMTAgEd7stBrY4QRIc2H6x5M15I/VESPHCGaRxIgAUCpPxlfDOezdiamIugQ PMfpp3en7Uwk1e58iw5b20bpMv0mn681Hhy7nboiJHlQPEpdD0ZdwabdRkaKDIBMdN8t6zqRf6pB z9OnMl0Wlen2iDmXpdBlz34YuvMONn1uLF9R5+9Uh/KDzHcXZe5mt7aB5CXPJncKT2VV4k0/ys32 PsPMT6iAHXz7bxAbAkvY86x5V2KuxV2KuxV2KuxV2KtMyqpZiFVRVmOwAHc4qxYfmx+VhNB5y0Mk /wDaytP+qmKor/lYfkD/AKmbSv8ApOtv+a8Vd/ysPyB/1M2lf9J1t/zXirv+Vh+QP+pm0r/pOtv+ a8Vd/wArD8gf9TNpX/Sdbf8ANeKp1Z3tne2yXVlPHdW0grHPC6yRsPFWUkHFVbFXYq7FXYq7FXYq 7FXYq7FXYq7FXYqx78wku38m6otry9X01Lcevph1Mv0enyr7Zgdpg/l5V+N9/sc7s0xGojff+jb7 Xz47ogq7BQSACTTc9M4oAnk9qzHQLq2XTbWFpkEz8wkZYBiQzHYVqdsoy4pA2QacXIbkWb+SFnOr lo6+kI29Y9qGlPxzbez4l49jlw7up7VI8LfnezPM7V5x2KuxV2KuxV2KuxV2KuxV2KvEfKfk7yd5 c/NPXfJGraJp91Ya8G17yzNc2sMpCv8ADe2QZ1baJxzROyE4q9E/5VP+Vn/Um6H/ANw2z/6p4q7/ AJVP+Vn/AFJuh/8AcNs/+qeKu/5VP+Vn/Um6H/3DbP8A6p4qx3zP+WP5awzwiHyno0YKEkJp9qtd /aPFWOfljp9t5Y/OXUPLeiL9U0DVNCOrzaYpPopew3kdv6kKE0j5RyUYL1oPDFXt+KuxV2KqdzOt vbyzsCViRnYDqQortirxfyF5J1L8x/Llt5282+ZNXW41rncWWlaZeyWVnZWxciKJEi4l34qOTt1+ ipVZF/yoXyz/ANX3zH/3GLv/AJqxV3/KhfLP/V98x/8AcYu/+asVd/yoXyz/ANX3zH/3GLv/AJqx V3/KhfLP/V98x/8AcYu/+asVSPzp+W+peSPLd95v8neZdZTU9Bhkv5rHUr6W9sru3t1Mk0M0UxPW NW4spBB+8KvWdD1SPVtF0/VYkMcWoW0N0iHcqs0YkAPyDYqjcVaZVZSrAFSKEHcEHARagvE/M3kH zNofmKHUvL9q1xFb3AubAqhmVSK1ilQfFSjUr9xrnMT0eXTZuKEeKPSt3qcOuxZ8PDklwyrfp8U4 1CL8wPNQ04appyWNpaTi6FvEknqPMgZE5vLxCKORPvktbn1GeHAMUhfk4+CGnwEkTsvQfLuj/oyw Eb0NxIeczDx7L9GbTsvQ/l8VH6jz/Hk6rW6nxZ2PpHJNM2ThuxV2KuxV2KuxV2KuxV2KuxV5d/zk Hbmw8n2/nizITW/Jd3DqOnyfzpJIkFxbOeojmjejU8Bir0PWtd0bQ9Pk1HWb6DTrCKgkurmRYowT sBycgVPYYqp+X/MmgeYtPXUdC1C31KxYlRcW0iyKGHVW4n4WFeh3xVMsVeeat578l6xrg0rStbsr 7UbdXE1rbzxySKVPxCik1496dMVY55Y/9aGX/wABKf8A7qUOKvZsVdirsVQurf8AHKvf+MEv/EDi rCvyB/8AJNeU/wDmBX/iTYqz/FXYq7FXYqxX82P/ACVnnL/th6l/1ByYqivy8/5QDyz/ANsqx/6h kxVkGKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvNf+ckf/JJeaf8AjBD/ANRMWKsy8y+T/LPm eC2t/MGnQ6nb2c63VvBcAtGJkBAYpXi2zEUYEYqlIt/yw/Llby+L6d5Zi1eYS3JklS2jmmUUqkbM FrQ7hF98VUrD82/yo15zp1p5o02eS6rALc3CxPIXovBA5QsW5bcdz2xVItW/Kz8vNGvNIk0rQbWy m0wO9nNApSRWcnkWkB5ydf2ycVRflXzbx/MV/KH1Wvq6Q2rfXvU6eldJb+l6XHv6vLly7Upir0XF XYq7FULq3/HKvf8AjBL/AMQOKsK/IH/yTXlP/mBX/iTYqhpf+cc/yVlleWTyvA0kjFnb1rnck1J/ vcVeiWttBa20Vtbp6cECLFEgqQqIOKjfwAxVg+ufkT+U2u6tdavq3l6G61G9f1bm4aW4Uu5FKkLI q9uwxVlXlvy3onlrRbfRNDtVstLtOf1e1QswT1JGlehcs27uTucVSf8ANj/yVnnL/th6l/1ByYqi vy8/5QDyz/2yrH/qGTFWQYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq81/wCckf8AySXmn/jB D/1ExYq9KxV8v/8AOT2gXNl5903zZqNubjy7cWCaet2ymSO0uo5XcBxuEEqyfC3jXMbVQlKPpd97 O6rT4s/78DhI2JF0fx1eQapb2XmKW30HQLeLVtb1FhDY29sFdgx/bLrsiotWJJFBudq5j6OEwTdv T+0Gv0RwGMeCczy4aNedjlT7Lv7CfTtM0fT7ic3U9nZxW8ty1S0jxIEZzWpqxFd82L50lHlXW9LH 5otoZ0yM6q+hverrFE9VbdLtImtq8efFncP9qlR0xV6XirsVdiqF1b/jlXv/ABgl/wCIHFWFfkD/ AOSa8p/8wK/8SbFWf4q88v8Azl+ccN9cRWn5cw3VpHK6W90ddtojLGrEJJ6ZgJTku/Gu2KqUXnb8 6mlRZPy0gSMsA7/p+2biCdzT6vvTFXpGKsV/Nj/yVnnL/th6l/1ByYqivy8/5QDyz/2yrH/qGTFW QYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqxz8xvJ6ecvJGseWWn+rHUrcxx3FKhJFYPGzDuo dByHhirEbH8wPzdsbWO01r8urm+1KBQlxfadfWZtZ2Xb1YxI6OgfrxYVGKr5/wAxvPlxC8Fx+Vmp zQSqVkikutPZGU7EMpkIIOKoHSfNPmHR2dtI/Ju505pf7w2kmlwFt6/F6brXfFVLWPOv5l6hOjj8 tdTREXiAbuxJ61/35iqM/LLyh5wl8533nrzVZJo072A0jStFWVLiRLYzLcSTXEkdU5u6LxVTsOuK vVcVdirsVUbyA3FnPADxMsboGPQFlIrirxr8oPzO8neV/JVl5O83ajD5d8yeWw1hqFjqDiHkUdik sLtRZI5FIIK/qoSqzb/ldf5R/wDU36V/0lRf1xV3/K6/yj/6m/Sv+kqL+uKu/wCV1/lH/wBTfpX/ AElRf1xV3/K6/wAo/wDqb9K/6Sov64qxT8z/AM3vI+r+StV8t+V9Th8xeY/MVrNpWmaZprfWJGku 4zCzsUqsaRq5cs5AoMVeneWdLk0ny3pOlSuJJNPs7e1d16M0MSxkj58cVTLFXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FUo1z/CPqRfp39H+pQ+h9e9Hlxrvw9Xt8 sVSz/kFf/aj/AOnPFXf8gr/7Uf8A054q7/kFf/aj/wCnPFXf8gr/AO1H/wBOeKphon+DPrD/AKD/ AEd9Z4/vPqXoc+Fe/pb0riqdYq7FXYq7FXYq7FXYq//Z + + + + proof:pdf + uuid:65E6390686CF11DBA6E2D887CEACB407 + xmp.did:4aa7db9e-e09f-f940-aeb6-10f22907a77c + uuid:1bc3089f-8c95-486f-9397-aeec6ccc4164 + + uuid:3e593450-be4b-41f3-a1ac-f0c60f6cd9f7 + xmp.did:e4637d7a-1015-5a4e-8a54-8a417d835ed6 + uuid:65E6390686CF11DBA6E2D887CEACB407 + proof:pdf + + + + + saved + xmp.iid:4aa7db9e-e09f-f940-aeb6-10f22907a77c + 2020-08-13T20:07:09+05:30 + Adobe Illustrator CC 23.0 (Windows) + / + + + + Web + Document + 1 + False + False + + 1920.000000 + 1080.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<>/Properties<>>>/Thumb 33 0 R/TrimBox[0.0 0.0 1920.0 1080.0]/Type/Page>> endobj 29 0 obj <>stream +Hˎ,GUe2͂h^-@=YU=ǀX@F]]߽m|o=~zͻGn/|aۻ۞_6oۗ'O0~p5dpᡟ~xo(=n;+[:|[8\L3u)I궧~)d?99|Sk>bFd<H.^Q&}{Gr'-@)PEz~.3^>;\k9\J}k%!֥nX tݎj^>.E|Gs&p]s81x#vC" Hr{ZS,z2敒3Y`MN ;n"rdh<)L2nkוx[[>-@޲h$IoreE8^ڡOƞHIKq܄4m4shZPL秙D_1%C"dA2Iж>5&X [$bIA^au o31xRh iq>*+DZ|GN_ڑ!A[YF4/̠h9.:V2,+6qgxɥ߻[ݽC.+~$%g-qK6C5 +Jamh-MPRNʧ#j2`fRǨ\pSiC=:;V=]TjЀC9P~HCMg3$ ^Qe4t^G +1t20WvJJGIq_ FU!(J=<h4DDbH:VÂASei.YP0ijBM +.P)ab,G,qS˕S3RwӘW?KjZ>9?\IIJ%ѯSӴ+tB +I=ڶ?E])=ɫA4&bP(hK[V{T>3W{V0eFXPY/TnK7`B5GO[Nz:#Kp[C$1Y(yPoqS-mQi& 8'B% ljHaW4g&!&Y<И/Z͚x'߭r-Qi#Y܍e,'n)ېOO ^O:[nw; +M$.!Hm,ߺp$R"{IEG7i.A-NKX'ƀ\sڿLh^3 +Ypn.c"֫]"[*r2j{~jSZ X9yQW25߾)<T"EKD>`v±chd*g54ZvhYpr#w[V+dLF[-yD+d;!u ?msXKBL'REjG +btϾ(?ΐRV%u{$cqyy:V~}86mJɯֱmgh?b:9<^/PUO ^5ћwlscrhNp@(=Jȇ׳)vǡj edq&0Nі6-u=Őnp/ LCg̃ Zh(28%g& Xn/6y7哆c@",˩q6S$%\2obRdpzgtJqta:SQ?G҉уﯥ(0k!̬]T 4#aY)APeX$I-pqBv-BaS7У)LqSWԐ=S4֪4Җ3llc8&[cOZa",u&*OZPZ^rNA55m,[I)EjzȗE2%}/c`a=}2ɝi-ADZ'hjjlIn|' o Z>K^ 96Im4~2Ņ(.@%3,]`>4Uuˈѣ 0 +endstream endobj 33 0 obj <>stream +8;Z]"0lFoP&4O-jn)K&Q\]t[mZ@ED:"tV([98H"P]_sVp +`^2NLd[YPW#Ur%'DuGXK>5_M^.`&[?gSEcc/4V]mOE*XEdrti>'!`.Ddi8rD/W^#V +l);M6$\BY$O1IX6MPal[hR%4 +T^TJseIEV1IterrVa;HG/=/O<12lPl[PAO+?089F5!%NGSa]c`>3B399he>V9hcRQ +%01CN>bM~> +endstream endobj 34 0 obj [/Indexed/DeviceRGB 255 35 0 R] endobj 35 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 27 0 obj <> endobj 36 0 obj [/View/Design] endobj 37 0 obj <>>> endobj 32 0 obj <> endobj 31 0 obj [/ICCBased 38 0 R] endobj 38 0 obj <>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 30 0 obj <> endobj 39 0 obj <> endobj 40 0 obj <>stream +%!PS-Adobe-3.0 +%%Creator: Adobe Illustrator(R) 17.0 +%%AI8_CreatorVersion: 23.0.5 +%%For: (sairaj mote) () +%%Title: (illustrations.ai) +%%CreationDate: 8/31/2020 12:38 AM +%%Canvassize: 16383 +%%BoundingBox: 58 -788 1726 -53 +%%HiResBoundingBox: 58.9851632707469 -787.03901575007 1725.84926401795 -53.8800000000001 +%%DocumentProcessColors: Cyan Magenta Yellow Black +%AI5_FileFormat 13.0 +%AI12_BuildNumber: 625 +%AI3_ColorUsage: Color +%AI7_ImageSettings: 0 +%%RGBProcessColor: 0 0 0 ([Registration]) +%AI3_Cropmarks: 0 -1080 1920 0 +%AI3_TemplateBox: 960.5 -540.5 960.5 -540.5 +%AI3_TileBox: 564 -846 1356 -234 +%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 +%AI9_OpenToView: -547 365 0.347222222222222 990 602 18 0 0 78 121 0 0 0 1 1 0 1 1 0 1 +%AI5_OpenViewLayers: 7 +%%PageOrigin:560 -840 +%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 41 0 obj <>stream +%%BoundingBox: 58 -788 1726 -53 +%%HiResBoundingBox: 58.9851632707469 -787.03901575007 1725.84926401795 -53.8800000000001 +%AI7_Thumbnail: 128 56 8 +%%BeginData: 5079 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 +%524C45A8FD137DFD5DFFA8A8527DA8A8A8FD08FFFD14F827FD5BFF7D7D52 +%5252A8A8A87DA8FD06FF52F852FD0FA8FF27F8A8FD59FF7D7D527D527DA8 +%FFFD04A8FD06FF27F8A8FD0FFF7DF8A8FD58FF7D52527D5252275252A8A8 +%A87DA8FD06FF27F8A8FFFFFFA8FD06FFA8FFFFFF52F8A8FD57FFA87D527D +%525227FD0452FD04A8FD06FFA8F827FFFFFFF87DFD04FF7D27FFFFFF7DF8 +%A8FD57FF7D527D522727A8FFFFA85227FD04A8FD06FFF827FFFFFF27F8FD +%04FFF8F8FFFFFF52F8A8FD57FF7D7D525227FD05FFA8527DA8A8A8FD06FF +%F827FFFFFFF8F827FFFF52F827A8FFFF52F8A8FD56FFA87D527D2752FD06 +%FF527DA8A8A8FD06FFF852FFFFFF27F8F87DA8F8F827FFFFFF52F8A8FD57 +%FF7D7D525252FD06FF7D52FFA8FD07FF2727FFFFFFF82727F8F8272727FF +%FFFF52F8A8FD56FFA87D527D277DFD06FF527DA8A8A8FD06FFF827FFFFFF +%2727FFF8F8A852F8FFFFFF52F8A8FD57FF527D595252FD07FFA9FFA8FD06 +%FFA82727FFFFFFF827FF7D7DFF2727FFFFFF7DF8A8FD57FF7D527C759FC2 +%C8C2C8A0C9C9CACAFD08FFF852FD0EFF7DF8A8FD54FFCFC8C89FC1C0C1C0 +%C1C0C1C0C1C0C0C0C1C1C199C2C9FFFFFF27F8FD0AA8FFA8A8A852F8A8FD +%53FFCFC0C7C1C198C1C0C198C1C0C198C1C0C198C198989899FFFFFF52FD +%10F852FD54FFCAC7C1C8C1C1C0C1C1C1C0C1C1C1C0C1C1C1C0C198BB98FD +%04FF7D7D527D527D527D527D527D527D527DA8FD54FFCFC1C19FC79EC198 +%C19EC198C19EC198C1C0C198999899FD69FFCFC7C1C8C1C1C0C1C1C1C0C1 +%C1C1C0FD05C198C198FD69FFCFC1C1C1C7C0C198C1C0C198C1C0C198C1C0 +%C198999899FD69FFCAC7C1C7C1C1C0C1C0C1C1C1C0C1C1C1C0C1C1C198C1 +%98FD69FFCF9FC7C1C198C19EC198C19EC198C19EC198C198989899FD69FF +%CAC7C1C7FD04C1C0C1C1C1C0C1C1C1C0C1C1C198C198FD69FFCFC1C7C1C1 +%9EC1C0C198C1C0C198C1C0C198C199999899FD69FFCAC7C1C8C1C1C0C1C1 +%C1C0C1C1C1C0C1C1C1C0C198BB98FD69FFCFC1C19FC79EC198C19EC198C1 +%9EC198C19EC198999899FD69FFCFC7C1C8C1C1C0C1C1C1C0C1C1C1C0FD05 +%C198C198FD69FFCFC1C1C1C7C0C198C1C0C198C1C0C198C1C0C198999899 +%FD69FFCAC7C1C7C1C1C0C1C0C1C1C1C0C1C1C1C0C1C1C198C198FD69FFCF +%9FC7C1C198C19EC198C19EC198C19EC198C198989899FD69FFCFC7C1C7C1 +%C1C0C1C0C1C1C1C0C1C1C1C0C1C0C198BA99FD6AFFCAC9C8C89FC1C0C1C0 +%C1C0C198C1C0C19FC8A0C2A0CFFD6FFFC9C198C198C1C1C7C1C7C1FD75FF +%9F9299989998C7C1C79FC1C9FD74FF98C198C198C1C1C8C1C8C1C8FD74FF +%9F9899989898C1C1C7C1C7C8FD29FF522752275227522752275227522752 +%275227522752A8FD35FFC998989999C2A0A0C1C7C1FD2AFF2752A8A87DA8 +%7DA87DA87DA87DA87DA87D7D527D2727A8FD11FFA8FD1627A8FD0CFF9998 +%6ECAFFFF7699C0C9FD2AFFA827FD11FFA8FF277DFD11FF2752FFFFFFA8FF +%A8FFA8FFA8FFA8FFA8FFA8FFA8FFFF7DF8FD0CFFCF989975A19A99C1C8FD +%2CFF5252FD0FFFA8A8A85252FD10FFA827FD16FF5252FD0CFFC9989998C1 +%C1C8CAFD2CFFA827FD0FFFA8A8FF5252FD10FFA852FD16FF527DFD0EFFCA +%FD32FF2752FD0EFFA8A8A85252FD10FFA827FD16FF5252FD42FF27FD0EFF +%A8A8FF527DFD10FFA852FD16FF527DFD42FF27A8FFFFA87DFD07FFA8A87D +%A8A85252FD10FFA827FD16FF5252FD42FF27FFFF7DF8F852FD05FFA827F8 +%277DFF7DA8FD10FFA852FFFFFF7D525252A8FFFFFFA8FD0452FD06FF527D +%FD41FFA827A8FF7DA8A87DA8FD05FF7DFF52A8A8FD12FFA827FFFFFF7D7D +%527DA8FFFFFFA87D527D7DFD06FF5252FD42FF27FD0FFFA8FF7DA8FD10FF +%A852FD16FF527DFD41FFA827A8FD05FFA8A8597D7DFFFFFFA8A8A85227FD +%10FFA827FD16FF5252FD42FF27FD05FF7D2020684420F8FFFFA8A8FF527D +%FD10FFA852FD16FF527DFD41FFA827A8FD04FF2E44B5FCB58D2052FFA8A8 +%7DA8FD11FFA827FD16FF5252FD42FF27FD05FF5944B0B5B58DF8FFFFA8A8 +%FF7DA8FD10FFA852FD07FF7D27522752272752FD07FF527DFD42FF27A8FD +%05FF27206868F859FFFFA8A8A85252FD10FFA827FD07FFFD08A8FD07FF52 +%52FD42FF27FD07FF7D522EA8FFFFFFA8A8FF5252FD10FFA852FD16FF527D +%FD42FF277DFD0DFFA8A8A82752FD10FFA827FD16FF5252FD42FFA82727FD +%0B522752272752FD11FFA827A8FD15FF27A8FD44FFA87DA87DA87DA87DA8 +%7DA87DA87DA8A8FD13FF7DF827FD1352F87DFD69FFA8FD147DA8FD9CFFFF +%%EndData + +endstream endobj 42 0 obj <>stream +%AI12_CompressedDataxk\Ǖ%]C ؃q:*+<ۂE!TfwsNɪ"%JnUH$+#2Nxϵ]}w?Gux?o_A>>ŋo~䳟t<*U7_?.O~8|{^ůO?EsnQm~a7髿>Z`凧߼?^_|:,pgw_9y`.d~f>z"e1{uLI͞ϯ}O߼~vׯ_~/B{N/> s+wO6_y0x~!=yW/woPY~yZJɿ}vm7z_ϴJ)Www/z9ˋipkKk*gjeo8WwuJ޼,sJ?kg߼{ϯ0|˜w/Oe*wO-6߼MS0~;.Ww~_dxxѺhV?!(sIc˟I얝E.ܧX߾y~̿|e9{̛_?}<ϟNoχ߽~Sߟ*~1/_Ǭ?ًۻ>7~ ;4i[{OBIO3U9)mo|{@=1?' {?zU̪X)n(~]УWaUMZk\ˍokYL̪UqQs(V%\?iU{3zA9uYO Uơ?˓O&J^U9Қ)Sêr5:WUk챦U :JO^seYdۺc5)me彬8V:ʸ\$t_cȿonW=ZZW.I)NXӲV\ZRnjeuA55ղZz]fΉ7}2j]GdDW+[y!(g{B9,v9%wrb:-2pWqN1$wRB9p!F9u<[lIrɧb*9ݤ[l{eCq&ŀUJWr}uVxwxJtsV }2au>]__o,;uϧ|M`n܍(9\rkbjn +^D9[4XȚ*YͪWzUNr)U Wŭ]FhjR6o?M.SƿOr*yUҦU K ӊ*fUiI?׫r*znWqvmն+]ztYv^g{aW8u&7sl ZKZTKj8);9Q(~NRjRR-88:"T d[K]]鍬I-:yY?l_ɾ2NfO|d6.ʻJ,oq%2J'Xqn0קHZi"@ HF-uɾ1YL DҁXLve!9M9zXJņ8cN 9a % "qb1x"@C ]&:czO W@  ׂ# ڟpqdpqXܜ-6Kr…qDp9\E%u +cĵp9\bƹk//cVQ \[mkږޕ ka(f{OpH 쳌=氻¾Z]#rK7ʠ +rc˝ W6ލ^ r/fr3{l<ll|p87gik\K!!{6` ',/2_n'\:dp/0**{a 8\(܊)0LH?Pixu۹W\وX b=BŬ"dY +Y"b!ZNEТU-[-ZN"h"h=D--V3D"lQ2E\\E +Hʰwu+b"z]AEr"~icEWBŰDz)$-h-"'_PvY(Ya3nqn+|P,9(\^,t;U_^ _z]tM[^䰢/޺}:; g#eiNߒj|ǧ5d럭Rp\D"""Rlu~~(aSl4q22Wy]Ncu9OeKm \[niZĹ %\,]Pҽ%?PR zrzdYΏ+qto)]㥺Y)T{M5tFiXKӿZv=mmib9BmYm7MqcWM1޴kIZz9,tԆBӦbC.PKThMyNL{9S.ӘEAґGPRSN<*tS}O;m7[0vE*bb ! b0&d\S ^L ''1ȥS VXVB#ldoĜ\ ʖ\hbV.+ai\a*f'bjN|y:wHbz9 + _㦚N ~#?Ҟvo1M8F635y\+jɻG1Z'YLWɳod6Jhp6j[(m.TQӅ.s \BWTqC nK01e eҲzpF3}Z<|%3p%"R:_+r}0W8$ut8;h8-cڞҫCGȏrcCAqWph3:$!9OXoӨ{}l`7&:r3u}9Z2=UD9;>QGAb 2ie +QFWesy +[GɇĬ?q5{T{AQg0uapyǠyƬC0euNVK"ŔERy=?(o⑯&GQ:)9krll[ym\dٟI/tl֎*!eZk$gA +-__G;vz/GcׇJ1Zd}GNZp p/};wn/WGevy+;q2-ˢ2&{ƀ [Jȓ'.lhJXIdImy6 ILQS'p{~F}_i ŃQ#:śS$*UtRWꑞRA|xsYgoW^94:7's_m(C-NtMpKIrm)]c*qսG<6LUݭ!Z~EU^8&K$/̀S)\b՗)ao&7uw.aE bHd̂jp+" icLnHb5У,Y\ӵHMV)J%Y"ئʾ$HMRYV'm_%\ y%%k ىv!G|%d)#EF^I'EFfrvN*oJ=e F׿݁ti foۭkǟ +ik th`{: T% XnVK{C VI?H`+Nkӎo^o>J4/hIn+aI[G1,%;umZCQ,n B9ԳFA#W(O*%]55]{n}s޹?'6%{G/]͚s{~S*\9r )rn9!{1e,6P31ȕmł}8Ų Q-cTKlىØ[qEXqޏrٍsy {N+riu&1@= NE +#HjExBEvT1b%TTSv~HEc@5WtE2uU*.G9)*vjDr)՛~ǔ7- +I)\!zH)|dx6LyX|8~jyUo&QiP&\,H t:<|^m"1˻7)ܢG[+ȿvl¡* R .Υ! } n|nj얫xˡ.A"U.@ʟJ".^5PM!mWtkQDRD4'i~R~3l]ZEZr-N +O. X^r/W"⼱5֭1vUd ^?i(ycWSPnr ` |e +94v^o:(=dPr.qJq9%my/ϠhVxj\;+ԧ\tbɚ.%O=)؊LYaE}IWYuݦ-]}[t%WI庖xKpJ]D#+/[?&X 25|xcmi-yaU_[jVK裿/Fەք-Nb;w5cepV96f5cԂW>8GUa;[qd+E(@(Ђ􁱋~8ꍅÒъ\Ĥt` (Mr*2l~>٧?HϞ|iᷦJv|HݍИ(Z3HO$^ HjQQICq*xX%T,ɟ;bY-`EW,:ߔ0 RiJ'tF7n#)K`_۴rn7`"RdVAP*le"M^m$FVTnqߖ*BpE%_+;2\jwX~RӸЩM⒵F`5(0(jscO5o4]*Tgslr/U׿S5V܈a*W0YCUi hiڮ)X0UBt+*UBFS~c],G(*'aj)"=e*t,TfޥLzmAx=>DQVF51uP%&]R%(ʵ4h|q&r [m5GQMYSmk;U)2ܭ+u(@[ +uveꢈ:;i?vݧ1#PGm +S3JˊRǙz_.Ӵ{{z|LjGY`ՉNC kl@R;jI-757PuhdS8Y0o7%檺Vw[HDmq.y8J}!}!)DU8BPs]#8vۚ6֌y%Җ?߮$䴖ݮ~yGŲ-b?ǔ"p -m+Rʝ+4,|Q ||ADDEFZ&AWLpA+!gA+!艁C2p-$10#Py*_!|Rs0׌rDn;"| 7PubpkԶf6Llh\MkjO>jj}4q5a<;en-5Bk* =.D?R)ʏGm{HQ~(?RPD_)G4 AY?]$9uchR4k{'a}cU( D +vW0To` HuǠW`p4z MqC{Ͽ7 a aWⴾ-חKk7>SzOսvFPsZPRXVu͸93ֹ4n2j[쑾:㺭ֽI;V9R}’;j8Z+~(VCI- !4S6g  :cs( S +\ k*Q!e V5|G}r{cQ h$6UPU!O`l^`A\-nu8|L}`^?uc,&o-y*5G8\|Ojq緃k깕M$>a6ܗ}oޫ&;u5~- S.w%$iiZcjwσCG935iuA8K5uUE6 Gq$xX2&4tPw. +}P̤JB|\+n@O':,Xtv@C*PiA:oHq:X2cfaV_:k'Yj_-C]\/ǻ2r#Zsq 5vo7w Qnw߷iZ"klBWmU9>uwJkf6pGÄ}.Vq99_@z85(o_zk|Zh-~_]MCUMt^EvܴJV5\q'mO=nfxՖ \9\7 cT7jsmOIzƨ$u^EE/7x1-H5ciNxe&Lp'ѴWΎ~' !Xb~ꬭ2,n-0e 3 +EM:cr_9ƍ3Kk.ȷ-CLw ];y7kG\Ӝf1Su;r~F7mPW]hwzw B:P +qHw-tV*w#qNGw7+.mYFDZ>Le3>iIU.o~ݷ݆ L:( +'_tג#PB6dG1 dtL`0׹o>aU&/ M5@A%ՉDHL(hC&Q(Ȉ&># +1`cUUV6$}$ab_OMVA~LB2*F 3%y}зݮF+J#sP~]ٓXAo+/Tǯ,lMлqL?S "ҺL2KYA]<Sꚁ,Ku<[J͆BWI~&O!1T7_3d( nI8ƪT15$pF[xAP"(2 9[!h% A=N-$ЩR!:n1'Akf%aIQk{r$]E[~SEW|-r-g!S#~xFxHBǻ,1&۲R- pEE@kH/".ilcll֊auvEZfk]h5AYJ~x׳ï3ßw7K );}|[5X}msu Dsk[}Y}W@-i˿]0ztܲMq!|=HX%Jѳ4}vvWyk\\t l:YSne6 g?:Մ6k+<& 5mwd /̖lSV/|kʰ!3c̏q0`BޥĆ5mGGe&Y%#㒓T)e\23=߹??=5Քj/573qn{1*xJոmͪ=M¤x{O{u/._!.cr؛Qq4KH)!rq`gDY\!A9K*{9,52y [럟ӋJֲnO>.&w8܃ϫ =ǓO:~Q6u mS TXo@czpUKG գrP6~yS ߸gVՄ0M$iiy%|I"7]y0=̐*އ 1΁~S-orhjxj*1auw&#ΫQS$wOPfzVI:G~mmе]G`kϏ֨R<*:Tr+.{R*'Q#ٺ`>܏{=qOܻ'9 AUrz7_}O?Zy?}%u}ۻ7~qՋS9s +8w`d_F##`i\.opUwKӿa9ۿ×O۟AK>8ïwwgo~(ʙ^~U/n|/NO⳻goz?v߾)ˇ-;@P`yk8hC5Aisw!=`JԔڭt&lj}T`9gOG . f@L1iTоc2*#f _OD$jCux&%U-s %{ƭL<|PiAr#(i% 2DFwhe.b& vxI@]]A`$0(3A0k[bPø,E~U{7i7? :0ء B \&/#'(c:𾘋pdN˃ J:8}]M!V;̈́ x6VqPWo oش!^1Ӷa )?p$:ey< +Ha"؏4 j6w#e)^|>vt./ +*0\"mAr8Ivk?Xgāg5?q.>RU<`b@"u0HOues9lJ)tmcnЃG*5m5؆Cpl/ F' `t؟?8mp:z OIbe!c@/hR3]a3(;/LHW.`pqh@p=#gj^o z#lb!YxQ:&A˅൰Mh.H8<-Pʁ/Ԁ{Pd^K+٤!+k*j}%݀f``i H<֓Dz#LRҘEv6C- g<&%H!@PW +|bxm(Z @M݃GIf\)Ns W[]"„cơz rILjW|Qcp_5h˾}/9XV.9;58ti[?dCcD ZEnxkxo ^D 0앺H`9 b<܉;b NƦG*#Qa._2CM@hV+jcmF1@)F Wa5P@g\ s)'ۇl]1 Gh@ghƴƴ0 Aĉ +ŌA3x9n/̀(j(ju0\Jt>3p-ʣ'g:c?a# + +_ip? xoij% +*щu2[*:ہQ]tc<$XlW.@#<%j[jRG A"9J*@B3W uG͐7Ң.dJ$UȻB%vr=CٺF#1V5QApFcDpFR' =2CdT4b5sf]Ćp`GdUHS$Fi9C\%EwJ}f1oe*08!`v` ( u$8Z~ DyzT1B} e!!hLܰ`^ehP4M4PZe!(եV 1 ˈ++3,yj'kK`ȗ5]I6;pQXNV.%ntSo:H{e/˅a;ꭚ Y&.Ʈ l&:MM4Y3Α A;6Dr0=ƴ͑eBwʥ4ADw|3*`7&Yϸ+u IdVpr6lLt [G} l96nch8C9n{Câ^+akkנ; ;Sl-sR2_Seܶ`DVk\[mf󃪕m8ߦ_a}T{:BWkkݤ9 @Uc;ˈ=|rGxߤb*C@}Vt̐{@4]R Q'\F5n:gyEw&B q%`F0?/T,J^I:U"g Z*  JCDQ`r@'WQ14Ruv?M QŒ'[X%^td015٠r^Gs>C֨HM)xѿZ :CDnoˤEYu_^O޵~#S],.;V{dhdT?<!RVcqzZ}$A^RHw8A^GZ| f)F\Cqhu/s)AwrENF:qc2{xjGO1 PMF&OO Whw\>W#j.~- N|;~p-ǾPm-X+ (-XޛafWQz +Y^-ΣϨ(hۦ&Q,DKG;{a Q$ѝԈ#DZ;n$d9-?fg+j:5{++rUOaVɇ62x@LvfIұItLb6D> +р3'U| U#k/.X!Oѹ&P&Q6#48K-*Z1> gEjҽ|{o@NG^@i +ͺWK/52r?LBs3]8_"m3Dܓrm8GYz)|UrWE}Z"vڃI'H#v0^>jl 6΃q ƋkRCgȜLgsHƧJ$N|ȁӪlRN̎>N-G7It,4fX a]8 +vn*V/?&)dcZF`豰OKRԴ\p꧋Ai) ݲj5j JZ~CcijHۗ5V!9_G֞FJm-)5-ƝfxH+(hc`[GME``L>m VCN:xCkjipJ? µDz!!mͲPN.$+;^n (4R$DCIBvKt4D§K$Z,E 5EY[u%GC %l1Sp&LpQE*qڷ=+P^ŝ8s&Kgt ޴ ^G7 7JAc+$)?쌄xϣuzIL;UZyӃsFPSAF#f^^hál .,HDǎű+ⴡzsK&]L~pQ )i.2^?jh2t/q` B@!'33M4Vj ҃Q-v*R%&܆15<n0^Ld?(1bL gMni8vC;o!L-wQ/@2 ;̃aezoS;>_Wv2J8(ǃi|Q-'"s"; +tLt 6!S݊N9gnQo~nr&fo@@ǜmbx)jl9VPԛjhM0=SGyxVjC1 q ɥ.؊$E0@%ShS$D d.'IY% MsG !7(3.]Uk<T1wՑNAk=fMUdsŏw_@#KOI5=S3nJf+YzRqP<IpucPH]a٩K9/:XV,&1~qF1::јf2mѤiqCL| s9׌3,3dWynwQSÀ]2*ʖ3}7 (ljN?BX;u=,2#\S:آps2w4N~rZá!n>.݆e:h% Ȧ;t4`-c*WOA. !fݎ!o68QkM/3aV8 +1An@ʘ<"4 Up%8^:h E sJbq;S+Ǫ4SKL-W7DU'}Zی4V[w[T]JdN#xpU>btE2A=OD!րG ;DvZ8NlsQbWOʅf(3A +w4#M- ^ zT  `uȺiN#=n{h&L& 'ѭK>y9nN&#m * Z%;-͞a$xniXvt/:?1]##LԢ[^䲄6Z]kNf1خ|F<:;#<ӃLZR7w|t GW`a}pJd |TV~`c@cHĕZz^J܀'_,~w3N+`$TrV8}~+=[/JƄA8dL`JFC%ǘ 't pDǘ 1& c01>v1@iv'| &c8}8dL qBhX8FE#FI0`>FCN/xq.o'Te1.X xq7a1A1qa#(>ECg+& bkG'qx؉ A,v &>1B*XLs?#<N8b>1>49;L<ڲt}fDcl zb}\6Dо3ZϋiC)`s+$GtV)O(j +-p08-mhŗHe?/]E9hɕ!BE:`N 0*.j{gy^Q57'k;o ;D^DN e^"u+-);&{CQiȜWKtfĉiVk+a(#/[U(rx17TT=BiZ1{'~o S:hF(aվ$:҈f2CE}4Ã.#PxLs5 D(CYaJ hL b\UZC&@3kA/A +} !UL]R3{/z61#y&#3wpY*CdFOE撼G/K?!p1>Lpxjw3V/3l1~[ ]!*@ZAwp0`B͢Q3VVu?!diʅI YX's"ؼpx{WK5%yQ2Xw'[t+ӷbip S}x{UK 4 Ϲ8{<Ӯ]ਖI|v0K/AN#S{SbWՎNAS>xT}r1n^+c.BtN dYpV`AZEN|紊]䤥v@^jϩPM}Pk\ +QKS"kmZjx$.$]'d%/2';_y;{@EKt.24y{ @׎E8#Z/c+ΙSKu%Z.R)ϩF}TkQKStɳɘ"eZ/@IF &E"$8X,u% \yB']^;4}|9d2%8w^]w^]y̯_?>?{bk/ͳWo_O߾߾nolHW|G9F?LVk/0eo^zƘw_ _~_ h|/B෫_/n^}Y\?8/?Oߢ +ec oU@%pP@~ +mO=U6&uG?o"Ɉ-fz{+w]gyOe)?{v\G$@6F@:aQ +K*9L ׋~:MFPZFyI"z4 TXgVåD ܌|z%~hlW^EccR)QD T\Fazt/WųPYl j [ kS=dT,gY⪵%',syR%Vyy|*U **>IrtGv'=:S8 +&((lէie/,&XjLE1fTT+HSx`bKzLԖ3*~Gvf*ظq3\&HnYA[*R*A + iQٌґW¢+{LxQZ$:0=ͨ`3%he(OMa_:b?>ʔDzdHꂑbRdj^V2%sWO d"x#T?kl }܁xG$Ӣ9́]H„X@|[tUyԖCQaz3krʔgYpfϋP<"$W5͉BÊӦ>dӚ +ECR kijgUrҌ6K+}:2*H`UMKG.˃,-R(-{ڂ2X^GqYY?E 8 I iey0}i"љ.4He d B0Hs躌!_cIbʱH +2i J/F[#B*eyW2 7X;-"9.`WS#L+&S:k\H$ϣb +G1TLz INAѱ}"ARH$0D65OC$͗VHTufPa^1d?`P`jb &h=3RT.)1QBI`\'e-ϽbRF Z[SJ RI5fm=A »qrړzP{f(MׂIN;'LyM> ƩZRE:0V|5*ĦAK +Hw/)->2mH7+;LX悏8hȖHJM2(~^I-}fg:}(IXABT ^}'^@JUypc-U6x=MhBZQKZ,6 :RNd::2ed:l@'/$\\yfpZ"i;//ܓRdɏMuR#YI\`f# F$%7*1%4GS  JxuUޚ R-2d֊$@^pãS >&9ϵYXlkޔS +Ƒ);hWtξA[oD5HP_!<1YDKz%%-yd%>eZ*rm^hf:ٛIjRɟZͱ|MpYqEN=p,s&+^_fL/CΌ/^_EWQ8^q^3;'n@((b@`!0"Q}H`ZMseفN,`LUv 틥ѷMx[=R -ؤn ʺ)J՟8Vh}y”L¤58JV :!E?+MUԲX In/Bpz/A!t&=Sbȱ>8Ԡ!XxE⦮gǀy<(wwu8tɤPOI +c|eq9ne +Ρ^p!@Q4%?SrIdH%7Uw))r֨sWKSRnZ +M,dV;nO 0 tY +T} ׇBYS[ޮNK:,əAx$1ױ"7&?x(=GA",tz-CJxbf_\ANN* +5tCo!'rd:ЈU,oK'ϷM!D`Fkc(M%!̸';e`1TRd5̋DI=;^ES+C'yITVV..VoU<جbK5R>l q[ +IZBHrjy֬-bL"c 7K+pw)N 7&}S_9&wDP$Dzm{T 8"HLT\ДRuiU5DWɮB98nߩ*7W䃦 +6jpV@o1Tc[Ͻ&yV@`m79Q9BHyMAU+ҝ)CrQFfqBT*8A6K7Vx pxswIsGI#eu+15K*zaN6dEUXJq*2Mfezfi6DInyK']"$\55'PGF +b3̴"V'l#!@(/f2sԚX 0 8ؒ17sEl@-TLٚAADᔬ)VU"Ml▮+uDK\C"v- +5D6*F6̈) ; RGCvY!AF L #9}qi Epi( P@WEVY*鑐iyTA55 !A=D +$;(WVlJ} +LWQkyدx9' Y|%:A :2"zsd6bWP ttN)%Xs]$ƱI U#+ tɊU8\EǠ>p G?@UZp3)Ŵ0S!S)UJ Udl'Wd5v́3X+N>K 1. dn_Wj@[d SgkE/*USڱ{$cέ̐ Q:IM5H*QX/ӞI@'tK +i5pq|Sl8 vS%-#gF\ ^C /㋌grY]Pa!2*7eZchӶ:VVn[bErI5ZЬ 6hm_^#/˜]kȱeI`e93A{s^&^#]Ge:jٍvh`V$iN b0oFgShD8B2L} ;3Oh!-m*i,z2V@rܫDvha;f3jn,Pv oƝWmVB6ݿ}VPH9Xs#G!w :؆IѸ '4FԎ?xH{`4W^sᔶ-:SxMuIu\|nreW {Yg IGéy,on]i}7'Ezm?̎gCqųk!cd +,vH(yfÅ:AMHۘAJnThbmUѺk0p48 #&z=X}ψ0"WHx8n^! +ZK. +"Mk> ݃hp ΚXh5Z \ة@~O cygVbi6j1'!C$O%,irb9u1%/)|S)p""*'GojS5﷣LX3GBBM;ZǩIP-|*PiqyeKS:6s%GYL2L(%n +O y1ߕշn ~2HDڂMb)bS,6 ia&s& $W9Tu11ţS. \5-@hS3*(c>3l.ƚKe婲2vµ㱀x7fEvYLY!:͸u]l*SWUI#s`Z~Q}茂gܵ՛~MRpHiWz9`vuJzf [vK|4xZ8 fr Qvւb;ReTk%Db?QU&9-S6xn<ڃƍ-!$4}c(w{?]UР4>؈!g ;>\_"px5,~81s{4yz o0TcM˺uk3Ao$ v3WT- qF>bsYpfԶzI`‹sh2n%P'JJN_ћz{F|SHuDf(#pl +x+dbӍ`LU23j4B{9\A8\ *lFuWMD Uq{_MR6- N- j{;0-ؙLi_1JU*X Մۂ-nIJ@94f~:^w>T2V0*pL@_қČO!}w%QWN/f5+PjD- E>GykI$U!ҟ +UCRFV;_KkQ݂a +m t0V%meBn Il7 y=ZDi'U.`4<[OM#ׄq̓.Q߯9 =Tr,kUp#7lki?sCX /#Ozͣƫ 2AwIHNX}Y}Ds,- oeZ9\(uGω@Nats V-L+cI0E Tnu{<)0ZO- :[ЛN(brgspVFo:F"QT  p5::}کn$YpXu +"t(yiO2*9lj<3WYP7(wlG"ws bR9_|lO,;T Ȫ"kǓ98_5,EH``hAf5G-kQ /UT+sW +lFÅWxE6(Jι6bW!1I(892p:lx} 8cp^[$p=N$ +((8*ް3_2;c'ʸ+CJJ0 ^Pv {`j@.PQ%qBV%bb-%Rx5ֽ9=p= A@>v׊{[L̷bed{Hh9Ka9u vgT*U皬# +P @KK+EP ZZ=IcSʠY6c1+;ʽ~i*=}WZPEbK$Z "by$ؖݪx̅.7;M?^CmvU՜Q߻(xwRO7bFe?{Y,](!:Ҝ3 27*M.xl]Ud?60z@1wKȑ5czX!j+Wp@' {ׁЯ5t_!>"(L[rȸu Ml;O T:/v^S ̴| &AYjJ&5ʴ!ep6y]c +6La9zlQ~FBh,zUmРv* +}ϒTug>yI|ۜN( 4&ȅ(3wГbф%H1H*PΚr2VBfMjqa^s6ƒ,Y*{56C:+r)nk.K^uD$(q=;ѐe/LZ@G]}_QyXA6fʟi*,J1}εz,᱀%4u\R̤'!Nh#N@s)W9/ɒ5U5{]klPYI-k0L)/ %[!@bK ,^" 0|s`ryϱ-.sA# cQ0U~No^IhѪ؛@5o|.2mDŽS^݋u"J/j}Z"wwQ{DWc6>Հs$B'%(y3c֩b^P`)#O^ctń]Xs$yIKh>V +"<Ӂ~CϐSi]~x G)>ۊ_ˢsK AP= oj 6эn_92hKvc0$ HX9$8:$"ېØ8ķJ#Z~7.qbV^{8CfSL}4ERuc +==pT"eNU \2S3&PLOP +A9ژϼ<2Y2z](]g6D6r>n4zN|ѐp3b<"h,bͦa]`$Jv}syEk T/}|6]:,xq8m*(_5ekə'gj?B6P cFS:Nzhc+!SMdsJ%`W((K)y|E}VaNnjem%ѩ>]Abڕwp7WjvoTao怎*ȐYP`Ie^%0 ]9PQJm=P"_hf7iGb KP9k } `u\)RN6Z+h̖O) zKT bњ5ˠ"o#8{MKKh8 Ul s%u*Tu5$:dmNU}"R7j!0b6nRc~`RWiL㙣۱t9^;MYT=o"j͗Dc$ů=4c\ۉUa7SS>L%]Cke$+X]+CloKx|I;6^TT +?z Yi $Z-:C#iUjR fJCsUlSFL`r}^Gm@Q6:1^E] 7Us'v_th++D\Kj6V&(T6D%쑈Z"|drQjRί'r3dÜ'z=B *sWXAM@[Ԁ&ZsF7|MáX]ER~_4"&e=UG5D!딝6VПCRJM;EԤܐ*#wf-_㸲̳~6\qV-r$]jdOkQjaޮ[@avӆbU*H5o֫(dnsA'ch8*DV_!No,8Z|nz43 j4&qT<{msY=Mo~2_)J|ZZ@o^7өf)1wcYR>-NMPFCȝ; "OC|> +XmQ鈯hX-f\ t6ym=[5eҘy^g㨔sq-EBB`ZtoUWH,Tn{UA[%{=6v;"5l05xCFE:GAB|Wdb].s2Y 5+#)"|ogAB8U52ߵjՙ˔'42"yFySjᗿ4>{DJ)j1#7=.l&:4EP*RG8nN^HqԗeGݗcGutB޷?H-^Pdc:7`ԩǪ?@ NZQE|sH>!y˚G rbDִG@+w䧮CJ4Db\).}ijR<#om^>`0u #ߏ G{N83guH:n&T': ﭵE؁h@7ʍyL +jȲ꼅Hs陎s + '\K%3jūE"XNEQW4a/4TCj6r((33SKK(A^KSU\t+Ʉ;p9OU%@7 +坭arqXݬXWFЏ\ -]˕9p;UWY9tGqs-%/>uGng V陂1C9f L +. + 3Ǐ)#<&Q{(OAaL:\E~Tbq9&贓cST~E ܼo +OUϢosm{#vvO[ +[, 8tFh˭}$iJCwiJ=O 2n@JqˏH>;,c< ,F;av%HA=nNωԜCXwavhEpAym{ep c@ߎe&Rn?dMQ?E]L 쇩.~M>aՓfZ6\HQlYM@LWz^cW!|{(s[;2'9>lq+?J4r䧗|ط<v9 *>kK}GfGcdCf˳=G^yuq'SPz}|>zktjf9j%c >w荨_y^Їd|=y_/*e9@j1kB- r>Vj};cִuq:3\͙p>3ݚI<0c>\|3ky2IӊR!7?F~ˬ8x-e$xYl25秩21d\RK7Rq:' +c1B/d"7l=2:!! y}<^9yW~c5$g,cLߘ~/[c>!,762Ooaqe"btEЧxa/pxp }?m'WY _y+PCvBώ;a<е;a"FSR4~ 3cAi'[Dv? |CX -3jg_#,?oL>\7޿Ҿ-/A闆e{9ʷ/ du}ڛcVq  Rg'_o{AӾ1z)3~cc|ʎeKμ|2GdՆ8X? c<^>Qs4|x no|x]rF)) y}XWDTI/?^(u~6`A[!oTםBN=*!oԵdIL y>W܍ bdˏW$P.oc^n㎪~_ƼL!d*iC\}(xH\0U& } +Gf+ۘ1/pUۙ#ym'uէ3l,jlrE/k9(E/+ɱ5޽z&c I׬]M3rsyzҚT!W+zO]'l^ym^l^?K絔U/በЋJWC5˪W,#T9İqiU"?{(Ak:6ܱ9>|h%d{<-qm^3('7fA;˽7TR(lOt$&җyo~+3w8 󈿮kBGW + q]|ip IupsuU ]*Bwmt^91>|NǕsTq|/l?f|}|_wrb~$P_/۾ֳ?p(K#}X:ݤ 08G?|KQ=/O!vw~n4,.<}٬wyJ$g-/qЛ:q<|y@7>5A0>mu0}{"%mH۽k>{M"sv$Rg {oWSoy,b|BTN۫XB0o?2덷! ۻk[rqirf^4|={E^}wHm fMO^. Xru+2%ìWl +@זk@˚wrJo}5D?rmS ߭xtOmxN8w}!ak Y6>W%m}ﲭt196-Jƹe}G ]Ϧ/ezm[LY>\p{6oiu;r* /ڍ l/֟6w_˗LLOkBn[ RzԎ$Jmy +O޴"*,h>,KZv=|]&-KڲJJR3O=E *6m${b[ +p߻MafHo侭`ȝԢFG*%m +NJyS}wrumkY^M˙pZ6nm&3]kaӪl rڳ*rnicyY}b^OUATu*+٫?\Uëi:ݑcYڴ0yNUWqgQ5шԫkBKlQqE */tx?F(Xz5w)]{ɖ:BE輻kr^nb;Ϗvjhj*yib*$=)E;򞶛9w7S[)e +c>Nֱegj7kOcS|E ocfߓVg*R_ѝlHiC%nۚNza5p\vZuٌ q5em "z"iٙru{)寮sl؅нLK-܅3l J^&ۤ#k +`ai3 .'\^mj٧iZꉪ[hs,1*6TE=WY Uj@7R@3)v0)!T>sPXIouRf|w\QQBuRcb+Hp{Yp!\N݊]zv\}C{ŰJr$S# &Dxjf eeڢ: @.@ c'uawO`XY.^P/ O%RΝXV0-?;,!JI)seӗE)"w"eF˅%o7=7]V6*~8lmk&cM>dl4+P-K̚gLYK޾#@#smٖG&TukLf9rĄ J&*-Z{mK$ꅖ41.r)]\ڒ{Ob.l-vČed"h{n +&lJHblvp~ +,Ji)!pu`m/ʻ|YP:v.|bQ$fOD=$4/.lSɻPzxIR*V_dLVn +!."RXHh;fZX>&\LԘ.ِH)Fz>$iՖ;+faMk?6ǴqtuH0y;vG13$؇cW.__@ H9'|=Qa;xW +}};~}0vzvòsVZw^QZmh~:ZĭOQ֑mGdI R4O{Gcrut4s 6_fXY˦DwIpLVYZC4|wsIyQ *nm7G׵]nn޷# jts゚n&ؚ}9 +59l3GWK 'MM ./ i`Y2r l KFNB>By9@w7FV$Ji(Viz5EIA\ +YOGEu%DQgOi8ꉣD *mGhwB+2os ":"6! 72׈~EdzH݂WHkѻkӽ1w=*&SJWcҺ)mF0ۼϵcYaި^:h<˽1]HW/F8ψ捧K05lIa7"dy捚x('㺷w7B)<4oEwF>u۳Tɻh_6 C@GiHv .I,F杮q,x7^z\(MP] SGa:-Hb{ew.curn_FAa;%H 8HN?Fe s1>WݏQH,R=_ƌ"V`9@?'ah:uH|$0%]T%$^~ctr~'ק +y/;F>oz[vn?Fbr'p#mhnc)ط Gknz> BZ> rZڛC#ܠD/?:42G}3*ʨ *0ocF9&]d:QH}:3l !f0 TȐRttf,TʡViH'V-?<OF{Cɬ)*d"8w{fҝ| =5kT[ު|YL@{ S(*1&__FgXttm_mHxAۯeHC'SgraW%] _?7qDL7sʁ~7qEl(5!üؒ|b˼Qe̤&H`}7Zp ۼQq%@J%MF<\A'ϸlVևwr˗WnѢxpdm>AfqL xnE*"V>\HH-S +%§*r?Kky(;_{Nj%b<1k^IWϧܒ]nrYͶL3޽'G ÐO8? YbuYvpP{POu=@ tzK-$W۾n=B꧝[,ԗԵ,옞m#joz#[dthgeK|G/?6ݤ6lbs}2]c:VCx{sYk r>M&P eO~7RCl ,:;T{B跾ȘiJ|ZAvD_VhJp@#o#mwtl.gT.enG 綧وJ%YtY2.SWR[!qd/EI$cj=o1 ءVĠ/19 ,Wto3 xx:rrV!8L)ela`LP֥hgywp)T +K]RL6_*!qmt$:/S@?$5mQ#tx? n¶'Z_\X)ʝN::<,.fy+ҶWF[$Fև +˲Z?ܴևDexauQ?@/ñrd/`wvX}j1>\ (:ͯP̯T|!kc..ed@WK'#Օc^6V[^UUڣ_Uϧg5lUm*ҵ=,ԲDf+b uSK!xN]WOM]c#x𩜠oR~P8oR Qj0{Fi e%q:=m'cܡ~L/KmuѧӴ0rz'([Ymeⱽ۷{O}蟬JME l0h-z۾LN Sg+[}/Iκo<DjIOK}bJ] ;~(p ?YQi߶InnI$ax#Ze=MϷ% bR?*,׷Qcew$rTԃesy,{kG|X+&+q8Z];"! +9 #MDNG\ATRD|92g~4PI!*had+ek }Ƅ7:~&BTZO!$k,{BvS^neOMD1@=@5$Fc먯R `7܊;" Y oL#i{6߳S+@D#9fjgd*x:="J- +3t6*Br$,dTд0=BLN=Nfv*%z07GuT $^a{N>XR% ^IOk/P[ԩ☶z*A+ &.xW LS +̀xQ#pޙǠ]OOꤶөqeɛ&Aǚ N(NP-ceknW; n9)QM![<WG^o' F7uVؾU[yK!nDJ=@p謼!z̛MEPL[7jQNݚH6J`+yċ.boI_~'BAɞ98 +FS{ +ЪSFP؅gPsɛ@iL3D?Bڸ9R O~%DA y\P9"`Rv)0Z IyK|~X ^`Unq + 2ia>!N?-(K)?x%ңn5`Z tjt`@J@^<  1{`^r;'n:cZ:TmcB:Wױ2%xOu(=U +nAy:5IŁ7U#.LР?E_n9cK|! IA= qQ/"Oɱd`c`CT1z !FW4~R=NJu*Vj ]Ԫ/W +L*"B8nz+'Bhn<<,/^\i;Bs +;Tֹ9nḠn 4t +mKe\OG-B^LގW tx&Ա!Te`SM&e,|90aVƂډЋ,=hvG5*Y$HH2*W7g$yo.uL,}8čvJnGj `1L-#'2Ym=z+803 $36)PG=OI@U©؄$:U yR@/i;xh`wkϮP7њꡤ"[zTYKoxk/`dz7R#re[ZA(JRtsy}^YhUb'OxN"Z ,,'PǟȜCv bAh+Ȅ;UBicV]CnUde>I9hvf(W>^ p!bɾ`QD*-+oX맸 A|g.&A23 +/kmt[j^G/B`jD&[:=Fq(~CfqIq/08TAAPPwqs + +oXqkes-lŹfɇVFLJRcDd`O}}JPC052$}zX^|C0@wq6w,Ԕ* 4{Ns [U Ļ>VB /G%wFs[B!0|z;NtZ8-N.}mxyZ׹O23b=qsƬӂOq"/wf3آPhG ;ܒ($+&^IugBvN*x{ܺ(.2ch9ub\L10@k +.X|!݈]9N =Gi[M9Zr8L˟ڇ6$`GC2M /IqQi86;#WzRl†8+i$dP1acے8;ecIhǎm4:I+].{hY4z9GsEQkwRnʾp@WRd+9 Rf5$ F'FDQ%8N:95pxNHk5#L-n杗)y`4A QT-p+(x +z@6;䪞 @S%|Gq dXS3QLl#V:]hV2Luu>L)G0#zHA"Qѷ);7mfߐ8 O Y*LSs&rID-yrΘ]0]܁Wշ+k{UxԘAcIƼePxU2v+v+/>,PYT{L!:8~8In3(54 +0VjwQb fy 0AyDŽOQw\KJ0:!zqEe ;FL*1;s(iCܛthꭄ{ +A.s<\wRX͹ j'Q0Gl*(l0@a*G5{~`_*Po//Ciwʛ]Pg'^o N})وZι41Zh-Z]FUzύظTO6De:Q/|wUSiۥ]4'ٓB@S!Yeuq@NoWSJ@O!#kcD 5&}uf!U4E܋#8b{In~G|q&M0*W(3U=^?;NZqV͙׉9 R-?YP~3qM(0BV$o'Ʌ7C;bl1&0IOfp>0"K7v$NlrasVXCYF}OM;LhG * +wpL?P TFD3,9A~+!лPc'"7`k<ڐ߮Í |LA2ēfhՉ!cbG{ܱfkCЩ7w:]j[\=jkV݈?S[B8QnU3qS:ff20rb7X$Pn J$kup'b#ZT-'5kAռT=/lQUgt z5GHmq:IttF%b[843F1;aO9*X\őljԙs9=s)bWNbCI2HR"|uSژ7ɏud67垿*^ư-1L&_ JxO^Ap!cKۑQ1`q82,%S8]=B1u6#DVn{!%5֛K$Wvt e*P1&k(;2UZ|mzBQ`!0P?k%Fȼ6)+Zb!F^꾕n41rh԰vFO7+vE pkc;F74c=JKiK~쬾{5eD݀1f@\QPͻ:NHkߥH_Ԅln d4~%(Oܟ01XMc4H4]ŧˋ?"ZgJ*O\w ^6pvS˴g=R=,f: T Ǐ'@knH?Og`vu#ܥ!h7#r.Cc #K5!T03Bd_ t2i5vȸDd-^-gYd>qOΩ\u;;0e$R 6nypW&5\x*ʓ4PA0@@aj-8xFoo?$?cJƣ,F*)q;=gjm +_\gC#^}v:$vj[Ib۱&^/ +xG\59Tu9DLr h7[94nu@bo8az& d=cRӣՠ9 .5g?{Ď,C7%>_˃͇6<}Qpj.2Ioh#$Uΐ4Fti"l@{B늙B[:쑢NjrI%(NK y8호Jh]lNgn$@I@֢\0GQelf6O]q5ԚT 9>9(U$l/u\YHy+ևIɾ)=icJs# +ّHR2{פbεlyQ|܁NC``@gں`V Ԉk.o{. Y``cHDW_q6n1k(9 +&PA;L Aχ*w@H-0jH%sdZH:C&4B܎HЇ س sPRtyX)BqRHt [M0h0Q$œf i-43F |nu`Bj{I UaDWxÖ\<"QJ OyV2jKDžK,<邲!ڵ'[jdU4:aD`D,j}T-@vo'yp8,P!) ;7Jn$r;g_IPcd+PEVD؇BrK:ҭF9b>g3,""(:_MckS\Kv;O1atnIyN)U|?G(2MAi՜H1rgD%֡;oMmL1m_fp 1ؕ)g&i^3SPL0B %S Qݾ}* 3FwH3B@RSpֽ&%޾;nm'lعyD0W +ªj{r5Hp *~5 :3˦05b&U֏DMw x]`;fA3;ts\k5YN6pnmTmHl E4Z+q0^ly vqp ؁\tK_<zn\K9J kcŢ2snOU1tx 5QNp  +T`)LgbhyO2t7@Y:BS+ExAi"FSdb_f>1^dٌGk,dC5OI5UK.%K!܅SoX!c:E$ ʪ`I=d WLYy?Oo&ͤl ~x%+O.𡦐pÚ_3K;h'cQp8u")v>! *)* + +aP+f;r(t +ѿ3\IеR]SJ A9gKfQ!-Ý=7(}KB%F e.gp`=b }RT >O%\{KmЍ}+x(y nYI<ʒ2;Lw Vb;,6ȝ7)Bd 0BCJrciRT Uy*,6%~+Go(.@xn)嫗Wq.x 52Cn#4Ms +mH!& +ۆrRţbʜL Qkjď>C"LVMCy|F q%%GgBE/ dNcl@.vX|CK"^3qhAEZjDZ zBzQ[➒Z29c +3$4FC4&jڍR%kc;u +x^t?fBO5gL΍^% c(bqCxq3 =HBz3hC4|q ȋ,k vss w`kkp3a k!.D# LEcL$Gl2 }@cz@: jBwA $ыԓWVfؗd$o\DN!s tl؉Io/R$/# +z9#a#IHFI?SW0T+&Ml` +jCsBD>"Y>!OIΛQw;r3ߏ7ӸۗV!`X}Cf>*V/M +)EJsiFɽ0P !Q|B.{-gX +=4dDp~;87ɫF aؽz麆eOOSRB?ҵñnsvEv8D]2Y[5ړk3@Ɯ6B^׋ε=sq?y{} Eb o$]s_V/F +D%r6CZ(NpdA7V*d u/_{#-2_ QPG`˗_| +Npy7ok4t%rר$SzQS~{M; F&81$v?[vUv2삱۰)AQpŵZJ{Č$Va倪< nJZB3=vT8 3liLJ238E=֬ L*(]~ ݜ[K=m  +W/^yy6f1׸#"zu/ +#JxsT~{0ɗ(zN."X/¸k@גS\شoV7*L@/,c{UO+S-;4JoJ^ >RbC7/D{WS c+if+&viF +e +Nҍ(kX#+^f.mRO,HxEPP -jvN٣QRqOGTSX^<pwɓI[e#1-MӃX]\BWE<"%T$A<[ \y{(DgAo>zb*Dg)#x-9ll,KǔZI2UTGu"1UփnÄEi"I\ChʽeYsQ8 " BQ@id\4]kY^O\ʴL5{Y+1$RAP-b{I9ʮa|Eni/*+Ľ;-g!+j]\N"$&fD&HTCj2u-Au/^qO_PNŚrA$;6i7-ua-T t 1b +=4 n=ݺu&+1l6rockI)̠ĥH1HjQx&).cjZC4#eKA$)a[ĦLƸKQ^ˋ*\BLU'kQ7@z#no*0Mt|Kf!zPl S!gt.zv=[/:уwDt6Kɕ'N"LazG`d#PkFfP@ oP*yK16a#$.zM8 +`$*6w2Nm#\攍^i "@ 6C6Ak6 +~iPA ѷ֋+wd{QR)@@sHEkaX<+ɶhF#n+V$KR[^o*%Ǥ-L^Z{n®fPR0P-A,xV=2/ᘒqcm1^=sE!mT . < +rNӀOX9[;BLTtb c7T(?gAk Æ9s7=/X.륡ШMzנ+!cݫ΋F(Z+t\,Yt^-۹ %D3]T͑y(|-n@b"~ vhv#=u7;v+tg'5IK32H!udiu3nbպpV䧒[@&JmSҭLH}e7>Ie!VTBNaB--:xZ^7p1Xu4"ҍ7_v}RS @⻃c ұuNLnz*R rp'jŲpqx.jӦiMTv؁Dif bXB&ـe{r3D |ق g`h4^YZ^<3wzz$)S# WˉP +dp ՛7S  e%LJHŰ5_܄0Zf~~Z6]M)CO7bRCCmh1}I_Y񵼬&UnB1WZ8Gш9ڈ":Ý]b; X8u<BG 􂬁S\lzh +)d0NX5]Ns_qp."8YHLHr̯x$xkx.ZRcJ +ѤG~ӸSI-ZXyE'5~jϗrPd袬$eud,RP֑>lO +$ .Bl$M1% %&Vx6談,etRį_M#zEfc 6!G`: +z[@2?r,TTݴ]$eQ80 &Ⱥ`FShJ,5!,5 hC:H%gE*{khcl#/_=}5Y"'4KWj4lBգB3#^S-J͊)U ڎxV ʼnRzB5b4ɓO_xBEyRc =␘d `캝X=ǵ-dI6ven {n5mp0veN8.0u1λPb @rt7:#zWѺƾƵvڻU(_F(CrHk/KGܤdỲ]P")lJp"],SE`_Qan&u#IV)_xXHv8#H]1{ rh%DC {b.Z|B9>gQMcϪ)˥,IlQ )>)n-"c!Us8݄QÞuSp&(>śB1TU? #gl[pWBS@ddmRE da kPd];:.G>)[r|G8=>S5JئSNWX}L!([~_\ +J@m)_IxF2A-Zmw6b +v[nx94+LnB*]D#fݝ^%+ސźԦRq py.$oMU1aXt{o%ѝ1u6u!e4Vᛃ`_, sd;;g @LgBE)]3VΚs H!^G=G0A\͡6A!/w +Yݪ&Kf$@jUB%[ +7-5}/%ڨ(y ,K$ FF#rIBa)?%t* e>ӡsf?gM5b. @âs7Dɜ.uNvJ-MqJkUɁ+ +2( +;uu0"a&.#:۩CCSZ)xa}:U/6TἚAז}@0lǫȴCRbNLX\kem#pnf]݌,02={05q1p(Oƍtt|hX/1 cȅu [|,;45[H4 GbAW fڅHʶ{ += )K9Yur3ȫs/re?{[XzكeLNbG<9xrbt c]Fq[7,ᙰ&@ɪ# T`̐ թ9沮 PK6r@¿"$:0Rd}ϞhC%6T&Vi'c?7=A2 g22{=y|d>/-{y\P'sd;YTd^5M}}g`ʘ ++!َ^д 0TaCբȘ "AuVf\TPp/]~T?z~rgŏ?~|w_}"zǿů~O~~ٟ[^BbzӋ0UU'1Тϼ`di[-wJ21%D):7H#GJe\)nOCGĢb6@SISE_B 5g/KtNT $t* gt^E? i;wSo:Y:ziWO 7Pao-[nrOCamDb4!]얋P`޷^G*;ҾYgAZD7)XPoMs(2-ڧG7 +3*xAofQu 3nNd:O":L'&(ADz+f H:ZVD%8n)ʴ.GO[%`bTcyM>#%cR[zWn:b +endstream endobj 43 0 obj <>stream +P gf O:MLG GBs'@+]~(K o9J%廗Bjg+^awإWzŀ^ǹD$]ق)EUjRioI@Y80m& +I@\Z 3%K@b; ۨ6ŅEm96ZCuv{6xhJ$,'n%R[sIvԘsݪrYZ_!P?Ӱ kK[Z\:8HW92ev7xCa4-tuy6-]fk ʮދ46H4ۿ[(t#rMx|dFQa"4}|ɭqxPBkАc*52ZxR_Q=nVY`Czk۫& zH/b(^ɮ$X~_ryڮFq& Zݼ% +'N VWWʏHw + |%@}@XˊϹJVy 7%nS7څD={b(+Yq㭽J.)6- ȿ]ku8kbSjcǺԪût|VuRihQ@L.gqT)ӋԦb{[[X[m1GX1+5BCjfYqa^ڱʽ0hVE\7" WhH*5qVESx`X7K Z2ڕz +W@!S0Cm=%fK`=PFLS:vlěDac^: 4f%f+A",|QEꑃh$Lu`/C9w`4gEG\T4RB%f^K*y{{\R,`]t2 (ץ(%ҽ l [`mٻNp|Mg߁Y +%Um0[U}2^?B d?ˁVpvKU[2n!?kr- s`#UM8~~3"^q%b\ + +]UD1A7L+nPuh&XקyYXEh9v*Gbkʼnހ UPUjf2&Yy 4kFz  'pi=ލ,L4O^F +e v#5xk6JSL s6pD{0L@b+M|-TݏN>Gk/@H^pӐ>Ҟw"́(,rbqf߽~ޝ5i+[$@A?;JVaBCᖴb~3\Pļ^Ф3PIdl8B i)l|gپI +بER^Z+hM@5l:wi21R] +襪Avo\2p=.iȮ-L"Ir]?Ъ+c稩8" }-ֺH" 99$A-C3=C;ҶX-(ڻ\7tf;>0G 9B(Frm@{IwkytM鄋_խS˜uV8V(x"Wu۔l~/$ԅ@~X i )9g?nz%:!h#̥?
 ݞ'>ge2[+qWUe4{iTj5A#EފV5L~(dL)[AW\bT|)9a I?p|O,&JSqx 6}'j]i!$pwa$' k0e..[7v{VeuuQIir$e>ݢւtT)tNh.Ǔw"mLMCGgdGQ% g nfdT?RvUCN@7Qvcьsvoem6*)I3K5S|bMЕ72ԞdlȾ~"|_Xֿ A>33ƹ@yG5Y ky5vD*zˆ2H96͎$Qi ԯ{LpD&g/k2a:QbohbiV*NE̯pQb +[QxpaTgZf뎭>" +:B w+p;oM@k~i]UQ0XS@l k- l/R +zmZBa*8Eu$ +eǤ1eQD$%S +Ji'(3^ UuDEĮ?2sK^l'#KZor&M`S/uBߴh 0[fCÏw#n#@ITcE©ឨu_UrL0νpT;dp Qq6gw@"CɲrsadY n"*pR(b"G +7DY y펳N8Ēc¬W&G43Iu7A97W/sZj}'L'? +xdI'?++C\CޤTQOKLs6K8VW߆DT@ O]b>US!@|CH|I,*kfuـSdS-2E1uhoSOY.O)Y-7y1}QSEp=y{fJ 7)eBD`}<O\U |CȎ5Qw`:t [(*Ǔ]۫$^t$0%zI!#5 xXoZ^A +DeO/{PthW +o^@Ѝ),_NN@-AaF2'UMKI۲B\:ƝM-H=JOUkY{GPEw3&'ZTK戗,(V=[,Yrlm.J& ;7UdqA)dӚ oR7%\E"KBC*Zrf%erbmx<2.Cq.&0fb r:#I@. + IcHMVxk&8S-˫-&#MӊEgb/ OfܣKa0ŗPp~k!t)rSnrdCƥJk %(E@P-ulPE+ vmLlCeB(I,uzXjvIcw/0oTײ6ڪTA۞աU_ oՉ9(Trۚ1.媩B*xZ3L0$Drt P5P5x 9#ǽ$Ra wS|V=_in!^B }URKj)(!ݶnr[Og/P^b-7 r[K^h:C֪-vգ!8'Ǥs Yzt% +CT&$ץe"5W#U(9AE P2p-'ݛу NtqbpQkOֱ8 I""[ąW86f$HފJeeb6 a6p>*,8E?5ОNetk't +nӶ4#o hUFSۺǣ5YH4oh-x{N|^&YYjhXKlBْ+}36zJZTB(,I-q6DXig4;-(GY#JuV1 0$D*n$[lF3!wb͛qm(3u>p CQAkWY9΍#xU*IEmxl12F!5HV^qMKV8)rjJ\ 4j6g ؓKz16z<ƨWf^r"Iq;rܲA][@,Jx=K45>Koy)T\yd}췣u;ѭLW4}V9- i}f.D ,opŕljS%^m3+U<7iG}z3j٥Wل%# \ އ[٘[ȡH~/kBxc$' ]{u{fu JZ +2* 3CB%6 d#{ a}#JoyA,B0:sr+ "GPҔ= ҉dKKt\4Nr Li`ie(WGiz!+fA;VAu%<-v6MRJALvWހp*=φĢ!$zU&dTBmF1Un(.t0ؑ5񬉽x (&K(o_-F. !Z$8w\J/Jc(cQ_0YS +ߛ)Ãl.hCvJ} դRDv]6h~Pn>M%1*qYz\+^׻&8-i/pϿ` b$j +}Ѝ` +wTz4T Bi;z {p,@R( $H`Px~񔂋J<]QAd^a̷"蠭Bbw}*9{cg֌=\WٽW5;ݼO(ˏWlk B>G )yT/Ȭ#{1BuH)Gf$hۀKO/?kƜ-)s#B&At1u\j9|- + ={]c:Az)4|ب_ŻQ#YMyx}"HL_#6ڞa8e3X "ZD1}c6*fuGʱJ.aHiŮGMAbe8T} f¨v؄KR KSIg{:%yf2۾:螹MAPzN `%)C W[$_F_I.#e6؟hdC+b + ͐/HUqak" {@RMڬrzkezPP>j'5,oWLdQ1mӬ>/;p+0抓p[U7zYOau+bM]ҡzԚ+nL^E>4Mvb̰Zu5(o pkw{٤?!Ƞ6!הc̬kR~PBү +<`qkRN76Mw GafMڹaM$ u = ԮDvo+ǜ5ʥaߗ7Rݗ0d+ +tmH.1|(c)OMIXdUNP_Za9 bmq@v \k#H9ZPFYXuE'|oߒ+rBfRP6f휹; +FS)ZZ#9''F:9: E)VIPYvhFو} T:dS UfUd(Q6o; htrB kdm=x LS6"s( UY%e뉈N,Ikg=\Z +>g(AFX3@38F)z{}'FnL >`Ojv#E# Fey dnwFA@ +Ko)!ޒ9?p[qvΟ0BR2+Ufeo*b7#nW4 &%Pm +bH0@ɍ#@q"PIE$n3D?Dܽ# )uumٰšk@@%fI΂x]z!=,O#񆀂> )ǒ.rCQu}~ r`Ẏ( P+3'ѦOrdZ,~'7_i_Y+p@SX 6ArUm(|` %KQ}(gXN2Tī)$00yP vqYZ dԷ` +ϒ6i]MR٪~0,0Ƚs(|0P*& t(YoX^61頒 17+Pv2fHnI3Ѷ݆H yQ%s*z؛HOů +^'Pr{f&*f(& R9] \ DzY_ن1~OPwMy`M(LKՃD}!QZ -5#GJ(`uZÚwhȇkلڥT kHO[P;nRfQ>X4+Cc_TPia5_XZI簗ŎP!%!:?$Y_/ٺ +8< 2pzzفAY㍇=7Pgr˴@gbNUBZptyA![̚$^m{pXB.JKD VU݀Om\ņZ[޻<$zd6[c`j+  +ee͘鮒w_64M\G#Ӷ'vZZ^j-Pu8D2Y%2+@RNdi83?'/C-mPM,kے9骜Rsk6Sp&Q/c#%.\AOokt=̑ĩ=%޹ަ"pzL|ZwA:Naf YIZ($Zjiqr.[_m;ӮiyVYB깨e":w U@M@ %۠ D[҂`4g60ȚB5@7u9ib6YSBɛ0#\KM*3R][`Y89V-!>X Ti}kli,ؚ݇E!T6n&Q +;I+`}"tDhRHQf=K_~DxzNo9F֘)zFM.]{kHUJJHNNRQSx,O8?Vv>vˮ3!e@b#~zN6.Q\UmM??܃-rʑ=wڡδGlMtıOʂ,&K&g-N0S|UfSnK$z%;iVJ5OZw3k WpG=\;x9Utƕ I +*zD(Â- 6+lr1J~#TT6(  D캊6\Ep +QTb1U$=}^>7fpZZ W.ɝ4XZ\d6u Aii)YDk6Gd5x}9G k%MHS!7D.Qd.ukLC@s`89"V.<6ۋ1Ǣ:Pdr[WTzT8~wX̤iC4M๑, MF e+k4 JJ@HZӔ 90Fm5վj_݊s >@L(-AD@j +t=DH)']QkVX1v7Hy,/3AIol5zAǿ(9"~ʟ:Y)-BE"{&ݡ,C_"vhnoȱՔ3!=X%eݢ*Ug{ q6D%l7#H$Zj᎝1[ ~E]yg +Qr XaǮN* T'ȁGJPeM(̖*,z3*BQtbKnv(o>E`eC3隤f(0{ +P^=3pZkq2# IKحlZ}YbN R-@$&IN2H]zl{ J"hO#d@y}`Y' i[OhoI&O1wIh]B,5H}Nr84 +V.(HNݨ.34OÁ|2TS5 7,t 3CäC p1%-36:r}4DrլF Q3\5^7訉8M-OAjH/mܦHZ[pֵ7ZPpVjY͕c3_xrtSe}{>\MY=vgKDNLoc~i61]Y$ t(^vbB"dE$%SczƢyS\p>d !r}l +hw}Ehd(AsC.%l(vDt ×kjlցu#V 0Ɓ7PC\"%@I1^_?ƴq0P9WZl7qpm?ҏ p,9ʆ4"8i!>`D> D׻xwd Y1_`6GL`wSUVxSSph@IW hlvS21n[ZHIo4YvDDQYgC-lk:w]C#1%!.J7>r钇Lfzc&hT0Q"%T `M8ƒݥ?hVwa)4QWQjD1n VdM3Xv5bD*1o/d#o9*AlB=55A;gw1Z MmN㚂b֣䟝quGR,կ"F k!2ܠ:FS ,q"M0:?ٕeCwI5قN'yi '[c|uc\ƢR:Z&,aٜ+6Z٤T V, +(պ>=ê~YQA0InDlQf=Fֲ+]:C,tY lj7wo2lirmmp|9A%6 ]k_Eh< Z2-xP̃t-lTV2<XWr=ӫEC<# :;=T[uV1)Z3+@g.P;FR[~Yo֏P{?|R!S$@٘tTP*o!zE릥8T>4me=b8J0,;dB`fQuD>^?37b)l؄N9蓄t[[ͨH|nw 㒵p;2!KHG:v+Nw*vU!+ː"n$sڨPɒwxTѵNWBnĻUD d]%?U +TURo5y]1oL#)`tRVI}BbR$l^Vg\GF& {?c&Uqг>hNbcNQ֘iav05lt + ;pGIoJY#U.Q%-jq ~ k$'`S9: e访8<]0oƈ7cM2s7ힰ~@+?8L 0,4 ׇV/%t{W؋vk,"~-Ĺ/agzv[fzv\DR;H{CijQK WQ,oNV"Pg"JoɅKjŇ72r՟!FC /TxhFc^g:+5oZjz*{|n'KoW?/o?_wͷ?oͷ|?}׿w?ozٟ_7?p?N/??/+?o?+ۯo^~:_q{oopo{sszQF}Տ׿ۿͯh?]/}ϢJ_ݿ?o~wz_\o/J6]oۯw'?6 57OzӞQayڿy߾Sܛy?O?拳P1NJ \tT5[МnJ a3ǒ@~"#IOF ˫V^:]// +ɐE#jTD@WBnej'.C'C0۩B)]77b?:9A0~4[{|nLP[w_F0}7>V!@ϣ1E{7wDW԰ r4 ~))x[8 CsB'[i +Pؤ\C}ۑEϚֱWo1~Z q+fa78j\T}=qO O>ci6F#nLg+YnO~E#h?H&hD>'0qc6ߏؒhGwa<^˄!{ޥۨ{asq{۠r~ORq6= WO4 gVv[ƝlEDDkk4g/Oa]Y:wWbv{@ɲݸw`OcOkڧτ<29<=5ܧG׌$`x6S>6(סDc 3ڇGJcn2=:gȍ8S9{Ea iܫDBg7؁F[ϱċnܘڬ_,I_9/O.CYHϧL->|" ,vÙj s=λG}7uvMWS:z;}QʟCkO;U:GKn'4k|#` +;kJaȏԝYvDE{n=PhA#n3 ύ^ ՞vNtgOc{5~93F.eC$U<H /i孤77{&cDطBWD{s>+^>6KzFxwn{,h+b"t(ŒR[b05sh=r~-mr_ 4h8bwc%Va1ۈQ 3R^>NAdgsi|; +rփ{7JyFJo}w\0̻C;zա?WL%D+3n3g֣s81bծdS"W){a`Ž;\_}ֶ;)<{Xg I(_jg!'J^ϸgNԹ~3u'D1 ~Av\J5\E#痏謤VL-$2icPܧ^$Z8hXwsQONӶ'A \>Ԩ׹c'>5@)IS0$z}/UɜI1f]7}53e+Ag>~EGscM|Egd_>3B玵A9T]re_oN3?^wO"A.njRv ϗ:{SJ'Ϝ=_D'yEcbS{f8_8so<38(t>#>7ј۞ݜؙ\YGR8x B輷eæy~vMYn'g'4i9 ҋpþ2{ןDLP9a;Yohk i%{اTb{IR$&ZȾs39(䌽d:[v1t6nq3+!F #Fڈ[vG1%ynٕ'7z=`eg'-9%#3}6v|A{ϢT C[u\'w8)՗Z&4&?b4 ")$(ήp{' DIٗFpFS^8FqOIЍYvƸt˩iR Pg !CC7>8Ex<;]JM3=.}/' {lyB(a!yPήmnvn?~'Ť}B:댠Y5睋,% \¾ ޝLJ.̒N8HcaU0Nn=>ZK(Qwv%A㹕Am0 ݓ 컱=\PovI\? ^z@|ɼzEϲZOpEކj #sѕv瞣q^knׄ_vv1n,Z?nL#OxslJ`yrpON_cXNuo_Lx@v[-ɋ]'SEs]P8< 9'uKs#D˖]gJ).IR6fV~ҟ{V~v%=Ĕ^UxF({^ĖP hux܋^ߋA3#|)ovXT7rַưS3nJex A!Ҙ卯m'ڱ]&ƃܭ'Azfy17DY=іgtrzܵVa"W7Jv~CJ`FT>z1OU)1Bw<䳽wLu3;#f7ueW<Lv27/vIev&+zS,[)3BwzO3/kt.<Ъ 6 Ҿ?|Gٳ| i;9ϚXb )i߽Gx +@q^Nv^OuA.?x֍\0: Z#_/O?a6OI@]v"qT  =%>i\O^^)>{+`ʺ0M, NGԴ ?/ͽ=XR)9{/lI"6^nHʻB'TOOcϡV:^K3@A~T 6tL4:Ie5FNR™<.j{ϝ^rx5p xT~&ٌuW= ?oxYt>W\hO$!.'0=W0o..1UJwEe|&vf "+C,b[S:92;o`=KLfw@Sk9:>?đF z!`hĝtp]Ro1䁵e7pnƽG퇟_C亁#r.ڍtaPENF iOJ%9G?݇SZ#(B}n;٥d_ZT~/,ק)yԃG77=T0-8Qv?h|nf욑'¾7.; +=ѵD泈>{R Yv9vl~˾pڕ:+zsII6~wmBoTUu7m?mtxt7_et/t;'3f=]3{s9TfԾ8G`;' L.WOUǀbjJ1ͽ405=U9l vq:gvFx5ι!W깗&;SΞq݉|;c.o~J;ϻNI wkٙfmyM=+h y7/v1ϔN@!𤶻^|} +r?wꇀU]j})|- 1~-@aJfp>}v3Qi`7N _$+p't]/#lo۠5b 7v蜇kGBR{zB_ijG;)*mƥjgQ3A|@y1R:V: %~F/vgt_' oBN5Kh,GPAt3Bw]wg~Yv& fƽk^>cl<#)dEpIhle~9{]Cx,~".V]wYҍщTmg;ZLZS׃ (/v4i^0j~^bi5'gy @"q>\wlyoqvư;-g_*P5#jܭ36K$>7%ONs96lzv+JD'E>M`H<{;u^dLgY|v8̖=ݧo}O6TzatPW>*{b{ 01%p"K:[q,kkAfcRs J3 d13333,K23cbfA)xsw$9gYٞ3h DX# ' +@Cou^uXL]R ++5ۣf XS#UW^dM 1b0ܭX {Թb,@f'DJ)UyԩFyA]H7 +vA*c%ݐ{e"WoHu+NhW'.KC M@ vlw) +>NQ5 +LXf&NLڵbF!?N=h>^}LEǧHAf$z(}1@~8BAA)U f"/ah, ca3 ht95jJ*l'"2sqL77nՍ=O3D +!6 b + n46Mz*j +AXH@oFS@3h2VCUF@ܗc0V*-ah PUW2P9R$S@flNy sa 2Ds; Y,k[ [;FMK=U\f$%YKH45׌ŎLV8&Nuo +j6cX^`zC#4 +%I*gQ(L5-p_uSގT Ah72bta4Uqh$j>k*jkP% N&##suLwh:0YT@j($cU1h˙d Ccu`>U$NJaIOg#X4; q1¦A(#+P -x huP-R:TK[aT=T풆!1W[Hdfl8>Emkl5Fj^j(kB{63׆"z۩\ڸQ4&jd(#P  LTj PF},߫HYVɐTk*}uN*ld(Lřv[)12KU+( 4ۂ5ۘ* Ah7S{4d*EA16yjHB-}.m³I%Ō4Dj{T] ^N@b28X} Te4an0ȏ؁CU*TAF + )RUIg3<ؠOAtE)Ufԧ&NQ 1DZT(Д5c"]T8QhƪLtի vaUKc& +ѥB|8PrRu= +Í $cqeTXj0l H3wIB'{6!V465. +~/u58~H' ľ Օ $1h=$` #JTf.^51R`,⚝v\]ER+@K߈ev *8"#+W`*46PTQhdRybI(>fBbduOh4RcqdM32~(UM49:=&B}&b)F@G ϩdbЇL +@ Yխ 4R&VT՜n&}ӌEb\L4axMTjAAS$~UL^OkAf}A.X!RgRuFKb_YEz';2vZڙn4ZZ³Z !$Ah ЅTԞBBN&e&"f  Pf'pӧlQME؆媚4i~^#4XT5 +9{SSeZPRvAp̄$؁LċcM6q +b({cI؇" sQ/Q+f7s1z/3nE?P ύS FTL*j~UTi5"wob(~]cԉEPX,'!51SDO&P+KFxtGj"PbPܰcdb(\l. !hԤrhʳC聆p5')iWMLŒ`#5+4h*fߗI4LiҢśT) 4%@4L(*- +b7X64J7N|U7jP (+«Z(RCwXSi_RI%v j2Skh47LEQ&Q*:]*덥&b 42t!) F2OR,GX.Lȍ굧F)*x `v-]gx^ʇFfb,tCb]p'TlP\pyH*3FꙂ$bH2qצT&_!`.CULpN F}|#kLŚNR>IRC qG(:PE+Ę93HS -V ++Ă;d|AǩAs*/>b1TCxIebHTSS*O5Sh M5'k6JBޣAho(3PyƤv ep`&Bv3xVkJ 2滩XPY3Ē.%Oz+ȫQ&`L]$U}͌ѼlkW{c|߂c_"ݎb[X#Oxb) ❋BLmkb(&TQ5>@֘bXحQSPܯ)SmR?}D&мJ,0;47 )UGM +I+50\OT[ho*3[]dduXAL˄zKFH21CJ'X # +4S 9KT)84bГRR˜ Ѵϥ ÁGdCI5Ma"-k UxX0ZGT6S +UUtb<'4J('VWǛbFAڥB Yb#)}fW4#RƬQ0bI suqXZHS;/F94L^2Fx:]1S Q6sޘHpU3͑R"֧B'W=sei*BxN 'ޚ'+V]w."{jM.bYGJųiH4T6@؆b@LmJ ~_a=T}fʑЃg70SGX1P&61SM?=[_də3gbU/B"M5#ZVNbj_>F Ű@ B&Q]_,VO57hUTs53q-fb1#GiM$FHX\*.R4S 0]F @4ŝ/ &n(hh $lXc"脷n,H:|_SuH[}&-jiQ?< XBe}NLtxS7~T\n@7+ urgWwoqvv}Pe@רPH`U,_ T8fB 7과:e1{Pap3/3J hswTݪ~B?zR^ߨ_!%zɹ5Ctqwt^^Om/ =xS^R6Gjf&ֆfxLď!}CSSz:Hɪ%6NHW-ld6kY*mvҥdJzVb#wgןp w/wokkZf)YxdjZI鯵Iڂ\f-YJΒXnZ6΁~ +D=Gsz;FKC1HG<>Lg5ZXӮZk9a=ƚvG|cy5S{=d0 L sFrS\L}>zU?8{9 ##N䄲uԢ6{H']տ?y>КsG:މC𻧷#K'0n4,g@H68k$[?Mj NFtx5~5mM #11/lhH<&E/Sp?u8?捰&h{-+Eˊwղ,rKk~V6[q ԱE󮠽HN7dZĊvӲe}G4 Vd +IT={|s e)wgi +=mZl%?_b##YRCx'o8T0N"|Y/-ke:tݺ+=è +].DtHG`;n6.dnU4I"GA +M؊I|h$I|x+tڟ$3r[9+;k7c&54'l2OLOmJR%d.x?z(6G匃0:COWt&ܵIG]ɺgdp2b2]2 JƺĠu6/5L0^1z[`#r 1dpѡuyߔal|Dx>kl|h8;v0r%M맲3Y\x860m<doƦ}E7co,rvΦc&H!< $[ z$ SԖ||&$eGyb#RF G@64g4捴a]tIw`+YkR #WKlHO[x Q 㡵q#ٴXɕ>ڔw 9y~V +Ćk7PNẤw3ER8&]Y3AtHh5q- @O g`%f A +WdRզ<)土-8 +⼣sʈqch)?GGcvf3R"yCF y`Ra9cQ{#dR:St&".!T5ˆq s#F|掹Q>u]4tցH :Mr' <#RH_HJ'p3s\L$<23 J=}q!yGc2fɧl kKmEgMgۿ!el9t1 +B=Ot,{: a(= ɄJǁ \F,6mL> RllX>^?LPpt(< CD8&${%.et6g|&w<1Yz̀MnF}O<@TLb?a.}ŌtE|ZL>a2KtH_0:,4>t`p:0uea>h}#(AX"]K%Q8sVyc]}q AlH~; zQ.H{@=| s0曁4DGuH~҆11eЬ4W9LI&lFgͦÆ%tiKa\op8ɇ ;u Y e.e&K+ @;n05Z%0ly,„U<#܃׆,D% :*r"V07`;C>dp\x8.K.AA7WXLh(=+./ک0ldC>at&},6j2<##s=scs蚉ttՓ'L6Nh]%6Ml(X  @' y(]:N1,8 e lH(Л-(i#f tˀCDƐ>Ec}E#=t ӑJd/}eg<᱃g;-@7 sl!Yu͜*<(rc ưȚ tyT̓+&w0KӞlD86"6BQ_%GәH6ꃌrXWOh(:*g4ңtB:{l*cL*a2d%e~?>lN#3H_90?{ [{_mK.J ZX>Ӷ|eMAܒC?W[Z69Css.`?2ϣJ.Kk0Ё`XcP F3;gRyG;b&P1Tx* 1  ߬rP]+ Q/-#E-MUQj&q2%.pyؠX!ep'n0?FQwgMD󫨻[|6a +')MCW +qk 8?zgVOaQOidciw9M"=:pO {L"ADg#B_Bx6x x鈋3Y`f l34 aPIO_ۃ +r ]+mYMu7A$&u>ږ1S0nr;?m Mt{@%|}& pZdz,`b|V?3R_`(+wб~zG7J >7f0?vṀct%SE|r0!)8=XS+ 0RaS7neD@A'"X" @Fx Zs3Ëĭa ~@5Z_?&>^d |M# 4 +5.Н` ȁ-H/t=J g #"` x3゘z[0ic1&H'}`lb|~"nz ̻>\3BZ~FxovѢ=@oHCκiäW3IjƄWCki3t@q+[;5O#NAQnHv#yn#c,Z .auC:;\P(FށlThXW%5LKJ3R3"KV6&w[u(XFquS٬s٬s0%M >C.2 Z\LD3Z"n}G\5CG?dOjM][ Z/o}bF5B:s:6l-9@=x"ΛUQ٤dus{LbT:aSGQ7KLݭehM@7`P3x)Uva.|$o}C_'RM-6%ظ^H`M/' + 'M$1A1pb3ҟ1QۅK0frG^>a`I#Iև-Eb2GNA6 ;o|`*ѥ_o;'pY{汙p IX% b0)Se,=7-%ƂYqZ%\ ϣM>}!eIS<2{P|}KAxu&0q(`dS?FVc~Ezs(י݈?`<u߸,ҍ|֯ƒ =ٴ 7ps1gY8G%4s$Wtsi?GRILdaN9&/QIS.2QңK5M-_R!dImчu^Fu.&:.}'b +'0E{RE2yfbwr!⺉[լgW:|$Mcx:00`a?{8Y!Y#\TH£{g'2Y1(w&r)ed5p!t/f_2HޑqxX/k [|=z0k2:+#< ɀqb:tQv€l{hN kE0vH:  ,vF<:`59grHT_Q?,rL' "|OhאvI ӉʋT٤_0if}N%]4Ȧsm3Ț fdfS^G6%Di\͝ll1@њGzKkdרd~4ϙ 0gDOmZ-:`%S3b=њ }1`i$VL JDaMrWdYc*|G#*ט!D'lJZH?u)[0Y_Ӆ9tXT|K)#q| #ȉ]–\|d_G82;|ڂb10[~&ognb;[1{TuKdήY;s']a .@Hڄg|1_Ll0:'!y`.`w+&cdlψ:O0UtdYqք,<|\ R&ALILƺ ܽhY0{`bW`Y x 1ƄɊK2mV[Z+WTHH҆HGGko`X;?b[&&AqMeLC +x?+i0P?8glӘARMw5c>WglYVʷJ^}˔Lcs2^K ؆-;gQy ok*ܵ@r-0`AzD+"t!@;#ǻgCHwqH^<]|sG ̧{V/ lrT6sHbKT-LPqQ֛BܓAv֓!yqG$^O؇_=N998O8b7 M̡7LNA,֥;F@\ o8X;*;nD^qσ3F(pż-,mV:xۏqy'2w )W Cy3ݔcHce O);g#E|!VnGV]3#K^FWrqf+#jnGR +w#13Q|*ZHgo'+w8F δ>ZG4?_F!n x1 Sh7o]ȕ?p~7s(?;ZںXESHJG p-Z$ZOQʤZ}%ǀs#/jn/fs/Y{[Ek2EMsۖ +MkI,,%rm ~.\bW,y'܋@.!7sM̫RRn!G(LxnFͥ roOoŧ5π,o/@kKAt/_$o0?ۚkT4詴փp3;J!! Dr +:2jnh}B:筽v +:K I-IKG4ΈAv)ٰna@_c[@op?]taX/dd|!XalQrLVED.9aDU5IGDvLqZ}Km-Gކ2l|#[n3KjE32}:k,E9Nͩ|$X_-*u +Uxjms/kHXhxʘSh.\롅.Ą'o|KW^6>s@`Y>K3jzڂh lҎIdD󣥘!| H_yc:_?q.ay ɤnN^ұ['+ţ 6֚Ue+H֭X8 +lG<4l,l dk\c>r@FI4\Ӎ5Ty)lG?刯Rx;z˳ud5T 3 +fyq~x|LpB1|kh={&S5\(af%f/.p,s:t+Chݰg̙s2eB>'Y9C:Kkpyqjg%{=/ୃum͍%|-J)s6.q>Hx!bvTJ>{Uzڈh=ʂmbIt_ڵNo1-v-#yqhbk1x)>y&{!T4wd˘]&\1e +X0 +CeǤḥrϋK6c +V5*Λ @b>p!LQC;xgܹ +e3NP{g%5Tytldߊ&TUɆTOc!h![}k)ܵ,`cBSC,8:"$d2X dTxx*0y(pMK87)sfd Z wd9J#S@8M. ǎ r~]dr~fOhbB0 +K@pw"c[x)8uZ_sw KGdc:MuRh>غII>vasQ9̶YFUwTlbT-Mo'+*<ɖ-VХM}s@dxQ)Cq 6glZB B)OZ|B^s]$n'^郑LRdB'!R9lZ`' pl61_Ŝ6mUwm Wg5pg%>_tRf{5j?"aP{(k GHn rw!.>aHG +rttx,y'21@.@7Z{9;f3qөD.1%c0ddX[`]1ȖbtgVߓqՂ|X} 4sD~Ϥ=+G<fq>%;hΑAx C3'f+jIJ)\yK4du] fO!o}($CQ~߀τIiǺ81q}H0ܕx" >q6YN[/T^Q{]FfIF:t ~h?S!K%NUg3o1E rO՞9A8.3z%n#zp6ίA2:f"`Ȧ6FE)6 +xwTxbo"M'(HlaI85q2hA)Cq ?}6$;W- LULLdl}x<淚qO`CW|d +Ԍݳ1;5=ziG +.!sh-Ĭ!VOE5~L_r|ϟ6 ft/ p>0k-j M+W/J:NޢyVQ4gb4Z|]s Vh-Qwt6||&cΗy8d:y}97ptǹS5w&c6~3,"f8PL17caӘ \6~?j'{&=R稻'A. +vzG_-jXp诀\!tBYySkAax-ұ!7pEc'B^ +]xP5g ɲ[26sH\C]rWþO6ST`/K/b +8,d<<_c~ad/^Hh +B1&/c%{4sM׾%K +w'1!;8iOȞM7'NKq7m^4lEOWd` dUz2Q4uf#s.ƴfC'w.m/[Pm_µ?7,Yx9xz-Qh1# KDo2= qTďMͦnluk"m~Ȋ 6 Cxb{86r9z)U|] +rXKdX*~+r{#Q[o}]A xI[!H%* ;sq[Aݑm_sAB8I;]өCs辀Y#)G1 e7;>%wdUCDqf{yJ*z+[D+. j}]zLw`KU4fd^$ҀnZpnnphaV_^2Q1^I[ 1e #YaΉ=|f=nt9MCZ\K*ys/M[h{ +t гlM g\yؗE=?鴝3Ȋkˉd[Zےi{lYh|<#ouw9&-r jw sCmy7VS'_۱_xs'{\O(f7k}vn9}=eO|IE +Gf6?-!{e +:yeXMӨ\' ·IoE5>^./d@gM6,;pn=gdE66BT݃%8yl>ct|.l +6㐊flly`Ŷ^>(*,ğ& SȤm[<0:лJ~r쯼ȯf[䓟3ǟco `씷n$q;co]X{;Ky⾷{N*Z{%=¼'S Or[{> ؆ٜ +'Mc +&Knɔ@!N̒ޚ>=f6'2g;DDo/;RC}lv}&}=#l飏Y@t?s5=rǞlbŖWg)U%jkg : ڹh^ +nA{ ,-ϗ(,q%:r׫6?m$~W'"=&N޸_nfw`?TrGz6Gs6c=F_6)D>/^q$q'+!'(m=7s&` ]A8gi 6jO7<]a[1XG; bv/+OccUr|dZz,כtȊqtdUS{Jh][Y-בͯWvL^f˯ts^Mwq1_f*:%z~}J&Z@]U܇9 _,s{zp'#GfkHGǼroޛϞ w8z՟;pOhy7̎Ýn +9g̴?ބr=Þ;v)o-V /֐{p3=3Aqg9y⽒ԟ9ʃ:VI}G39zP>;߻R_SQ~r'.ÙW;ٟg+?Lٵ,hsr9qw+a=P^gwہ>:WX?T +>?-ryuܝO~@}vT_2 XD7%v|ZmG.vgo(S{d\8>va?+>IwesfۦMϗ>X[[n%O85ͪ)uᣛKOϕ9ܿdӋ*Q˿9l:ػ6t-$$;O4wmHëY`==˚=k!e+HSA=~M]}C];mEm=ةd +cӽ5Nώ{<=Xݲ%:^NyÂ;imޏ)=w?FWh廑nG_ /s/֝ףK߈-mX#QK +9)^R{e٣/63>YQMⶽ$̍!|4""Û$g׋)=w[ +|ZnKeI3zڷ:oԻލo﮽ͅwdwC[̭A`sNT{[n%.(Ql}U[?ozC} niu̪mSʣW?l,xTQ|s2MW!+y̩݉ߝot7tԹ>Wr7'+O2oA}z7Ş +wGˬzZ|t%ߋr}q:a6-]W馜6E4m?XXϗ۪k%U]fޠ/{6ֿ׿e,R?:Z``׻owS?q/{5ĥ:qW{"x>^ySIՑ];î(DG wK#T\qR|iOU+*q|}ա %^O۫J'_e^w!y*5!=U1{W?E/~r>o(`uW{ټ͇N~휪IeTv#ԻRHȎ]/ܟM_bBk1.&V코XvR|ٖ+)/}wkW_)@:ݡ2u)rqo/wWl}Wu;C n汿{b?ջtSL5n~+탴$f-$&|/15_.12]&10NtRDoUw(.r6b׏)%+eU\H)+:[oW{5Mݫ%nOU#\T[t#RZkInZ 8϶_voPq鷓]{}^BQퟥe#Vg%_O֗L'&',-}ѿ B苙Y:.Zk2IŤdɒI誉$#LїL8K?zddܹK$R+?cמv^FKN̥_\)블RuRlɥ%oD[p+FZuƝ;u Ҫ9M*^Ur-re&FwWuv>|ZP,Zµ;GYD+6Xw_R#XT=]2Cd$fw+*,"$C1}%d֒틵ݽk~7Bc2:{,y5[Χ\byۅԊcW~SjR9e.Ɨx!µؒIjzzBz_4w^/J6s^銔GE3fd,}s$֥ng6f{ia]_6DKjZ]ۆ+10b%9Z+~ͼ\v&TNyYW˫/dTlZ|JsI'>Pz}hmPvr|%wrk_^d1?=R91qe=? -쎯\wwv9 9mz*-07Di᷁+aJ~~+aQ%mw JDDQ b"g覻O@("IrFJF@`v 9svttL0筻pv{?W 6tSUwSc8 Y\J>bI6*LYEͻ.6\SJAîk ԞUS8X[[`l~}9൜E Q/{<,斻o}S'x~>J4npr M&xgҚP#v/;]~\29WQXͽX흪"7onN}и91 }|yc#sg/~[zbnMoe)~^/ ߄4_82=mT|E_t4i㖂VFfmg/l:y1ą'tb^M.5lVZ7Xu@iS퍍;qu;~@N]@aa9ak~ s,FcG1C?9ur58gF602U C,B nOej^]; q8j(7oh|_ȭcGSkby!BR=|ďlA{5>n+F;dQlg:j4 +ٯٯW-{+=:NOSMd=j!ۍKMq>7rݿtJn'.5nZIP_DGمg;ڤOp D#[4q MwHF mj+v +? aml?]ڸLQәW.mu1szNKyG/6t^*Ϻ]ԇٿk@9mCP?/C1h%f$ ' c)hdda uf1hƪ\dEa.b6[?А{I@E3UOcpNF@LK3=Mۋsgu anݯ.R1AV0OG`m2>1Xƚ#s%l25ZLFC&"#9~֝Lߙ: B]. ~FZ 2j.j؆}ߋ=.mZ^׻k;Ýܞw;60ymaHovq +24{t-"ge77_ +3:K,?'vDϣp?iMBcq?Yۢq|Jd1YZO#3 W4z'7EѲ#sNgמj qM /جqzg6Aq"_p,vFVÃ9 f76*/ڬnQtN'AF"Smsd7@fFsmL g-GfˑJҶq6hM{"CK71t8"8] DD<+ԙ{>to @v+̹oduQFǕlK8?VLCmE## !ye眉dp$~Fg-@tc],ǣNYh_9εt;rR}^5 g]OIԱrmmʺ8vFvqg5vV sֹ^O۟ =x* 3d=i,&c N}2ӱ\l7mJ4aF+Sd<49MX,]7FRlW_Zb a胰s9{ʂ u5vTQE=.6BΰVi'͗pVYp0|?5I1"^ؗYao4'a;?dl/uSܾh52֛m#~LMBV՜p4q~4"MC*vV&9?ptQܽ*.<慜ړ73^MAzge,mlyφbd( Cs0F%}efǠ7J4~m:"nl^ `%ZuPcі#UANO6^-mX҈ms8cݬml0㧇ot̂zj?o`n >#h'n2m*d:ۏ1vfy8bJdiM!i ٸ9N4wQϯ~k\Nog/|rNo,:%xť߂o_bn ++6^561lאo!!XI]e q?,?2?pKESBД4i +dlǔ"4q&Γ h_%4#h,~kaa+.z'Dq߿/x&aoSwbj:ا}ۂސ֭ [U^/lx/}m&A|(dFwA^hYdfH V`62荬WxiG-LX`´bn_xɖ7Kso/M9>|EmCǣtBG!jq7'/S~Rmڵ 9wj1uB󔧏,OV6d:و4hUfQ%h-ڭaWu`y` ;";R*.՘+/G1V,}nq.Dž8>(SQqҧᩍxUbD_W#ocq' +⏼SߋT\| + + z4q3]4ꖆfK,hΊH4ӖFf"74k1SjK[᪮SNr+8{=} AϿeFӵ鯿~=yuWǛY{[A,)0A!HcA&^^oBޜmmc䆠} ݋‚[6l)jrn4qb\Cֆcx2`N{i?]X rIx&(}~B=*/q$xIo ~,#=&3?F@so}֜D')?.r[ߦ7y2wfvhl)s7"sZKʾ5[@U]MP8=o ] +V=ᅰS<_{Nl>xvVs?&zDO a_Uq?{Lp Rw\y"}T|yqC>5яtOL^3aňe-?Ʀp5M^#ۀvU/DK7!F!G!1@|ŏG!;!ZtKN Ņ-E]omc+AH|X,.=b%`'N|,b}k +孫A.V' ?vlѧlӳʷ*E;߶KőШ`"Σ3\4dOnmuaO" +f1]>| nQ>]~|R {n%A$*k0ŭ[-)2ic\Zʕ9frGpAP$$|Vߥ5S  8K#D-Z}Zhkciv2DUvia#a_W/ =* xq2'W~ M^x@9,;QAmh7vFK+e؊O\IJA˶^X@z/mH2+(5,}Ȩ.L Z: ?!zso.ؠL4G9!5ל|:|$އ쉎aBRAKxwqck,`8+>ػo˯>Mvͅ97[z/ҧ0ѻ$wB۠jM=KV\zcT0c#]N{e6c>ɹWzKaaxy➟ }On1\͵XX ; +]!kthfX?+sebp0S_$CTYfUs*}<}?\gzB!IZ-R(&S?$ǧ?-k{?+"G} +MžyUhc Qs|0~vntvo_e)?YVt*k ՘KkddcbGx8{"+ +)A2U:b!zi鶋֯ʶ_D|~ѧɾMټK5y!Wܼpot'W2LDI +zGm:c2Xc2 +a|6D1(קKC>9Gpg2h M&H4}\";l"qSk")v)hG5,0eM< +齫칌ͣ7~Ĵ_NW#[F +[{Qi!mXH<^J=2BBg3(+V>>y:9+'Kl2tYĒ Dj4JOxIhɔh包*B4BRW//}Go%TO]oVJ-~kfk쫕MBYv4 Χ7Q:Lzhie̞>TG7I"IG7!ů>{un`Ap$z=|| ~go':#xl>i鎖Θ쐗ѡlB>hhG"DMlf-]}")힪*7cԦѲ˨+VLN9h.öPyvt[Ó'*·}kҙgK=FS6KsE'n' ]w'閸KH{$w^S䊜BNK!Y|XL|[!sw>!ZVX)Qry{('reW h8u7w? y!FrUFPBA`~¾P̯5\KwVRH~re5҅;z~p_O%7K*e;pP^x|ǐZ ̠ڴgm,9NVzZV|wgݿo'q_/KX a}}h(Xץ⌭Ʋ F˒ |n Z1o1lL.5-v\r;=2{Rf3\Zh.ӂh՜3dg@qԱ$ݿ:K{X%ى9=]}"Z7'YbȀ>NȎ9JnIwǶq=D~~gf5sD^ZuQGwiɩ~꒐t-Y`^h\ +#tR(Yw0o5b68^STaf!=Aw 8Ⱥd? Ji[gd4-^Pmn k]dg{ ؊SMS;>:2_Үe{r+N3M1D5צRw{RݨX5oENRɒ$ WIÌh75 g[DtE@5JA8k7EkѸQ2!π_5\=Ł*Yz)Ue]Ċhu +Uїϕ]:R2ݖtcvcd}_\JiI½ҌѲb=ojX7KN֗LR 8)Lv[ٳ@7􌈞l7bZQ;8RϜ^6mǦl1S &w}t[1u9FI}]6:A*}\NTL/&\[<' }%Ouc 9N)>onү?Q4l29b>HA[fZM*,E[ZVbr=QL|$(\|H*1"̨4.$NIXqూ9?U2N4C Sߎ<<-鲢kcLqtIs$y;NJ+XK ]q` -_=$T ,%:Sg#k]4y15jZr;&ʷ 5yMfswVO4%2(ME,ĢQZ BJY2Kφܱɓ;=-|p]kNΡ=\F?\nAzfT"v 2[ 0D{`4@GDG1ADs;N2 Qtx0mG:L~T]u+LGo4*:[J(gr??FS&~FVIZt>sbfѻ޺oJNCtlI\|3>?]-e?Z4RÔ&HTŁTWSdIڋJlNfLBve \TG+}ߋ\]?CMro)\C>ڜ.a-v͖Ο?2Toʳ6ʁYwv]{&C3Zu/Y88Eb Yt;~*Z vS m[oЁ^DB"fN9 _}*ԑb?0ɪ7c +,eO{?yS?f71zlrjK⛺`Tyt9LqDtd+1ߊ?;SY^D!bVK4R=6>聦40M t@^7`KPM_Wg*FkK ]*:c8hGe/%5_ϖu\ +( _z+?t5HךͩCU!ɓ.Hk :#JYUt5[Y١)tv$YVcGG?d7A֗Tc#U͏DNd\b~Nu Gqz|VK\vˉ9t=\;[6[0MAטf'ٜDSgSp,^c՜hIՍu=S ŐZ*tm=t9t|?' @0|=.拴gηNTGWYSAt +ΠC! k;—=\d~Iw~pd6=toȶ^^HxKSp}ejFc̮g+ Z-qHϵ jcXu\A_]HE rq|y%6LmJg\J \citpizMIH0;A{`&oWOq=G6ҖK Oh46_\B ,R=r=ܙw\]{N|߽z~\J!]8},d5sזjq齿{K]inŔ39y5VVzd,bQ7'` з!ŻcX0~m(zPo +U:kRЅa#ţ@K#hQ[A0 hW-1`r;,|gq~b[dԛriMS*GI -FjC>6_^[4> !PE:ZcTGxp +9/|< 4ؤ‘և?D8H} 4ahaK:@뉮?g Bb?1Z/y)ScL,ac2t`LGAMͬ4d"҇I(SāV~VWc2Wn"yہ=6hmj60;ݧ}_|ߤTmȺs aʚۂ/O= >:=AǕ ¶l\c8&r$U[M!G𑅨9,AK-A|$4EKĹ8GXܠ? O$jIu:xZe”6t닔{YDu7E}/xx +;'KoCNKpgptmwO H8e8`3O6' WhB(}Gu?YeTkLYԮ@sBl)o"j"=jM|h qwAئL}Y-{s]sG9cL|8A繳2!B"}\e)q&*H)M42*hєk\vΉ\Lx胱MWWUeJbН7A[c(eYuH\ZuOB~ά+1GkyI}3;by_ԑh1FQٺ+UM>^ ؁VpԠ`- ؝ 6m,iASsmΗΧ>·>LBVp]@t`?3=$(;:mowRvܑȷD<[+sk,`;;mxlڑ 6LM'zzĤ`{?xs}|aIA)@~wO&,vTkDWVi 5.hU Qjr-0kZEtX0*5gm}ayB36^Y4y.ЀwlqW<.M<x/b%rfҷ&H!!b褳NDХcƩrڭT9`Լ}| T=F𛠷Zܠ7L"ț/㚯/.+h/e7o:X~n0Ruzhq{:z!?zH{M|}_2 9eU8^(P8/a/<镅ݓ׃^ T:L_yK幻'ei+f#6ƖU.Ŷ{'ң qajt~Wj~9B>qh\ \CE^۞x+JOu~#٠ZĂD"e}πA?gQ=؞5\%\v ^kgOe [vh:㱃@xIʓ%{>9է0/e7 9O[º@l xz!wWlW%C) 8-X)N:0#JAct ; ۯc{_y:qҐ$M`ʸh zʸBed0Nlp6G4msYE@6#MADguC<@'u#s`-\^y_Wv?A~+9 8DTK$#ܫjj-&;vVʁqL~3;.ғ8Eh.TU +Bb@kUᱹka?FDGRk4D" -W2 +xucl K8.+{"^cB=>rHZ,0AλlЬhAgh80>TQ9Re j-]a}Kv +E3I.0C1"z22;˫O۪*?#:kt +e,a(R[o Z l q# >^ȡ>4a|qx*7L h +v׏Mw]wSw~S՝|04s{&"RaMBf0n}xfQj":PM|ͭ%cۏݠ-e$9DpX$,"CEVKer C3'.%E){-s zvSɘggM5""?s`I6\LՑƇ  ,n§ήj KOxrMgCחAܪ>3'ϖAmE-w` 7TevbRXG*`acvOdoIiGw󀵔?Yox+LW,%Cn!v +њ_TIU$mXWi1\eg\1;a_^zzE@s0 ֨?T~9d yF)>`7x~/Y`NX`o1u Dz x$}r$ wl?60bI>d80OU2TUm"p!/'lS;%l`퀰`KN {S9+{OB;JrΒ1b8'ğ~W/7V$ +N ,F>4^ Ӓb .lҞ@Y;YJ +SLU9]V;oB9 mn'6tB %86Km.Gmo 8)rS2p^_k-S/|.A;ag~TEbovVV34a.*ԹMԛvvbg)';+iA8Tb^`l}oo@L,0er uV y_FCՀK1+$`jdHxd='\? tŅloYǓe?#4`90豷\'쇀KCW@uc [p? rm91ʴ][I7[ xsH.ajYZQQzxpI;, +H.2fC5Ukuq6z}9`eC,䷿==ȐF|>5LEEK |Sf+Xbm3S^{VYzp+YW 8(sۭ؆c l 7 +|x\G)7bXpvlX_#܁%[moEb a8Gֽ*͊/ .QpM.+6*]GʆBw4*MX/ VcC6h+cJ Uo3|2ܫX9l>PbRijb}RǔYm㉏~pvam? K%̖㠭o9Sw|pi c,N@q s8Wo1+8T؇BZso& +0xp,bMw,`_Pc`8?2obEx燭96M`d_4o~z?1\p ;&JF=}&.SK1[/v1#Yӡ+5Z Ys>=Yx|t6;`7|m>|Yo>_lIbMl0tҖ.uM$)sK$q~ >}Jw:2xvޓeVs&`ئk&"/o,q +(MYM\j.-" +@U& f75o }±ӊמFM3},XP|PT]wXMGp0jrjp )' b,$y[^8|emwB. OӖMi1V3}xOdgOsjiXWz߮0H.]ӸCvx-ΐYUM lb2] NВ'׌}=잗~Dl25>2mkRq44-HZ:/ mpr!3|e߅ͤ1P3C,%O-4$qJfXeA';|vp|.\C>/Ǩ0U_!6 L# h7k- !p>8 {؝<Hús a.bzَNS֜%:c)Ĭ྄{}mW{Aag`O \% 1SƘ5^8#~p9e9=a AB(O7 L,!FsSMם^ 3b<)JMRkGÞ6&KW +K`2[ǂ>.S/SyDA6\zȳ},ܿ ^^n50ۋ[99rin"x Wb `@OW.'/DZpn>8zuW +WOX_IFd 5ҽ6\1ko 宻+dm7g{:)wslobAzh0[> +THLNr VAL86} 3f{E=ְw/4B HM__fD >e c&.΄' Z[pnavC1a5|y5G>6p xɓ '9B|G S*'P:{8&>|C_cRp {iu?XtWL Ovmr>l><.37_{iORل mDiNkztűlJ sm='؊Nە^q}|tiLD6ԇ@n +pzN#J yy}tr|9_/Ǘr|9_/Ǘr|9_/Ǘr|9_/Ǘr|9_/Ǘ1eklKPb]cGEIкİ~ .!qA Vܼ9 lJ‚l +(!2"2?) n5<]l|RLZfe9^~]Хȩ,SqJXn8(DgmؠP+~jGγb_ǰV +?%[͟gk%Wγ +/d%#tg-X8nV1KYd |?^_zI:afgҾ?f:>9}\Ȑ0g_ۊ#Mz4~YdyvvVJ@kW/~~oKlI_B.y|Sȉ豷x<|*@?}Ƞ0zD!,M[ +e̴J0kJmP5͆PCGצ#l ~qQ:P RYVcA +?.MWQe(ퟮH5&d9윢}g&C"f)9jˬ/6`\j 3_R\|>l2'[ωLN!&P4ʧ`>"s( TIF2BAY&ydPE I_9 AɪT!C5MW% %DR%- d(Tk4@nOd]WeV˗GN" %A|lH.q!S/c J rj 2HҀ$MFɄh+)cGdUØ O6-2^|-&mN+OP&W$$J-5E'Бڊ -cFV8E٥ʬJs.6EJ~)Ahc> \6c2!EORf2 Db]T0A@gH &$VHQFe YP©l|b<Ɇ y +IņP,;S^yh2c2uaC H A ) L3%069V%|r(䲆)R% <Hs"P[= B.6C)s&pKFqk(FJã .&_Ȯ9xa$ o$M !p_&jR!רϛb5LMX2!jbJ@$bЬaP +Ug"Ԉt3nIsI\DTRP,) +?k~ +cm&"بPcF\`岤YOr"Rؐ7lIƊCyBs^D ˓*A|@fCXiR.RDˡT'"W +\ Dz^"=m!ET.2 +FmySvMA*J:T-> +Oܟ*{HA />H%o!eSIy67EzP\>AQ3 c3EQ~p&=6`@~ "bx@lG尊 e =B@>aF=MrIe>KP x9"CZ)HA`.A),R[{v!AVA8M&1 ˨; L33Ѡa!߽!XdLp ˶#g"e,ݧ/K(6 G{ E s>r> WVWK\]0>:+K0o@E%$ @!H1h$?^ R {[ R 7G+L|?:.&5ryQC _3 ,smd_&` {B Ip9cFx$X>Ʌc.h|LI GGK=&="3򴹬:TNxw0$wG=c:,DjbR׫K!WAaɂKW@Zqb=qO4W4# kpQ]'h~`&W,9 +T"yXdG5p?I9F|Bq|\aHP/LrX2ZdQqďK~Y[`ydV`$ǒ ';9gkAHi,X"פl`Z8̜Cwa,?ҁ`qrb%dȩ sw{&BDN`gaviJB4q|z/1dFP-)_aWjGjǧ/!|!rch d31J oc\xPIs~ ΀xXLn=D4dB.蓼iyOS y9Vխ)Y7d:sXƶ(r8-! 7O"`n3cT;ٟc)gBb>s +#eB晱/@J +7Ȟ|sd@AHt 7 VÈ3ȠzG 94npY,z;pDV^ӱ5q9PM<8b5B >S37 B^Bxo2<(!'d@Rp3YP'feN(>MGf#ώhDR#7Av@[4.:-O>| +Bd#L-q m@zq\74FHJ/!L`a7Q|xbg$aNa6HAB%!`9+,y9lεf- +(gKf-e6,G(V& nH]mRXNs +k,]{uB{bpN`܂>~r%zMٌ80et>qFYpC)i0 6qEsA |M F+sqX5a͝CfQN b̈#@*Jͥ1 HK"^m8߈0Ry}1GlG\%u'EdAd)30G9r,-!w"7?\'M#@h8WEmXZ dE3" v]V<`Ku*Xh-sEaAp+ģs]Qޓ)~!`Q OY@p?Z|P]=<./_bׄcg( XK9ڇx+m T-]L,9H䓹xJoW2ǖv6s"Z!ۍ+$tlHV.f6CX:cvR)+z9$c1LTN6AYT*LTaDTW/rA u lW0>Fo6z&XQKO@@/BW[0Q\cD Dq}'^:P4 d@ Zh?2 T<$H Lu>c*w'<=Uta,&HJ:.wڅx +anSa St{/d|vG- _ g 6,Ȁ{ea<9HS9G=}!y__I9dH/BYB/ AzKTS?D8BI̡@j +eq YIK$AJ]  ɌML +KӠ\ +R*P $?M\0^P'X5t{e&z Em&F2Q ,XE T,HSy RPIu{وX4ć$claȝ`RFX @8\pQe[)-xЙQP'n_KY `-! 3g}lbELjr. ^~^蓻Zx)J1<^VImbe{ѽ_\!9- DBoG&@8Xxv_{(YKpQ1 )>O +*FFA2PfciALϏN'<4.p䁟ڔ!)gȟ)@BK)7Ydxot0_ t[ ^*̛ ^=|knN/] 9-vJ!">a^c䚴.R+ +"`24]N +endstream endobj 44 0 obj <>stream +#풳H蔣usITA: , +Q6,Ru8z(AIc@S5& ծ7Ld>&V GRp~^F瞿:`)ױ9lcNo!7ܒ. ^rAAk +)&Zb+iWyD!=4EgV oa0_'$>^_Cqb䜾X=u-;{f-|`?Y{m%T>^&LReoLf NlCq!#dVG9 & +#,Yπ~au".QEb21_" yCQE:VL:)1WCsu^K|lP.+wܪ0ɰ d0OqRzPņlY+mr $%ö`C!s[826lC!S&,9P }K u[W҄C;b:db2X03˥ + m,bLRn7G[XlAC QhX+[usuc!e ]`&A_>a7dlIQ0l "׻m GKr;E,,B$PLub +$P E%/ (`sC=)@Pg`>Bm`IV5'/tO{NYKY 6@`shčX8Br)6>^K_2l qqylpk$N¼ P/`up 6@Ls8,~P +6_6fvc4oX!B؅)DtZ-`sVP 6) >\MlaЯk ^53Q4Q΄,e+#Œ&M* +-ExB! 앀>p~$`d MhS{n>Pcxu ӣW3QpD'Q3vq"6Q=8/^'؂=DR Ҍ0Ǜ%NfuTxXGŒayF- k(R+˽RҪJ2⫝tF:_PyXNc +:l'@[hz32`քЇVP PՆ/ +ѴqYJtB]XFu-Fd=+!`i)/I<^բRU/-rDh0R7yAnCly}*a: x9􏀫PC]+95l`$mm) O] +P'TT5M CB=CwɵJ q S`ytv47Þ/QhqivqJVMj[ b䚽lz&] 1Cӆ ŅlCqk=/xM' + )'P^6)!C'G/=c)*W@r5NaXvs'r3i1( bldn hεqJJ0RrݠлA@B7g߰f m( 3zDYTyYxccǬF'eCA7#^yJK\Cj0Za賭bk0Ein58a,)xk:"QŔ~;%ˠg--:tm$^+|QsXl#5t{RH2WkȤ6l#AzXZ!a lB`]Ap&ڡ6`}θ-^H_@ +؆:`Q+%v_m(܇m(DQj#/D|%E[!elr +ZIJ +UXQaZu==,W$. [,Jm֠C +7}kB`kѡq$ѭ:3G5y9T`.A/3VI \^>0b NBk!m?A +ulK-@~Ena32}H3_lkA|}nSp8e8חll/CDxA!tHzQrY)b[?9,6Ɩj9 }XvĸȚ&u*]- 5md`Ku'ܤĆ(yd-6o&=:$/6x]=q!Xĕm+;x (F3w5wK@\%_6P'`?^utmP%_Ŝ's9P$NV%UiGeb_TJa9#X>*l7?;#lqc B.BBy16b"gRa{ahY#`Lj9(QzaE=£2pDu,N9Џޫ{`7pS|6B=S#^Fu`sz4 +֩`}ֆG{n +x_9XFؓ u9p}GC5XrpFx9J\"} FfXMUAFޣpm"1=*_ z& {s;pb_Wn{  =`S|pػs2*0_W`_]˼!ΰ cD?`+\ XGN 6ZxO(7uv1a{šs}cxoC|XC](9I`< lMK;g)Jڧǵ$/9)qi bT=/o^׭ov).7Bu m它5kXvDŽK_e.}!X3EC ?3W< +L 1p#(~:s.hCl>7ϵUc?n6=xqh{Mό,ɣ֣QXLHf:b;h1Zy`O{O wՙgF٢SgGzߕܭ3bDwZNr R +yiDugu撼ZS2:)jWK׀*)N: |.d +]֦Y˙ޓwɭJFݭ=Bg|U6zlР״zGGdCbUCd5z5\ٴsB#oA\&ꛄ,݄.f&(!~%gJb>9Uβ'I+]Uu}fAl@,~xRѽZSiFO_&Ij8Ȥ@g7+t-Ԯ,jh|%uh우Xb4t#e0X;~-:>27:ln gDO +_vwRT֠:ظ[ѭ:!yoPUѐ;h{=|QHtU,ɫ;la1ީ.HC2f#%}[]ѮNM=:JzJdԩS-ʐ4Z`͂L{Df~:@_פowu[u;RY,⣄}(6ƻ0Q*~z}&uUWU|f?ݘ|U(,oW~wӒRZ.Aj+Q/?J6u@Ec6Cw蔬HP"+J ѽ^#anT|S$n$iaZ(Wb}- +:D[o"B)>4e :N0EG޳Liq#ԛoFW_T'BQ-,UW1ߛw?!M2c/~>ZLM,ݮMfv +B|KV 1CzN-~l"|aBE N>^KGwlEy SSuz1(fzMԋ>u|/o-6Kaw@ix)PY83EˆywIj=xi\p)WBk]U%U^2{$^ zyg|2HS)%iJH{5'EF.J܌l/NJnDٝ\F:Ǧh"L(@W]Y͔$T * ~z#~bŃA'ڳ#δEyVąU\9u+YtXIg]u O ͐j ~2mPY¸Q!y#6[[oZOJK%e55WDWs.3M!FU$egŅgQ32{[wj!UT)zf-o&?ɲ_U_? |$jBzƈ?Uѯz̍>}2F:*^Q=o23zUyQB[uDrHn C>y/%63QAˆ'E;Mɻuɇ<_UNT_,m6)?nǺ#UyliYٸmᾗ,Z,.EkN5oώ2f> ~={.Cf2R)kZ-mP|Td +w 6ȏ0/;,phaMN|1h]-#dD7sLM^G긝dٜ~aQc5v+%l(EME|_6dGv R(2h#" +B%M?1%ݧγ?g09uDw猛wI䉎-i~A՞ᙕwBʛ/؄VنYWZ+C3]"}b-b^L{D ?xԃ;>Q!ᵏLW5nw] +<zst,Ը:i?O>{O +NJ[J$5o#\낒S/f]=ղ99d$b K_FٴD%&;Ի%Ze%KJٮ]wZb#n؇n}c4N*"^8>+q[[y3'R_)",8%.6Y+k`nD\Aq[V\T_Hwp6|y܉c(?V{FG7DJJ +N:58 +ZT[fݹlS[}H&<^Uۆ߬uOtWg^d}n ka?Ns9ԄW{ėEfGUGFmL<ט}5;B(ȴYCs}sWͿk zĴ<\KV섣{yóhNooL_[MѮQ" ;9s֡Zۣ]c]ï5_ie}ۦdut}z- +Q}bCbj]znE]t>pbN 8Fw35ʝ3Yj<5/mX'eNz㰸ƸD۽DȔJ +XTIoٙm5TH5e$9Մ'T&,tz)ܪ1ҁ0hܘI~ ua';b{]]f;+zbNud]litlK~jz>Q]ly^1}hNHl% +iѯՁ^U~1i%E,v,?Ւnu#tg?0?Au(xn{m`"WE.Qw޺%[wGQ/[cDA}h6YeC-?G1u ~1?>:iq<[QWc[.h1 dĎZ]ŝ͊*ƭ{uFE}BtfdZV7pԗN'W^Q7߸GFDGG_ALsVQOUرŇ$M/uA!#pFCӫ^*NUȝN0GnΧmVSUEĚ +rBBO 梇~عĆˈENFC*kM,6GA5$+GLK@PX<+j]5 nn 'tiI ~0z=CrЧ“*]=c]zcJLXWdׯ>Иm'j5qs~]t-댩)h|٨mZ]Z1 ˕ylj +ų +rK5"&8bPY'-Po4 pyrF/E}=~!7QK[1WjzFG\/r@y12#qcěR%(G< |T~SDlGyGV6f_核AaNsDe0qFi&;~1_ݿ^ c7{\\$>K&Zo;v}vV=?ޖu.6֪>*keQLyE8vzU rȦ +p?tqA?w,wkyG࿣~31ˈŋꗉ}.5Szԧ +[˲WQw޸G?-pz);.E\_Dp + *ʬB!fSxR{+gST=SFfzEb?cNY?_p$FsoxscU'i+w&i2ꍜ:Q@NKx`oɶHMQˀW"郾EFȉsJ+fo +ƺV!._U.Y0gz=+xtƣ)xg$Z'7u3z9Q7u^HsoqiJwM(-tn@cXRRPWs4Ϩ'.Q ŞQCd5i[]^~u\Ӹc:}?}8}+hX|"N7[3;t+O/߸ƾ-pI(pM)/qJz^WRP תHT#"*׬l(߳쿌鈟GN@F&f. C4y/$fZFL,6n-1[ Zəu(nWpKP"+C_lɈ@9&)ʕ)@L~֖7j QO{|-;26ɕ_$.#fOB̝3c!?k;!7}1sFb MD{r*͇Fٲr]qj6#2վ"w.}ܯ~pvNsLiO(/wL޵S"&?xɏR G?MAOy(,$f\I1n1sfbĢ;ۄ2{~Loc_Fv%]KlEsleKBecBآ"{# P>}1 +*PTVoP___yrx~,#~~?\4Nfl%m%G[F,Xb@Y',5 iW0:bC$a/#1n)|\YTL{™= XiCd}SdQc$oܡA2KBu}]'0?xFdfsƯ$L@̛XNBqX =̉yEļ1{9I^O̒W#=_&8OR{isGL]L HxY=[BIko"ޗ9 V '֕9&&Il3_)Mk̽&Wr&,#L\MO_r*Bn:4vs!g!f؇mJcb1x)b=ٝA=ӔsK8Eѣ |>[[ӈƩ>1\gCZGcJG]rG%|BU빽hcsZ <~PϦ!" 4f.D-$&13,!fM\m3!7Gŧ +I=bfcbhLϏX$bS=R*6}%/&"UaccTK:bz*b;c󋜢/Ƈvϙ2{T3e ы5E8=? Uï'ǭD݄ (on%#.&%[̉.2Ugb5@͆E͖X '6ei[?9/Ip};Gr|( A>񱅈'4V>+weslkH#5ղ +(/!G.Fy~!Y86gϙ2r',!fLXr#zL]MD΄PXoF,tX׉XN,'֙ݖY?amnjp)I 3iA!"opzՈ,)i}OIen?z/Y_la,<4f13e!lc%@Š61/%/SyK*"uB~1psfۂ&Q6Tq yҬO_X|(;c^#,b4!>>s]RoSJW"48% +;q8?&kAc*Bb&ebBU+(ގr&gh͟7w@^XX&o>NTs! R d6V:~c=i?/6kpϭF==ބ H/.t~5_sN])2nW Im0:,9,sih䦬%zlZ݊XݔXG,^XjbSĢ"b9b^%{&9kG\NI=_o3G[ojȊ> Tsvlkؤ7ޱ[%|(tO /q>bސY{s q+O*=VIۜ]}3vL\1qcVNSVT ?q >qg_=Mk>cஅQ9مGV9%Օ:Ƒ_ZUJg@渟yikug5,Xk7rћ}Kmq;v'#78ͳb6։|'nsx7V*.Iss{nrK[Tsv#'D:-?]T+ag}ڀOF"w=WIxiĒ{cy}<ó jJbzғXK^!6B-eMs[O5m5nidzd7;O]׹|>*1_xmF1EKv/'<_@'eApJޟ² ߎQMwganDw+ 3ei.BX3XƘX$\ݚX $=NJ+֪+Wk"?bDžcvߟ5VgǙ_;u }~w훕[6=Z䞵7圁~G8Aޞ}_rYiϫ(N~ЊWI?Nn5U6UѪIC+(q) a4uΘ7OXŐجqN;){bđ;MS*VxyѧoUz^G5+?9}O;'f>p +k +a8ZWprat}6!gHw 8w܈;u'BsZyD=]9l> }IH1Oi[&NXj7id4no'#El?zɻR8y{;~(߸:?#Z9nJES;f]S[\.z +=-{꯸+8u΄Ý i'V~yS32QٲRW#%c$ӚכDŧC h ۭE|,ym1k7&-/MYƚXMHl5J1߁x2Ν2nB#*8R5hLa7wRc^p ":]ڻ_3#gurT4U%o}>H?b1eȐkė:CQ;Τ_ۄM:Umg60y-I+IoE~9lM5)c'/B̳c:k7STs9gg]7Wې 6צP wn",.2|wPl[VUKlᾼ(㠋~'^GkNO/kA~u2~C5K13Fd )=)u@9eߴMO?ehC:"'=G,hYO3.GSeQ]X]@Pꝫ[ +~pco5|k@;]zw1Sa3&XX搹DS貽uz:ݣoQ~% Ë*L?mHQ/s~Fe)k*&C#Tr4p4ݚq!rU9k S^VUڇ]HŘul|ߤڞ}O +!&2k<~L&~lf~BV*A›mdf*󠃥w +JEKڠi *7Fe%NZK!58WAڅԿ5A-k9L=e&Yc@R9p N1n1t//zL>]M,@y`$}c"h +*۶zJj1gvkcB}>qn?-F此}`jfH]],8AZAuP!O 2?Jr:iZiKͿ\S˷O&_ΚwճӚ]м3UY!>6B7i-k#D? +.DO;IL|FJJ-رl oBo +a,6=|q:MΦv>_Fc2G|e\I[@,ouþZo꼘-۩EWmlvhYԦA2gPTf~ZAnp;c΍+\綪p48=_9^qچ% z{i3Z ۷ZzuhD0c:DX@|ҖK3Ydr^7v#@P䡲ԋnʝ|ǃ'm%ՕR-}gOiI{XL[M?_iPȡ/4r{LSq)w쉚meǫLjjʺ՝J 8z0ɀJoQuop&G IpڠitDấ* E }u{2i4] O$Mzpi֗<8*kHrZt|^AR&mH |_]ֱ ~Zh ?a៤n5T!.mnnPRSQJJГ\]-"nW%J eMByVBca/u =Be~BSMSHj*krvkRkl5t>qduܭ>&^̿YU-G_EQYQ=ʒ >uI̤Ťk\%ew^dWQ?cPQםzsUyn}sfd|I P[ bflf .L3ؾl%w"uq,e-=%N@h׉VZxOgkaXYC<.9WBdA 3wc-ʶ~BBpNXUmUIF(wåFhhi;Ƚw]YM߾;F!T=״uCۡ;]µWq"uc>^H,i; hx4kO}׭:RϜwyi܅8sαW媫9K3fXS,}ps.l`T1V? ֍g c-IUjͭ5r|ݝއa<Р;ntY91I,ꛉY/KG@'Ol{6y +_Īw +Gn/"\^ڪ?W⏞||V#2I9LOxw3< o>O>鸃p/5Xݴaf=ݛj<=I[o",W6V {]b-C|5 +3У /\N\ǫCG4瘭*W9^|iבk`P;O/ @گWs{?v]ڢum1%v/(3[w˯+a\Ʌ٘+J`yAW-i|כŶ; =#ןuf[g.ЋݘO:)q|z٩|;Qx߆rWi>毊EyJf>qf$f lӽ//B5K',=}w@hҭu־q\pΘhs^v?L<6.^͵#V1ɶjR1lK9*Z ɧ>ꅧp_uZ{Y+t qrVDnÍOC?w_]1&-4= T_^$8wV +5Ao:ay#ڱ|VF!9#0; - +G͇1"X~qczX^ _y +~+fGp |]k?7 ҝ; wͺ33}|8sw}K.^ii͊5[Ҹ8NF$ղ StW5ނx 1>HY!d7M +;;oՃ;}|ݭebzD}@/^Lr)4i +V^Yo\(95/??W7vO^}.́^o-2Yrv6?ASOf=?G]S+ _}A31|\=kEfvXtn&A{{MdpA|'??_#د?"&b~Mx@sewN%4M N_w!aK7]Ӈ[NK569S|S-wml]Ig~fZ@s}0-cGxK0(I%(يOmt*o,t] NjhH4E +27{gw@ |vD|x6tx8sR{(g ϵ]g|,Zx޺;Ų9bn$bUǫu F_v>]4阬+<['ՎB3G@k>4t21' ="r>p/d;)s*֋v ^}mRvNnhA{o6iXs'w^y_ /%.ѷ@wqC5܅_I:w&GIK +]Gkt϶Y8~}FW:KW^ae_~Vi"|nt+dZ0˟/]{]y ;4QBġܠ/Lї_O:\t.0[4з!Ȓc !U%Cz4UxPcta%cΟ]7 ͸'pEfʸR{!kP,/D+&')ỷcQqBRX7N.Y ‪bj#[_]ovBC*AV||1 CbRhg= _{6HgҡFf4z'!TyfX 'Оgu/nܩz^nL|(m:n=b9Z5@bl(44l/>dk}DH> wJexjnw5\`\>:>[:Zd!i<03v-C{|@%dDCZ*\vr\7OR*.,"߻g0,턀KK}OHksAX7_|AfU$b.<}ӭzV?ߠi⑻]|4[lԸN7Fcs0;@j Z?CivЊbj#4q=˟GHWBJ\{k +fh U,lRL!KAE];m mphhr5cu9g؉Q%W8ЛLkMd1P/iL5fOXAK!u8Rr9|>?̂4qbF1e+2(!i#C|R(bH낢:ynN:=#x`ENqs'x+i>ar3}PX=N"HHV~vD|`.w@7Qj}opmi}k`~b[~gI5X"8(2sz*0_n!$75M^>lM 6jhRtX 7VriC*aA/?`CG@6>[g8l8BZJڅEm3{_7S}|yw'hJ:Հ;z0ഡ֮nZ "qX fֱc 6'i ]LU2&N#,xfcIEcxrZG"erh`U\YV_].0İkԉ-o.o3J/ ^7|gA3ٹ'BST1x"ncTh +i BT>( + )1KjM,ngp֟'AW`zW N!La4phեc1-1BJxbuNWK-$=)9{S(]M7W.e=ssb.W^\RV+N `;n䜶xmn{$jb;.yh=/vm}  X3Bח;}_mN(z"qKO:^l7t}&|=7tVX9WV =]ԡ>~C^M`JAN*:3Wo-R~O~#}/v<Cv^y Vz~1rePuotĦ2# f ;j~`V~Jc|)r[1Vl,v<ޡv?/VͨHZȈл>oƙ;;מZ~,Qz( )~ ٝ3 )j>1z`K+'7|Amϯrw'JǦ*,/`eH5iq'IQ޻f|hqk&-޻ |Ǟ\'[H5e^ A#<{ ˭XKEVg,@P;z‘KQd+=(f)yg +,<Ÿ-m._mM-^ +6 0j`qYBupz[kvC-:CB0s1 JM(Co3t>v]{_mu/Zfys{!$ +qYqUJAa6XwjBAhBYBO],Kz1z܆"{CX'){0Z"MۼQ|dh }n0 KƂ Ό+O0Vm[bO~IUsTQ}ŭNr5^4˯،< \ 8x/\S|I=Y&vlvD,,F}-J&vV1m3l +bÓ.m ?^Զ]/5̶sTk& V +g5I96Rp ݯ{:1Ȍ-`χ!SKƛk·{+rƣly;oSםaʋ䪃OsoCL>ށW +[^68Z2 Oqq +9'Q 1שA68kpWtP& ObgkU vbF'A3֐o,1N`=z /ZHQVZ6aUcqz" %cpKM]GZl8ұeMQ\(7~Qw_#c/Kd9Stf&Pͤ3 3tk.,f-Σ7*N#Ƈ1"wQjL0]~ vQ Ip^b;kYW9Ҙ5CXpj+u +13*45M!Bv_B;w,1=?Ь}1VSJЋއuO?_:^RTڧ)y}3)IGH5fyqͼy3=8j_nD+gtOAۏJ fZavbIEcŞ\.:-ι^s`~bg$L/ΚbQWK9!8m!4j'CFGM|)j^tO1&xJ'l+,g~`tcr2bbJ-%8go&uMo2tۯb% = |a}OБ;l-*G書`%?~ <9Y-{bl/[b>C`rI9tO9P CF}8ˉmSzn!zĂ-=;8S;pg x`gyᬤgv^4Yៈ~ݐX9^M*jV8F9(Z RYdo.`Q\ۥ϶瘝4:s{܋<ȩM|#.-H(eyG^4)Gp9z|.cv4CDjdLVW -4 O?V><[l vOZ?$vfbgeeၵƘKӡMl#2 vVe#;+5;+ifTYގ9 X`R>@guʞnAfR!g _ue }\]K%*׎da#V2'>\#9P /j5Gg5yp|Վ#"~Ei.?Y'&Ko-ln}z^|[ Kf~Z1,_+YܖOʘeZG?/; 鑩6Y)m:\VM(ᵃRCF$R~ ķz<$cod3 +X+ 6y$9^"n:>η x|2]x_RíВǹGRpIՁ^rӭu~HKcx-צI͟mE&Zė4bg=.|3Ȩ7 M +Ffgʺ疩S):Kg( 5_\ +v= +gŰ71u:Ǯ69YKܘ6g|/bW#9<`%[K-77_D̫ciPH |b;9ؿP5Kzň l 2GiՎDnm*G}kȨ |t+OA^šR|=b!g4{K!;fPdq8y /5Kס5x}%8c:qǯZuyjU0p41L*CX br 8w0 3`zQ&W;\p~k+6ɤX4_nr'S~ש=y{bN袮1=bl/%4ol=YSO}`gG.̇T<<[-kzη; *}/Y&v|ub#b!e:ق|X8qs\wi,g拇橍7I'H\[$3 λ.r|m|;Y5WgxOjŵ冴BH\sv zG<"m=ɵ}NƢZR6#OhxcbojkHpsχX2uNxVz]l[0h0B<5(#i!e]`nŃ1cB}sD+1ٶr\(aĀaT_ bGR(G\\ t3z"Jql0EuE=J~40iΥu J"!r1ٽA*׳j4ıA^T,kOxi}2;>Z:zi)zůUF./R5ؿ"FMnb|<,p]Cu[~4*PCҬ,SZJSJcqkvVQ,$XW ~n ՂPW /.-[l`6Ij|rYU\Alf! [b5{P=sypw7kdj#]OCވ vg{7Z˰[)pG<׊2r 1{C bۤϷe i-(g: S\<͵|oY";Ig#RRQulÙ[]_:ye~1UX\!Wҁ3, Gl݋g,?guZ' +#qL| @}{[Ŧ`- ~}sĮpS= ֿ|ufrV\1^%xO8@q+t@^X|!8 +g2 H?l'1!$ݍ崊F>{)GǛxo/W*,B{{bb Prrx{=3J ?KԆ 8!Fe*X6qR>&Ku`?-2NLBMA^^݅eG[ f{qV{l3Ex{U"|o>OdJG/.{u`X_ItF*?P*ay-7=;>7YM{:),vv3[> 8;yl)Gns\ǔ;ɥCsT+j ps 'fԲ84QfVWtuv اp3eE\i&CnT1ϙ=t3zk1*Ffڀ)F(e3$ópC*TfT|%IVs xSJlx^I/21|fz=|ioWiG6A< Y):͆liv͔8#Ggڪ!_O l}spv +J '#X@gO9#y^Ot&0+f[E-zD>UyEτXR5?8\>;[cJ2vX}6j5B;p +ﬗk`xb?>)hD)O<1G'|giKu;rj>Xt+>]F#$밶:?W&AX_#k eAq2b,MvZ>8?zc _um|ν}[VkO~]w^<|Bh5C{7z{0Ԟ^7+c޼A{FۊGsq[Ǿ 8?֋}oK|žT:D:/4}3.gаhE]`@d"|y~+V[y{"߾y mlêM^r,_z(|}׬ܰ_k;~~9F +mGp%xmog_Xd+}*gu=Ia=%g]By3]0ο 엯Z|Fg*C~y-{ 6?V++G/wyUod7vٻS)hB9><\ow~YoVhCfk@C!ڠ$+5|f}Ƌ02ƙ{#=.|4G12 mŶiVZ?A $t ZK_?3t;gW +5H8O+!֐91UM1u1KyvÕ0zaKƨe&P0*T0: 9 [eͥxeKfB~"HߏC)#"{C<+bH6!8Zqch)*gqCK֐r ⏎è h͇Ɠ 乤LRB^U7DGffLTO-2Ęf&&ZAv{<FpQ\#H(cR08Kn"$؋ 5u:G!4w!Rl(:1M8&+=1cN8ZOG!~aKN1Cb޾!f>3U9&HzQ)V>HA#0#D<~d+2{gK6:C~$+0DA2O8Lcא +IHX _oN0W:QvjD TiNR Ҭ1+e12i`[f@=)jBIf/qݧ̦IrĘ6TTv]!O|/FI %^[m8rzJ2 E>OH/gbvE#^i#11$X+&c줠6~ht5dˆjB8%>w4~IfgTX)g(:9kM- ++ꙩM 1p.H匒s[a FKzg+Hvd9nIC;2ۖ2$C6clrVD%1 7$_W! ٽ=b '+(+A2v[~@^dx0j2{>I|`bC)#`?p >8czd.[ a96ZH:p"`:zݸ9!4N4 m Y/R:f %/rj95B2M4>(12-EYMqX.X ѥ K%@)$$KilI?ug&phib90?)Y`d7{/ը;!D9 |d +,'mF MNȅ 7B>*dd!ɫd<.ΉƪII|xf3:6Ƴ!MZ`(蚥t:cFzź%Fn ͢C Nt5CHXc^306~_-?ʐ+.-d6dA04n1żAJfk8XLrg̗kk<|?aф(99,Dn'!@<٤*S +FsY#@ gq|Ƙ?1|ȉ<qx >K5gfWy)6PojFZo ȕח ͟oko ᪫i1^Jo$y{dTZ)[ ?a;)5uWV`l]Q,Cbdbe",~fb, _#<Kxw|lSkʑDLvx*߰OШ5FjqM I ѹvM%(ܯ򱸖 +|IcI)X@6LN_X3!%v(#;΂lIK4lTM%_IɕRFx.G!B 9;tG|=1NΆ $a1̅QtΔj.h_hH>H`l(ȆEsr{THw8z.mD ~[p a6:`'B++d>K!fZ˽U*ee^5!AH.jŐײ 4\9'~t\zfXYY͓FLmpsڧ L$ρб: 0ȹqc̑j2#!#@GIOvb αkI ai6m>_{c)$aCWC fװ5zVf%!/kPGkPVC  ?}f> 16JX_͐+Ɛt)K.$RgIp.,r8Բ$hބ]h'Z^W- scp~w$9YVKa#!G H#i%u8WR̀w_+~ZJvÁdր= +~zPX-}b$,'Ƥ"TƣNXST+ZH֐:$!喐 Ju%$e0|-YPng>$74WB&1$Tg~+_~%IKl$1{`4 tww"^[Bd_P\=(}! ~ c:P2*},ZyXkMl-}wg gM' +Ms)"~[b>R HLwbkidCY#9VWGHr}O)f|(d_ Kzdl 3P~z!տ9mՂc3Tk i&Ŭɔ/;m#,Rr83IeR>@hxj7.B?|QrrD(Zo@!_N|Ap< 1BgInΩbV)G8rmx&Nn LLr,NC]J>D\ϙcT\Y"\.d^^Coי]aM$/<OE}2{-,Gy/wtkt{<@[5~ztMB] ,N 4²m#c쑰MH?"5.^dۥ~gf$Iu` +"#Y1$[1kdwoz)dMByɻc3?A9$QO=#Q??Ǧ@.}0% VCqC"ӏO2II xnPzm9p|rz䂸_R%Bw;c63匶b+w%jpYSuwo;o>@R,ѰJN$r0 9Dju>j[qǿX~"m1VE눓,oB=Rh{\ >90ra1bbדR\ L˥WǝĐBNfLo1k&p̭ 5ELjx%4{BL;kW|T24zBG@).oR8S#?+p_"BA[u@2ddD؛R$? gAd:+o'd4Vױ$LhXxm$:].jET7ggO=|5g,O[> ־BB.˳A<6$zg!?BFtOi=3bBl=bzJ4O͒sO9#NS-b:$΀BlȪ8 ࣀR[R:F9P=ubWc -qπR?]oב5EN؉mIb[JY9]D +vYxgX}})@>Sj%$tpw|pa\<9J[dQY|&^Q#B@IB?$fr˔3k^,^#!@0;k6Co}JX^J:ko&|!qo3s|׋,'1"XNuJ=\;Lҥ܁9.dpFD"t;B@NïpJcn?ȇ#!'V:@ X@ƮL=W;rj>p+7 |Vo,p66ې|7vn!JOH\6F"Td3|v}GF8Z G#9dتo/Q_^gG$5̑Ҭ`Bx9~w۫赡pTI7) SKNqET_2ڨ.Bb(rs_/#@3X "=! +[#?T=Pۇzzn4\bb2{3 {Ym>4%XK;#rgۦCNUQ9v W6h+8]ŏ" UC6Ȃg O7\d"ތq!#M5YEJCDy&TKթ _l믮 idb LR؝ѯ=,_Ǟ|o3ћ%: Ri xj[e$=SY P1GFSg>8[bW@l>,c8ckW`kXGpfy@&B}hKS*7]3-\ Mېwm/W7EYE87`۠=}w }ɐ$`Iu52%@KA} (CiZÑT(SP?Sj'$FR>A*1~]bJZaRoo:[!Oub=΄DOrb-q6p .n<5.BoL A2z{"p?" +MƘjbp5fv$$[GS[tv.]L=0\w +߻ﭢ~b~=XRbh?a&V1"i$ ,Hv<:NQ ̽}5^|47CèJ{EE +==4^. f' }7~r`Eb&@@@zb&rr Լu!~qckjpExA* :AψԄp4"/6.8oRb́LB}(Bfp&\G\Dk\Df+bOy'$wRϠB9ӏMT/# }i9}0>ب,6%cь/K"'.G:_=4Ĕ +{?_'eLJ 'Ԏ}j0s׽ݮ7W_N;G; E ist} +JwJvҏ;n~=Zqnل|F~qåG/d!}XWcBk+2;E "80 Paz-8 # Nžb9(rE89aΑ9΀=F?S`DY5 z2S0\`::9;3.2V]Y~Q&[a +{Bg.g:W+gyTJx!*aau IÇfGVT`Ы57MW+N/^xG̰վ@Ag G!ΨP߱fL OO51Wt~l5ٕiVefssm_l'3&T7]KXէ 3wq]E}bأLK +7YLgpgv~#)_^@kB^V6< x!)}`VW 죢Dݝqb">L=o>C) )#lkv4ѢE>$ĕu\8˥壟E!gNS0 Egة,T<(~/ԭ襖[@F%59pvoa\]T^Y{=kV}L__3ΦL諒| nnBŢRa3YõIlmms.^1?Ƽ΋T.;Ke}^4pG8ZaG7}MrB8>0JkwB%c*a['W<ʪOw$3ojEhFQh'Q˳Y~31-b,3梳m4k Ɓ0򺧋uN|kh2Vכ똏E^~YQ'`p.E}wJgn.mVy[a8'W5R@X9ru MeWIyXZDsrP0:g^wEKf.:Jo=\諭yĞ3.Ig|ۅh*4+G/@?Mw poh=_[Xb!, +*.,?~}b,|/['%~ + p~A3C4wf7g |$\ayĭsq +,⎳ߋ5ʼnKguNON}p/۫_^{_%=W:gzy'qϢŷW]Ov3Feء/AgW5rͭRƓO{{\@]B'u4f5F]w<ڊr;mjNKC{+v} &ʈ\p=Ϸsw/-v/|b'@sJE/.g< +vw +Ͻ亾ކ׈ ^;`3v|gG?^h/}zޮo{}O;,(֋P+Q>>.>.<++?|OWrO7jCd8pQ réǜ0~ j%`p@<&+Y^lyل /vܥox%wd4n]|,>O+U*YNx';hO NF=(1_lTo,cG7ҷi74](89+:ۧ*W7p‡Wꟙߕ/-)NH\;㽻Cojk www{Ib--uJT{)ݕ[Y}y3'{tM~ x###lEvl֧+e +r{m$yVڨg#GeA֮Y`Y԰냚1m"o؀o5o4~dG$Գ!(G1zAlcA7.x"=찤v>.Zcj/ž|&Gn*TuUչ.s4ZvR\P'!:LȜAۨ(0qGˏbAрѥ/QnNukq-L+"yJt.;gaမhH"#vBa(`Q up !-HB2Q廄=¬D/?yd7!n5;\4 F`_N_~7Xy=[pIoKy_hqs]G櫥xUaAXrAbgGE$X~j~>AQ?f,dA?đ\k[ܩ1 A&գC![ګG䊊ͅ/_ ~7}OG$wݧgC0%xcE>F!ލ!+NCvx6ofW_x' A]uES$v# +D}I^/A=[PZ)2ޫ%bKA-yZ}\p$7zMw 6 n WC uËzxأ~9,!PeTuYYY{1*"(6/|0_pC@m&[: F*bDcځH⭟ɖދ!< ck\Ts/y)hݰ{Z^]N&]2Fz؝oFWI(U8m0}@ӉY+u1*xg^\*zxB|](1erkCzN#`G `N[vm?q/1K"L4V&!?H5:|VEʕȊ Kxv<>h=ȇ2 +h3LB4:t ߌRl95Qu6jOc׃Gc8_$hs6,Uu t ˻ΊK\ĕo/ +:΋_5979Pzm7}-ko+OJNCgfz?/*q;?tOiSt%P_)] w\ ]\yUaUb˯bG|&k (N~8C7XD-x\nG#y٫GjIQLrʬނ|-|`)Um)擢']v=VXw(ze+u65xKXPju#h$Wц Qgmљx=lNUt:ZL8};š#+|KjcSRtxHrlП Ml<7jH]1D .%ɒ=#M%!6{_']-Xi ^\kq&f l兞z_wI;ߖm0լ<5N.k3~zSh\,y]{"?CM6R\lf0lmӻHRf)cq^q~ bh6Eqvݷ#uPWd m~ &}7xN6Q[)}tIP}Wrr,NWJK>?ɲdmy~㷣d7'†*7Ӣ_WvprFPcFߏw^e&%6^IkH=בj?Q6Z+\;T+zaٶc]Ïcx ?rEu8?'z,݁rW2w:ł0щ]oӻ xBn<UYmSdЂ&?| nWZ{:~xry*L1l*ֱ-55),Ϳ),5G),L4w)Fe\npKs37rMp|hEp\h - cAr|BU]YUAP7;Lۂyc }G~kyiŽ Dʿ?찺c9&/i]vV.εe]/A7$Ysv2~DPVsƼM]$'qǺ吏ovmB["bcxK679GU9E_.ultkpk\++bTy^ +9f%6X?b=Ȼx!Q ###{AUx=\H}4E>D=h:){{m&8&J]%K+O I%Mu:^չjͮ-lt-mulH82P2LaN;a?y<𨏕]IL^Vp%=LKF±\d]3x-R-L,x5~FXWŝi<ՑrCAЋ8g =W_]ȡ_#w⭆IM{cG Gz|Zb̚/_FoX!O\ҔiXLk~C}dQm~xR_s&} -2Fôp&Mh+ӯy[}>Ӥǵܧ?Eꃎuz4e\N^+&ԅ +ZڼWasx5L4ԋu[%~{:$A\&(I/JtH inI=ѝH AYc; Ѿ;7ɱC&Kl6>uo&ڊUJ35_hPNkpR~6$1W +V4*4־jmXrdԟjHdӮ ٿ!vV QOZ]Eqau(ZF~u4zN#uP6?''{5 }`n u!i:غ 09'3urNſxyVEf\LIp8(>_64NCAf1G d0/J +J|(ˮχyCBM y7X] +gmw8P?Zv2 s0,*`><'[B` rד*?H3`WT9`|0Wq)X8a5X,_[vq/&y=m-xL?GrUO&6VځKsWsV6Sd0L1RR`Re)8l~E } b6"R8"hzB>NT>0.뭿;?)qK{JT\+XMWLIGLfo|\m`RGc%᳡'z3bXǵѻߨmۯ֬^ +6nZ t,N1>Kª%K 0 LPdJ] +.|ߙ[= qx_xOlw˽KW\ M >a|qLH|cpSTBW~?:dv ڡ2跍Ji@T }_oLkпD=0 q`̍`.kX?; + }h#x Yn\[r|㠏yY_.|Pl;؂2/< xwNP귶`϶#b:bٿo8d N 0/Dϧ6͂ r5&Ǵ-h&7 O:Q(;( |kיQ&snO|&]GX?U3nȗ>~Z[+Xq/MqﴋAe f}CmG9WY`pا~N?v=_*8o&>)J_O»W }"˂>QUA 5ºCz+6?>ޭxgE-wW`=6q9 >zթ`.,Pk-u[h]sRP=К2TW+.>^lKΗ+ck`3uJmY Ia~!Ҹ5~Ѽ}n_*vczfI>?=Og['2oΌ``;JM٦^ԙ+j͌k,M(NhcR7+#{hOJ|SJh651=U[_wo6MsLD`>XvR]|mmZo_N{e@7o}RfT{=/=(( zơ÷6<wiBԺNama?iLlt2ϔ f+,qR>iK*0]q%̜̝֫yG(iPCo=a"ń8ǪPї'گJ{ +}e #נ%Ҏ `oU|%zW@t*JMVvy*{=`.0kV0ky-=.,>_ +T{} UTx5{W(5F[;!b~[gB~r?}98FsIk[MbfI|-<`r]K`?_l 7lc 6Vmj2RU>OjyR7xٛRJZđFWh{jcg +I L1C:M3{TL0g<`*0ozu`MpU`am ך+D`{0.T?S^YNf}i B9>-p㺫<ќinqK_ kh&Ъ ml+`3ey@YVHp^97Ka |o p:452],i~@aWdyѧ_J-EiKz(|jeUn1q^iIؗvw- +#Wls⟱'ʌU/ gn7MzZ(n +z?n{p'lDqK]ՓvtL2eGلN|LolݢWwʧ5zF-4߸}!1,]0wNd`` OC|]}W8ɭXsKyf̐_ +`h@77N;G/R{ ǧޥYGC@hv9 +[!`$Nn CUuh$~]}_hs1GK-\d׾Y/#8'|(~?oT?L[PO?[Ē vgNRT]0K{h=4 `,^6{Nպ.`7l?6v`Fv.ظ[vcr{=b$ͬ/׭ iS!ӶVs%޷o?\:iJ?׍n;Mh&uN}hz`*^'huVb~ݏcT__}djmHe˗RUm0 SeJ`nj)خg``HUS6S^cgzcֺY .?ڝYtl4;-6J[Mis c'ن3. yմ5+syiSwxͤ#weڲҏMeqLi}\~M߂ODOtϧN^SOip7`{7g Z^4n+mm1N4ZwKS4+VV+nEk(t_{;i]v;mOGp}k|lVgر]Lje1 ].)zH` G^sQ5 q;,m*< Xׂ`Yn?3ŒB;>6F4ƌ+hN m]Mjb9MÉmAoGڙM7c7'h)7iYx4F+aQ/6b/we"quEP> *mLCB>h'-_U4?ki` ,[o='n+\3/֪Exo9kZlq=fdm9*8UDZ?Xa\J +NwΥ\!`Y:+rn0v!|Ui+(^S/Ҏ*'t^-GxKf)Øq 죂4<^N׮Ս> k]7_ۘ1 fUm,sJme^;s2eC>ZZ`xf7L4ٻt5}Y6sJTᙝQ02LrB$!w""yKIR;?uys)M΀ev21ZYO. k^Iz|F᯴G[KXt'l׿ұ6?qU`T]Lca $wnrNΓq˶p/OC۱]pOlLK/gdUr ɻJɋ ic][ˍMAf)NerΌu`z-t=^`&`}yҔq?-4&wCk)Pq)V> ЇX\g|p]=n>Fؾ`˓'^m~i>'fZExm'u=+ŐwW~|e,jnFŠ?tx!V<vg}/l'5͊$7b+7*vnVVCs4E^[%`57`eFDnI }X\w[E\$ϵ9yLC3qbdg5d 煿\fU(ǍEͅɐZ@a5"<_b!O&-%ln祼݉'>Oy >E=+HExިi1Z7f^c:h_;xy_l+/ +Tx.*`=е[@ëdN;m8Hȯ^74izx&2= +1`*?z?cɀ))htw,5 3{vo +s瘹6M b]XDpravUNJOEF{0k^M" t3G-q~!lW7c/jt4k/2}/~._iGNҘAB u%3InH \T:OQs 3 k=Ab!O` EV +֎ʶV,9Ff@GIhSDF7K&qxsqrך n}~K] 9,*ܕiK2^?g*-~V}DM,.j݇i=ɬ1tͧoܬXGxEߌMKL`NC[zi 8n&w&-+|@#,c #!8G 鋁P߸E#8%5 8J,θN3\i៷ F/D{nc"^g-Gb~Gu|攠;:uqq9``\LQE+7c)q杜Z7&Ume_UsRmZ7iH`\W\0ʺvpUSffr6r=ւC[wc8p; h/b鉝Ty7k +7 BpG`17 (ٔG-y%hrohao{ͅOa?s7ir>h 봠wMiiՅԖ=ċA]هf:&T5Yv8-Ϻ6ô=]Z*/(rc5`>;iyâ3qIKEc,QZ}vb_T#=h儴qvy)s)j+~wqs˷)s}焾\;'녝Z@c>0Zˡ}wVLVNUg,`+3e-hOMY`:)E._HqEE0, 2g&4AwQ OG xXJ,yuHw.vL{]|4^ m 1q?旻wo"?.K]u6`#νL ={>Fᶞل'13-,&v3ڷBAk3SL|0~"#Ë6`iMfyaxwYĽڧh?ȏ}?Z]M<ƾz~9`:D"p ?gT6XXнU|iVn~j<'XlYغhkkcև dž0d@_ 9cuJ؃ȿ=ڈX$n%ql7vmt +| 0e􊙇# jƁńTK]Ok?3?Q6T x ^<gw`H:,V}@%x86t;N94(Ƌ; +y1}U3GtyBZ^|y'k9TuHTŃrcYFu2sBoEk⏇ aII~6/U􊝋E3`.? uGvSL)V9Vv Wឩx/_HT`=wCx"/>Ó3̏ 841S~)DaYUN3(Ex-d͸>Q޲ŨYTHkwEƫ8Iz%c:a=x~זKlDdh-9;6w>|ĨuI#` fo)f-dIɔg4_xxGXΨ?ujvG"[ 1{Oɰz.O>:) "ĝCqnvwɀWȜ*x(P%7C#y"8,*hm}M0:H u.xu9{9bSR~K`_Ρ.B};>gv^kuRivɵVb CSQ϶#}2k%=ʰa%eq?x){O: `(kQz:"wPǢ \%!hEKgA\#~.;Ch/.~#oQ|)DF&gbw|~PJc2Eu5f@|cq/8:z(HW X'yPvDp0~ Do i ؼ/b!+O_Fx/ |a }&CD"`ް>rI8F*f +NNOLa FH7'gXݭ8obF(9=H_ !]^̈́Ctp!n D-bVNP͵K_oG : 5xМA 3 +ϜB&/By vm@ ZdH (bh`'=&bi'15,9MrT,Fq"NzOFXOH8\ʳuT3 i'bb!+nqFdH}}RAV W Bn`X)w`q/1"W{D70{ol8gQXaCe0< +8vBT l]!VN#=f>A){xYpLX 7OO1FzSsFl+7i<eo ͒vqɈtKCts^~q*VĥCL%LzQ0% +8׾' +V = +sʘg[I(S[+PEu5"bcg/OE@| IO3}'9+ oE6I@>'Xt#XB08MZt ('邋A*ˈڅtP[ts um9b!"6 q 냟r 0ZHO5wQ8g|0?ucJ~J^)a[h/oJBHȑ.Q !=dpǠ3{FCgm<35?&p6|3rGpJH)s^&Hw80G[#·$QNX1TеnWCDj>QN<0"mBHOtoyϜnJ6dheovX.xa)(<q\ s ˎ)N10>/$΄MGkUkT#}k5;(UKsiC +mB1K:Ïg᳈kD&<|V@|Ȱ 7zPwi]ULAdsJ.@h_=Ҁqg/2:e0 ݢ`L9qUBBL(6O㊂3gg\LZHQ2tPbxUE[Daw6!.Ҭ"NC:R~ 7o9Et ׍5dV3JX8»Bɓc;$TaMDJ"'+! 1D6%Î9c3gqa~bcbS +b|V0z3~HtEu b#.Ho8{e:q!F0N[$t{-ÓB뜱juE9]Rt;K<9sωdz9],,MrO3 iS[ausֱL6m]8H@h*<):NpR2`S{V6:i)DZ8;E+Wn$f"NSAGA1z-bf|) ŐKޏ4ipnN-~O> +f g!)F i2K,RF))Åy%)!\rPZSh k⃑ ȴr5<0Vb^< a{G//C:␂u'6p>=i|y`raD;H yCFLn`XL7Vv㞓08p<W +{ On=(ڃXqHG ZKCl"=F=u>陱ßoR:zvcqF1^si! +fQ~Kq oLxg/brɸ݂ڂnCqV<+Zop.o&-{EFm'35UZ609mk`[NMCxZ!2gĈ2Ah%L? ᙿd1Fsu&-4i:'Ž!-w#Ǘ#Xr$m앐F(F|2M 9 Qv1~+~>S1~)0&<2 >_C8g-5rb2`lsH857 DĥlO*y-x-x/B5n SrLj&m!7 6U„7(YAˊL]Ŭ_[ؒѽ63~5Ra!xg}!3O,,ew9 S'}_Y(Eg?WS4IW쑶X|!Z1P% ZFs[g\g +b4% +eUꌖi׸Hظ0KMF正n7qk$/O!!`?YYU5k&Xfōjʌv5XCoGATzO,an_F{87M/Zb=W +WSxVNJA%BeBK7֠\1<"p=Cd[<2!?1H;$"ҖS'qy&4N8azOaWrV +i@8@:HgFd4VmcinfOzNC*+Y+bq $ˎwf 9NS\ İoaڡ'o]kQtH,!7|[3YB!:2Ƙ\vVhۜ󜊴)mxK%Jnwh3v 8CBZ;"p1|6ʧKoxNl#}yqR KGOHk9FiF> ob .եDjy w? ;ݧ+.갲*y%&nc ΃%c2V sm+"ֱ(hV(d.DDLǰtm}nͅY}.ahgK;+ig{'a!bƊ]aS;<0 `m>)kyR _f +b-hǐ*3x01k"cv>bz37قqf.@zufnQEW +bR_7Qen0g:?Y`%g8Hq 1kh~ʼn?ׁ]q)H;JtmOEdnZvpHi~3!f5o fd6K2Hg1uSVp7ڹ;ߢڋ.!iHCS䞲P蝽8&?vq\x,ظX!ދ\k|n}q0rډ"z:0S䕸HS +뉬6mKy0A2Hm:vZֈPLd$ÅV'׉b;B.FZ܌>g|w"?S.Qg.`R O>X"C?^$}Gqg0zF- i q^]{Jr W&DfTjIkdt埵 0ڿYWcV-'3vVz A| +gk,W^2S;v'Q|8X;Cs1EgÔQ|7q1gvQ:O9(v\3[fNsoxQ@{EmYV[ڜsh`N v$I(9)rsV ̘x{孱=߻~J*k9ǜk1&sjk.S\]cog=S-CSo 5M# Xh؉~{Jg/h%B@yD4S^#Ml .|`I-FԚ12V堥OSE}tTq'4ĺǞ7BoY+[jB;K ,bOT;k{wW0ig`KVhl{sn6?E|_9 Ps6}W;KOR +:|sSO`W!P;BQ@VjUz9Oh'LS4TZDT䂦T;"/N{˩vVP> 1%7M>d8puSmZ~2{v>#Dp;0/CƁ$S!3k I!8[9iХf?x҉M Kzx'I^!>Wh~yr{_J۾A \ ^}6Z7v}cS9cڢ)%G>5 Nm ' +hs1d 6縋RB4hR\IK|.etVUT; .>,Y;˫jg#[$l!CokJQRʾYe,wS}kg|SKCW@ɳF ~i{흄8^(m\Qe 4i- sh.S3/<=JgATb,/5qr!X>{`0==Y,jQza(0dT =t>1Y/As+ŖMΖVϧ)f^X?2 ڕ{GDZeZУqaՍ.k;Dq%;|,YZs8jgA+Db1B;+G;K>˿-b G&lfs-uǔ}#DCj&KYgWXs=DLhƗԯʚ)PJ91zvޓpj7o4k-6*W /N$ǢFYBˁḼjg/ڻ7j>G@J>:] {k,L_Cf$_f 4P7 QePXhw!BJAsCZg +3~#x 0ĖĤC3A5޷rGrw&"FUL77G|p_R;մ%bD5P>#3P!mSࢨ2}{Aח\'Ϛ|YiHGK#*i5o\`~E5jB2_ys@ e]ZHBeeeszJc>kqY%@ۈǜʻH5֥{>kCT;O?:˿2>,fg:>jE'X6N9%\yxY8P^R{.%CM >>uYwCUՍflej9IAXk >b2}ShΜy蛩grd aeLfIcabX{>Ǹ.6b%H>BԠZooKT(Es ,jQ6p?Ŕsc3QGɕ5t1TBzIU8:kmLHkVT5LE̩RСVc ܃-YMB14w 4s o-.֛iKd̀}"P2X ȿX}f.t֑[Я*$P]=,SnG#ȼL>'yB s'CY>8o=;6BPuQB/.r"Wں{=Y."YǐK=a_{]!/ +>M[i!EvnؓZ8t8=9kk`"#l'v5fa=z爉5H`F3| "1 禌3U̺19?)t'sDpA:t,~8)B-?B2v-Э&Fz@V񵓥0V>9+W_Yuz& !JbYdR|>] $R<sW&/\c<"r.ɹc&S}8! 8_ BZIUć $9@oJtm(J&WU#K3fr8 ]w$fmmCc31kHYg@O lIQw +b;W37p$MJJA ;u8ci+)o40+Pn :ZMOu^Q3!8sO;㏠}J|չV,dȽ 'vF" ɁYc ق K:C?:1CZ)Dg&x{i};ZtT t Ñ'As3~ ~A/ȽRMRґT\3W:i|DEٙS9sڰr' p=~{nynt*RLevF!q`:tHϛs_/Ǘr|9_/Ǘr|9_/Ǘr|9_/Ǘr|9_/Ǘr|9>&O^vk]nU[g̭1&OY]cGm֞>ܼ9 5vџb}*}OGmI k9MkOlci9rt4%y~tuf/]09 -]tys/XT<`E .wϣ???:6]3]eӦsG~OѷG=o5A}gWۑɷmvէOLםJWg> 7~2 9vϟo/*ے7rǙ)͛۞'6D[ˉְni6]knoɉϦ?,J$,n{7}"r󿥷m>6dz0/Y-M:7̺5F e40hȭ5l4L56 +持Ș˶jXF>>NmSCs*fMyXa49k7̘Mp*jm6vגȜtL| -#- ,lvE1ehfI|n&՜147[ט +{tQ&w*kp +G7ٞ8}l*#ǣ}-D_4}>ukܦZ=x MS!"$+tg; +Kg-o HS&Gmbfajnah*1@By-6t6ZZ2r= +rO]v3>[۲Q:rompĚj`l۞2CH`Vo0ak[ؖ<ߑ:s 4Uaڡnp[i n& ˝X{/޻_<@l/a߽\/9M;mX4FZN室੤^>a?ˮ~ +]%##(wLL?H<ۼrSnyKߌ*]wwpp7PnaZo#pZ6mʏy @_*0g,E~+)${ z$Ku{oў$b +m8;mp_6vFw2L1l=5?;G?#xC}hKئR6e_Vȯ|Pg>BIƂA}Q)AuОȡW`iW_S_2n/E(û74ׯ4 >r=cΒ\pג d,wid,0Ɔ\jαE漍\ŀwZ6X"m5X 'MaCX? 9Ћ-:?',wOO_)Lrײt%:F(=J%g~U&6~_> ́;d|tFS8\?=Spg:NQBDO:zrEr_A|/f)SO-P.QN)p >ϞeⓉD璱}61==6UDtP{#~Y3q#f ?Ϣ`sdCKvOVF&EUMWOcD'Y>^ +. !UXD<@);]1ADo#cF ~#(2x]t~\.=!0}6@ù*Td,a["> U=ǀ]7a 63ಒq7**F&Ԗ)5(w|IKa =&2ƌdz }ؘ[=Ϙ[]nq>Τa]?CoiF.hrՑ[yO %a_!9+BU)z3҉߆m >x1'B_1ߗJ)g3+'|0JRld),&)v4"ءݵi>ѕF>#BxFp*É-?dP<7Gn+\sģhqKCg(gPp8|6x{y2dL}{تIVFNV[#1%G >4GbȽwA"ׂBnNo+G A<x-8&8GtZo ~)p*_K/? <(Qu@գ]C.'X@nn.>T14gC'Ȓ9i \.JدWiR'9+mҮ\ 2)7Łkp/K;Sb@Ho˃& K9{A]j=`!pP;B l1\2Qn*a|f 9='f2)D}̴WKm<4"(IM34k6ѫh%~pp_3io)z2Cǡߖ[8=F|9ľ Х {&}{{., xݙ4rGr }@cSpE PA뼽 %Qm#W|zza9$K|#/a.ϥI'g>N؁_(> |Ek@sig-7COL92[H?1=EV}QGp|EՋ/2%ܯT'WpV[ER ..-mJGڃJ8񆍀kC-?p)'|3~0Gycbޟc,p?da3? p.`HCgh~pڻKlTI>v%A~/T Th:pa%c)Og@z}уh|3<;EOރXbxx>,asdb p~A.tW*EN§އf7I8++2}FWN45I,7W{3UӜ_2GrCُ{r8ioio7E1>u2|z&Q79 . +E`hhB $9]h4ICAr2~}9$HF +G_p[p/e8(XEoG~4&=XMEY໇}RY\ ;`+| _C 'p"rh%E ~bbT%0CBԅ ~:~ˀ3_$)g!E~G-j8H>4\l;;j_Gl9zicQN 7O %9rF;e1@}hhrK^|2?qDءTXޑ$&dVx }1'=i044po*#J$BkEӜw y&xwDgg ' .\3ai%v%w%EArG9{ z6n诰e|]ܦej"C>&t%%y:܂T |8$Ǎ cw2!ŭ?:J^=W@/,-'(- fy2% hٱ꫋k xr[Ompρ9 x ߤpPj($ >E. Hn:_03Ў;HM0Ro:[`Kp"O ,;s$fAb==5sI9r$e\^U}bIQ)Ǎ{ kʧ=\Zۃ]ww"^CB^_G@V>xe (68>#vG v[lX2$J¦ ]f壍=g6-5ym?}H1#,ᓣGRp[I^mFrS KLѠy>n+];D{Bp*}{cʬsX-;I׋?#u:02F +YS'p g$9.(}O,߬ȼ"L,TT4cSOv B,Rؒ Vch>4$oQ{Rg2Hw\"S +p|=z(8{Bi4 .'EPKSfKx9b+aꙔ#:1z*XQdjs[>p; +k#w }&U6R>"> o!l޽e),ƂJp=! .~_hPҲF(jXޖFWB "/FD\LD>!&ɧe^b`,cPˀ]zubWSTKiW_ 9'ɟG&{yPOc +$o?;v-FNn+p1^[ IᗁwP=l|G`h[p6cP?3nh KP(Zg5j1J?k<Е`(ˎA8ky >˪߮dGL%gMeM{E1ω|Y건Rܾ鴞D?_upN yBDŽڸҋTGQ=K?IsX +!}|-ˈ)Of*Jȃ˙KeWW]QNu~N3͟ +endstream endobj 45 0 obj <>stream +zj1^|əl\vӷ\x׈h.Xи-j^Ql}Dis~%#; K>?[so;ʭnqF!_G$ 5?y)A`-```a#aߓLk"^S[93 I'f'*o/Ү咀a$;XLP\}.T=y bj6{F٫8MsI76$fCN&yhA+ zZRAjhc@7t^eؕwu }rR|?c=hrv\m^?u`שƆ3~:O[!<1D_ ++O/=@)B.,CBߚm$Wg#4G?|}1:E|ڱ9zB 8s'_| ~FvbZ9A +KukC-;GL28PH3=7vN|D|;"#GQ7)虁jcĖC#=:/46OrQ`s3c@3 = [~rg0Nɏ'ZCG' 08ް~@shЯLYK;H^"؆j!?k˧\K穾G` [iӼjcdjoJTbMa=S;Yc41pm?1{^X?U$؅EDyFL4I{g@-qr.[Z@ fB?dUtuG)&>Y=hk-u*䶔G!§@{%P'BϹ'[^`&}}ddC$Y%t@ c@tKijsglUYTڥ)6Qm -Їoڕ1,RߢWK蘆6i^ ^Э;%"'up"2v*cZkOKS5v{[ٗ2ǠFإ-z !0nٳ ;Sw嵐r[^B36FY&?o2-j3C'6@ +gmm 㨶Ɂrvi}!tEZ rBL`r }Ukdn"sj1a xX<-J,P[AB5On6۴W{wm+хW8~b3+S̉Z'2-r./_$9w5u]G(:*<9~l=m|/-$V4nTVZHU/ d Qm 62Ag04dE7MCA/u(U 4g6FX6jcxf&ŻX&qͯϽ' O9_H)TZ~w*ԥ}1l͋Н9/8/ayPPt_o!WG -S?${JA'xv٬cgؑ6i.9jnXg¬Zٴ1z(p'[`xw:Li>rj|F٭^yKڥ \m)`Me`%B7:xA\d-.]i#('?\DVMR?K˄kKUeOߡ"EL,{i%r/Gy,}{&ߊ-ڧʣ:6Lַh$>T8k68+ 3bA8|&Z֋L  ^Ge.T= C_zb5Bڵt?젷yЍ/̇6Kk`ϑ*2eLde҉уiJ;@;c{`Wi \;5ء,h^'Гg(+'fOu6:tPeɧk1.6۪;CpfZt/[ʪcayɵo%շ@#t=:[#ߣAr6t=HnJpU!t1EVY؞:\T7yͣ%E=b%п+H>VOuHJҦU|y:š }x~uϠx} oX)m`o}5!h a-9:ji$G(^qSn5zziS25/uɃ/)Nys ݀AΪH;Ҷ*h:{Wz&}bDX #-ÞpJ' kBTkX_!cb9<"Z֠~WɣM.fv_NPj.@ꨛoxAEc>i#qTet>#Pk^/{6ˁ6C`EԞ䂃&QJg3yg=PMzU5phM_@V<t.2& ? XGٲKTXjث E}б4 NƺCȐxÇ g#` 4PUI f`/-K`fjet`eUV= +"`!кcarLO};rb̯S?} 䍥s'@/Z /﹦os? -eLf}G`OD.V]ZD5GBC~6tPI82]wy<|Z7[@ԑfYEWAa8.bZ+T5Ok#\[; gCuI^Ab:7yH< ou\4Xh.}֠ұXE =#Fȓ*w"X:F_H[!V-\+](_"KVRcNFT% ao)DCJ99_L<1c5kS 1~z!Q=օfJ|/]G #8B( M;5&uT :"1|q=Ă񊊖\syE +hY +;SrnAq[1mgpzv_PV*/kZ +rn~2/.z)g#g0Uk5kP "w#* +yeB.M!ߦ874S;`~t,΋7]d-˄C6t$jX|bOPQ90fо1jZ&Nc?? +;G睢Qw ^dN={mzda馅|[7ԡ‡ү-`s/\5-.\ŇT=h\a_ +vi~=J{a KKy[a'jQ:?!9wr&{'ALJ89ϺbV0̑AIq$^RZ5Od~IiIYf]@Pp*'Ӛ քqTxH82MG~1|4SyoyƲG_Y;EXXW<_!~b잷oVh8ф;NşC@Q_yU:nJ~A%7X]ܝm#M*nOWz΍ϼ>*]Jz-k̷jx'B-Q]hpvY +'ިDercV0k.>Lh{qG%TO5b1b3&kEϫdWy+Lovۻe[z0Yx2N򳛢gauȓ?J'^|`S$d5jʫ_jN7:U; ;9+3v #SV:f-j8G#S3 W +ť*/v.wWn\\ OwؤFT{.~+uv[q=LCkݼ^N ?;O CsG!/Ú&">^e`NP*o6?siz.,mPv'N~=kQU7K,].~ؤ[mvRݼkY;lݛVK[\E*l@ƒ8WD0)`8YARMuڰo~ +>M>K>=O/|jMy˽)OuzRvȉnH.ȼJoSϷ꺫^w&+'_"ӝNVjyKmV7t: W^;?(s$nUg]T \plWnUkj#R^~.z.]k/g"]zLUytɃ}zu?M9M|'툓ڒ'-Ž^#c5^9թ앷V'|ǃP[Yo.:?=XQQ`Â϶|'9ﯥq^GZ~w1?7#Đ+= dƛ}\٭4o$ڽ8kJ/UQ&62dX^S >t⑍͋+9OxUV:v*|\P{>]ѵՕvSN~6\^ݨ{Aٖg(?Ly}JHMm^h{!ٕw#r;rn^v|z0O)UzבdrdfEodů"Naw\[+O;|ë hY;ۊ&D>L..mˏ~\쬋e=a:5,q\nfkxnus%(@cp؆ Iq9ޏr>ſ~{]xo<ꢼꮼ`tՑ?;ďm }_^ƺv-]ޱĵ`Ń'g{(oɭnSKM*^-6qx-rl.p۸{<~vM7[TO֫ O>;xW{E:?AbvԬPᇁY׷Jq?^OZzsCoƽ{'냲òS[+,]I߷$ڼ:?7aՂ;FO"hʕN +/v*ߴ/;#KÅ%˰zӤ:_Z\sxE~7V9-uxKji5m_^Qou!o#vf=97FO6Go:A]ySXN҆覤<է&݂iƽ{W ދw[^H7;W,N/ܛprlޞV=v6>X~=Ř\64uqƾ%CMJ7kx29ҽeݒnn]7gnO?_ߚ?l-mnjz{R]ՃxΣA˲b+mg⣦0T֪b7"rN +y\@||ջLN()O|PڒY&ŋ ΆLyOk vv=Ϗ~X$|HP)@W6Ʒ }X9=aޱWm{SJlcb^E]=,,w#ǯ+Hw^;>;R P廁Y'nF>w+D]y'2%oog"&4&査^QK|ֽ&ߴR>m"%1dSgo^/n λ~_}?;=T~[Sjwix{q` [ݵZBb]m,Z#djfU̼E?0d-1aVȃ4ߧԽ^˱9FfgHɿSq#R~=)7`|xW['0>Vt~76DY㵌МĿo8'3L81c 3|E}b~5YZd6ITZ˿g)C2cU]-=fW̨^ؾSas c1ӧ`3GlQs {o^[ݶ­ܭ^Qފ̻q+(V]PzswJsscÛҊJnEc.y76w˛sj߽ٷo2ZڽwJ%sPpn{1hPߪFG yަ_lp{Aޣ)+Is &2z5`tCO#ጾf0}fR3G *0n}{ӟkbU!Bl%9>?Ӝ\Px+:zz57r ]M!7en.ؙ=秾%ݞ,kov/^yukSY1=47E3Okx}f03a^6.-Os/r:UOZ׺әy D;E3#ԗչsd߈ͩzdkO^:s%,BodߪȌkJ*y/8^]yW:H ~+==$wn,i;Jև& +Mj0yФWW2}4ޚO#QCf1s2kߓ;(;mGsk7dVߊȮM|LA)֯ϦGO-muN:.*iM{g׶Ԃ^!Y&v]su63FkKϿ79C|Ki8;|߮5?d.хYdĺ3wo_V=kJ鼐ع7ǻ%?aNޕ+ߌ8sF73C[s_̒>%ϳ7}b[僚3C>p|Lf&L0ff~ kUFŧ^JR.'{zhkygo{zXWe݉W54T7!H}>$>2nW>4|9KQ g6IS|cй35x/ןxU]f3D{3fZfw +zdu:yl9e+ ʎ'_ZsUwCfуu}ҋۼ=&BZgQ{ߋv=*sշg\ /{#4ǷCo )j{Vh.Ϝ7rnG~V_dFsǟ;pM>io/AC1L1WbJ?Tw3ޮ`lCr/] Ͽu= 7O.j#1%wunSա4nÙSjs1;Qq =2NÈOCbxfdfPI@̐~Qz+B[j=s=5 ١ $R\ګd#`ķި Tw{g 9OGVov+O@Ɍ16xyKid̨ /gF9b)7t13\w3\fh9P|3@^YjYvo4ݎm]w]Dbߓף +s'raUB+ߴu4#Zнd`Fj3Cz$? "OK3#zMc 7>`3jZiarm#t] ο}|Lkao7 +jFH͛Gf_'s#TMYN^]\BݻAod߼MQ-˜S5'.\Nb낟K=C/}왃: bFhbFLcFˌ#3y6f"{pf1̨)rfxfuW_mk|Pw olڎƸ7'O̾w!ՈE$_~~/8}c`޻[ѭ4f8A {b^to0zG3cOf{WյNRDb.kYWHGzEP ]cXc&c)&FN%9w>ϙ +d932x{Y7Oe$o13zcoYAmf?;[8G7ZPד)n~W -w?*ݛ[_,lÜ~A;/f!%O6ϕxfm86"W6d%ύe&N01v#t#6Ȍ8,Lbƭ*`&0S6f* ?goV_̸㦦5Jr;NZ q7ol{q` Z^oqԻm藎<?="=co:HbA$b[hl +d,f̌[2NcI+ %Dbfl#LQ43sw.~>~~u7vՕ]in;Vzpʕm6ߪnI{r ouH\'1ߎ4BH1e;1㉟K,rM򟭡9qp 'b1dcgK/83qŌLX^Llf23T޷XqD?ޕ/W$pn[n_*gW}՝솿m{4w׭5mLu?}I.lFF˂5[2#h c?x6+{wb~Hbci1sSQ|n8O~N3v sAu̼cF  Zҧ~mmVQ6;w\"4O^~ǫ~|E_.Zgzk܌?+r.$8qc9peFu'b%c7.gyF˘Q#W02a2όL(af:kN+5pnS%gcu6rU _/}Lu5Amf4;4Y"Hf('3Q$OLb&3*c3_Iy#Rƌ3標In)̬:f8h^ז׏t{_~U[}>V觇nDs}-]ַt\Вogw7ou+ m7ʷz'fJgC{?,hoA"5q 9h2k<[̈q~J'7^cW~|recĒ-lտ3o|)nAѧO~VSmA=ԮW}xgyMO>vQǽO [ٟ}jئ~(fY~Ldf +Ljdf2Y鼍/d̳Ƴ .,eꇖ +\ueۃV\u_l!릧vU/j'zIVqwG3r>;}ѸCU xڃO$O*Y+ 4c>̞]93yU23dfUlu3?˦{V+/B`Kk ]k4[]8,?`Պg6n;~~OQ6]Z߷zɻ3OM.>6U/k#zM_Aӿ~ N܍|,\5FFszfq$ןnjqT0㦇0N.q$lfZLw3<)S< %f![k(̀9Wux;zwWP_ <Ԩz_T?L/zmﮗ3n>TM~ݒWlyq/ko굪/~[+~N}97mwx;n. Q܃3r6Q21Y3Kڍ7]c4 +:__?Sn'YЯU/QUY!eyK}wGͼ6_*o#H^t`>uq싃_mI}ܽ+ˣF5 ^iYn1aRZ+2nc]I3$]ϸ_2YK%z{b{sGUYD~Ƿzdo޲VQs-w75^,/=ϋzzg[zO}>Y508m"ӃA%1ݏ]KsչGkJE}N zSE 1OΌ4[ o4"yɱOa&,e^Η0x_IG="ྞ g?UoAdby|ˋco]}> ';/~+*w|6m8?m?~/H}F{Z^Ïj߄oW M]g O.jU'.eN`̐8RG/`Mcfd3.q]nqB'Er]'/='~uM›e_^d֥ʊ*r^>?˗WP3aY=*i*,DhzH:ZsIxExiӛAެPŃڝt0Oa~7y&1clXhVpa/~?uA?S_냉I~m? >9}Cz1 eRMNV?^+IˮWq?)K{W}!}>0ys}}}،6# (' NR/ˏ3tWe(;.U%jOj, 7™h#n]0CH\>bj \};~C +߿u߾~Q&7qO|}/쥿kko4io>-OѰ9v~n~ϒŌWt//yي̍VV2ړ?RUapbIbexǟeܼV;?A:F\~mlxԭfk<끱23͝,K:<`{>yR_R/C])B 1 ͵ /$mlX{zfq7Y2a #U. rY}ZעrGsԚt3><,YiRKO<륻Q2M}I +K3宾J}w[3=F3^f[{LR)۸}s^ Js=?-{"(uelǑ̼QlS0,"j$Ă"Fsv*<7UIka$cݣy𠣿{(k.Nrz L&,WdmQԞ(~09ŧVi I$685mgyL?~8A~w<xn5,cT7.A|d%M]ù6bvp{p4P3݇XQx3mJjȗR:W<ΏW]8B>R*7Jv9p}o y$˵(^<}l^,}oY0 F4e rdlzԡSC"h7}ot|ֳ>'|2q1^5mf͑n}ETnz#W2G)l)J-htfŴL*7&Dn^hZh\4Di8R< aO}`rہZ}x*vj;r9lBz[}Cu_E_ (|1 40<((,NjY~y)A_叾V*;.+ӚUN_+ط? /9_vG>s=SY/P^>^;o`5x%jRFk&x($zJt'1AE< >tCCp˺P~s=0K`fo7mף@˴܊Vz՞|&}˷Xr{>*Sb [vʓ] T_ևq 9R+e\j3[x.ra\g83^WkH^۲UG qZnkR5!o*z(=4ʾU=cT=?ܘflQ&Z4U8.9;n88-9+>$|,?{ЁWޜ#fa`ϋO\^t?AC?񏥲=#dElY̝zGppg,z/n>O%?vJzB3 秲{޸g]ϗ6;6oeZ+;ɫ5^,k4d 4F2._aJfX`2?Fc%˘?T!6ֈ$3ȹBEPi6JJηDϰ{_{v3xy*Î\EkWxhu5q,Uacϴܞx:MWf\v~ӯdtg㿮 +e͑CʼaJ jSش+=[ytsIӛWZT}2K̰L4x "&T64(<٘4UҌ]Wy0+\p ccSk" Eh!baIQ1fbxaVH9H&leY+{,אϬ*=ܵgG_;Ws罦7S=Ѓ&oם*Tu9rN%yTEOm7fnV)LmA̲o&@a>KE\󤙌-3a 3chf9?_?>=y g-_,?U +I4V$FS!0R+KF[O;?Y꼽31/~~)<[>e`.pf;,e;,vs})7=WalK_ +=μIL\–sPƕɸXC/Ot12pcr)CFqQd|  ܺeY[ &qflJ%_EQn㳷M'OW}%}AyG^}D2U]Z{_딻.W42SR|.b*1ߌ9>YƍvlIl{U[QtD~וʌkPf9L˄9իW3>joH~$2<)c}</دT&ebnʂ{ejUihe/ʾ׫\*T)&XӐi" )|E}{dPHe~̲K]*@%(5ᆪHR[&XrIchPRKD l+֜&;Mtl3a2ӕtZo. l `=WodpLI(j6:և.V:;;Sf4 e3ٮ+T=߯^wVm<>+l:1^-qx.g /$G4̏e 3%Bӱ|^lRF1)B*5-ԟm!r7+Q|NXqd6F[!w=Pcez0e{ +;FPdv0.1׌K,O?뇲ykЇFrM)7*~0K* b#  M( ;Fa(<ڋ*rqI`Spc !nG?ÚK} {!T5Hn'2B`ʅ%#P~^'R}꞉R锕dv녹TSd^>:}>5b#ʣ c-xk>LҾl}+ Ds䮀|1|0KEjO]-W~8YbY6}Cj0hL.6Pj' '~3³Lhze5eŖY0[nmm{N|+6.Jl˥7 +OJǜkIN!&Hi\O<8Z` ?n 0ٴMC gZeln0  1P%2$S5bOб# Cc frmeyA,ndH sgdt(3*C>TW@_jBO|]gtKUHׯ2 mex F>@"ҫ˃㩆2"^h5.(r’M_D;q4ϡaL.@kʧ s(X)/hL uJp]Ŧs)n{3 >́{/E +meC6_9jX.}4L,}zqCB/[kg4k4R7kU\;peɋ""DM2!$TPm*Ln/ O5Qrц\:)k-_sx2| M KOy]X6;ty0wbQ'DwO2]j/W0НE_=4ѹ^> +~ꔑ<@k 1ڜ\d1TP2aR +!B +KGvz󱙔Lrh!,Ʉ6;2\.hV 1 -,M߰_߰R~X"t P]}8꣏Ժw֪<0Mk T0S&[>Bf 1jl0/b6˔j w:*!$?!_z4g4؀)sz3e-D5]qn+Z]J#ů0lS(O +ͧfKݷ=5{ջz +[̓]J{+]T>d[ujbvߏ`N(;. +F$I` + vmY:2ep-2L|d"yI 32nrC㌸Fk~á |zPpR:֚h ,P̀D"Q;︃L}SJ9*?5b 4+27 H1A登,I//-cwE c-t4uF)MщybRfBmWi/ߟ RM_[,tX XɎZ;.:?]mq(Mq!zuf]JgLu+wWoA/ʏ'nTy`Si.d)x+-֝ɵ=Z&5t+:j3M$ʋj#䷏ +;G|}Ӈĝ\^'W@ QUg+u=gߘ/q9Sӄ7J__jBzPw?Y1ڢƓ6]hNFrr8R^B41G˅~b߫[`=_yZ5K/Zfc -cA,3 8c|qBNHUT)4H.bbC܎Q]]%yu1W;6m ֛t +hHkBZ3[+U=5q8OWhw=ỾZM^.^ _gy5'Mɹ|m7Myp]^bhgu'tWUMU|'}%wŃzB<:W9Q2#5=̢1RI-Q2q8b?HB3؝B6VR1U/֞3ILN8D2BhRy[hY[oOYȈ]+ƙveG^5DPZ.?GquI`|c` +;XK57HjeW>qvWrӕjy'WZrn<9 _4:{iYB}T}݅`q2\?vA=خkK)5e瓕 ͺb+6V$ڋH^[=@~I\FKex 藨KSG5.LFf#6kR kvе?TW%Fq<4@2x=Rfk0.6\4gg{Hcc(&hRSs |;o 3϶pڟ/?WoHjcSǧ{jތ +?>]{\Y櫳vc9tBI;竡{xyIbޜ6ԊJ4A\YɵCwagyI-,Ց8*T;;^{ +0ޯ|U$v֚2sex1eUbJ5[Xn) eʆei[lIz'HsC"rt3u?+T8Ҋ^;,$ +* dqNb{\q@<3:M;C7Lv??yHY$fSRi#v_TpG}[,͕*VնI-m + O=ci{Zt5c2B r6 C>.$Fy/mZ| .3T +T&Pbb9gupu^` YU,%Ҕl f6Ľ? + ɃVN0䳂Ìx͵ZkG]IƬ5>ޔ{x'V ^]p^H͆)RE"X&CJ 92ey$gJ(bYTʙ38ޛѨ~o|Y:p6YB;Jgѯ {A1Y:F.It4hg v7Pkἳt$ gÿĜv嶎mc-6B22 j $}q} V3j7L +N ;ɝ?Dsw^w{/Ӡ9Z]3Vi5 ڜb&kxfa2.@NREsΑ"rL|BfSu僩{FPMmFC^ǫKE%{-sYJQc5kD@ %I=Pk2SG GCS6p.|[5$'~`fY#35QͶֱ;{RMp8b=jogl[ףT6Cbmeyuv>Cj?2 YhBN-87ԯb D`=XIJ5# P;b=;Pa-!4\\RJ$ey?Ph#mn|8蠣.ڦ;U`jV=fe~#6]yhgbl,/2B5HQsIݟ|*]"y;EJwZ; ELb[?YBgKvVA 05b6}T;K dhgkgiU;+O|p+RCvЎ/77@ LQEg(h+דC&YkrХB@uړ$/!R_dAb@[c~]B 6rm nf#w,ԅJ9uXGDAto!ֽ7 zT1~>*Aq}SKJ%ZFjo~&yOH2SS;+43:4KpYZ5D +5w6jsk6\9!8[zXQa#暓zm2`"e%xuok?u AF}@0-L6tYX_WZ9QSwx~%bf@RT`bX7w8 +[OЙT9w;;9G4Mab5^c -܋-\X_ܡ%X@|[WBu%Ru_]_LB,dFre{~HA.8-hCc$kTgkn4:gwA7mTq'MjRBu:T$"Om< 7T<$@}'}ШC"l99Gzu1rEWv޸_$ ]:Y tҺ1q_op.Bhgi?UB;K|h*z>Ċ}®`YtTg&DYeY-!FV-4$~-ȇ眥rU㤊qTkwqET\R+ +I} )bM7 rn'yM'C:{l<=Kco"m:4kw9Etm+BDj,Zkl<1CjBvu $"tbh)|4%VAfk36ڊ>s\Ndr p +z]El20º1hAs0sg-h .|RQ47cBיJ+/ ̥"KaTؒtϠ %L#SW,lk"F+QKf5sieR TmV0LyH6ѐSE6n69Obh75 ͱ:UbpJ!1E5jFR/!/ %4_O붒RUw~WEeH/dA+M]'<+U1F"ԎTc]Sǩ}h-}ņ3.hB׬j=6\%{yoBTqc;n.;sԜEMf U6٤#D{>_E!7|g"MAKrگnbt@uIfnDdԭ5INM5Kz%C*JF1|T8/~ .AJVk@◰Ihzj=W!gRC{/}4a +u57Tzo:4u/b,ޟL4 CHYgTk"nMQk|}HNfu.p9sM5b  3SJ&ȑݴ}b}F]I|?MbV0 Yб$6qR -| _JM.rGM#D>Bu^ۉ(T˩O%.khZ-r h@pOVp;,Sא\JiQB'Wz5{F>4E$y-w'.wYjt=o*kH,+ȶ~k=+zCS؎Kؖ Wы*AS2S=aIɾZCK,4YU6Gć$5%@oJm!RgHG|fye~ +&H2ȣQuG"Y1!ی645Sv^)!\x|fiĞ-MEn`^{~:sjgoT7=m5s-Ͷ$8WSf;h:S=aW,h>йmN j>Tk&$O#hGuuv2ڕ;# " 8"'[?\(m"AΓA?X:yfPzDgS~>:>K[IۆEGp.w-$hantx +Dhb ȵqL NYܰk&NHX:\d>gA0yF.52ey y*x[JԘĄД,%xnάyΎqSّ,%&:&< 8^g}V;.q<'xQTǥS| &#.t7g.8,\0gso_4kނyss<O 2' 2dqTGu[ ?gL\?*׊\GkՑ1@ڑ^9俙Oupy5x?9:o#}9W/qtp"2>><L2@G PpYxV0~jFZc,4E**TgH7k =deތrd@Uv b= hrCE:$#6\\mfda hPTcUdEH_P0l -y%҇T0Ab!!s?NcEepͮu :Kq脦/j]ؑ.MmYXtK K6Vk$\scisЭ2)>ߜ)Ol3u%LQb ~Ik2JQLu ViA*-(1!2TNcNg A)qH6A!SȰqFbbLJK3C%ȪJUR +3ХK?1\^Sk,:41|x ܫNԢv ytzlJFҽĂ֑|Z<`#f5E FI}NI6C5AGAp®z9\p'(F :,(@5kX(1_@[ŠT7R5%!c?S^ +s6\i TSY(ҭ57PɠZXihPxoĬ1z 2 +*! AT#{> `R28ݘ&QEBKuZXunv`ά-t@Qk j#\eU~ĸ 3.&T:\Qj* +lXpY[P7#2i@%{T:68B7Hih %5+I]g*!0\p`BiBJ%,uFWJ-RGQrPQdJquub\tl:FzPtkMWj'XstRڽlG,aαERen |rRKƂHN%܊ ,PVgF7Mt9l jKGhbANP oTXgNEM.'t3?XbdŚ M"㷆҂xנ{Tt3CsPgTc1,%8YM!t4:H"AsߤJKJ2\Ȩhld Pe "du)j"_J;Fɜ F^2#7m 8@li#i\M;2i/ݣhIby v b w= ݌-vȅ@>*{H7r;!%<.ʇhҫm(|)f-9u*Z[k]WK'͇y*R{RzA;h"l㟝G 2вAAw"~gNKt(w9vU[QDqXIgėkAdX!lUʨs]-A:6Bl6֚\j vm@>>li_BǴ$IJA q_BP]$ANj4 %ߡl:H H76R͙YO ̦ShWcz+1uiBbug1J5Rg7ک7)5qzwCW>rUx_qM#GM=w5F #| + 65fn vdqDD;SxF@]5!TH.$%Y+0^)Cp-yB^ey$fck6b"LNw$Ixј؟Ym: J|bhٴب&JȨsklt? + +Uxt2єڑRnE/r0}A? lT2ԥcMf+YH!]0c :VBb|Tm +:>v6ً݂)8a_X!O)H2PX.ӐVxCT~5JJ;ui ҮǦ@8Ac6RBAw i&[pH<PHJRA $w!j<DCLGwT SmX! %ZH9l)chL$} aw:‡  א54֘J2VE$xz0e +. +RҌ6'Yp!ir#az *8dGJ%u7=:L1q!O?J let~lLWo/I)a Qce7~eg<1ΚH1hRr%& BjAe )RiS T@=ǜId +h [UoJ(2i$fA{)}S%Jh.LGxq{QZ(%$UF~MJA^{b{k]P@mǑ߇ĺPz+nM 'Hk (8 P#y5(5b0Plۤ> }$qӏBLP?Cm@ie(oħ`$@3!D6;2ǯȒ:4re0$P$yOXCl72DI #@㢲M1U!#>3AP:rd$VAF}PI 2yp=W360Qɔ:n +R#qANA +{,t"[="A4$y?"S + ȸ^* TB6Mαcfdn-7n'T)cPgR*De8uOp=ݹ_-t(VZ"B".bGbގ܁rֆ`wc[D@}-@;EHJww `'t7apZozsc._[BI$>&B^&0!wr)o`Ӊ 7.;c*GۈiS0~~pK0 "PO&_?8Jݮ#SȤ0dc6p2g˹ ʞ%rLLxĶI3 ~A|r1MkOvЇZ5Pn{ +|(NgH2][qЃKl̃E0k@(:rPKg0W |}w<`KsVY 489u)Ru A--;{f'|w~b2LD)"s8LKYm"@RA|b,nq"-=80slkS@&'+g@> ۃ +%Re $XxC<:"r=An򔄫a\J:Lڃ\K2 JH[@qNlTØd@)c28$g{i%G"$JӕL)̕)XS)s vJ!9a; +Q!qJ 01m~m@)B[!NhЄpLid0 *1c'9W2YrB*bѳ {A-Z +rx `mˆلR56V2 -CJd +XKQk +xا׍EP.ZD&=_9g=FB}:U꒢1 ̡ a,ea3j ASI pp커ط_$ٰ%D|Xc =-r +Mj gg!= @T A(LDjĘ)&y{m5ؒ” wi񶑎.Qa.jWF2dj ! Q|Y9&0%aÒ(0AxL]\TI@Kv1m!b}>p g )9ۥX?( +Xyg~w"` !J?RDR$RRK)B$?Hlv~S߃R_""p#Jn֘;5gw$jA_ ~Tu&M ~xsʝq'8RBj á85(L S*dtt +\>`lb L@}0p?X*[A裝w#PV\ (|EM5_țA ``2q+/5;@ J 8f&X}!>Ӛ4E7{tJ٠a S%d8ʌa25Qc>AkZJrP=~a,C[< ~E宐'kو3az")&ځ)H-VdjcH.V߆KI%)eeuBVc&M-D"9Ἦ/brR](E` FfHr\Ԓ@=PD-#v(*EK)P"-9@.Ԅqgr9;Ib#sچ+I=n-bZwqW:> KK\d2ڴAp^ | E4O'}6SC:_P94Ɓ>a,~pE:9 +ӧǃ} +t1E!R$VZj9,! D'>`-|2 0Kuؐ RHo2A/LsMQ+sD)"0{9 O?"N+aҾ [DC ƚ22ܸ0ȵ (b,=8L ^/6Cr7ʝT`?!S saұ(jzJ>/(N_S&,vq/T75z0rzg-jv)ED)z8COlLgPWչCp&:`=:6/&~2S2PuRL;eOKo-#8{+&`8LK*dJa!̄䭅ڍT=n.jً ˉ(Yk lu>*ŕal#oz=r>C qMĪlfnF{SbMN`S|)S ,&rQP/2֣Q[Pug~.!/# P?k *?TT6PDH5Gv1e7{t `Y$y&} $sO{#5K!YL$pg;n/y.=Q$uheG?k {PbqaPv1@ LzْvҪW*MD +Tab>L&PM()u4uE"@uO=PHE0YgKR$£|CK;(E(EH" .I*D T2iK,?IjЀ )S.] ~Dqg?[} RuIr RC. !HUWe+ +4,0/V J0&%RofO:L%Tv2v3{cmAPO 6Pvj-PdԸf&(AV6K}m)l5BTQEijGh1w-9LO|;pb8_/"=лL*Mdk\RwWR);آ )1R&*N/E! %O6@~:*7eBO_*2KIOC"KЩ=bH j.s5BƼ _ـ:*pJvJp`o2cL{B͏oK>1spꐨ 5Z>iPDʜ8'(uci q@zbe!xEÎٍxHē~ 7%Vȥ^ eD_^+g߀O +[YC(J@ꗐ/_%y!bKj ďrԚF&*O:{ODr%٠? Tk]TF&BY+9T賂\WYr Kz jD=f `࿱\|闅y!8Lxz&SW_Y|omyJBNjpŗn/\I[lCAdL!3oRKH'#V(HAЋy4١z!$4;9cHjPP9i,gT0Wb zs2.L'K_! =HOV2aAQy[O-q᥀A sbePS\JYR\h*lBE/#88^ b(t%{|_j(R (1ג +o$'5!;B#i6O8I%;HXP@:$*ȧ/ց*QW%~٫/1OXc /!'@Ev؋sw!(Jh5)&0 *b8ɈIMm#vTҭ8f:*!-:׽e\U$ +L-DrU !]Rwɝlp ~9>y^$씤5j:7s]A xWTpIUq>W:zȈ"(w@O2`l#& +vc.뛽.`Vԙ }4%k6Vi7sHx(q1W?Sv}ZܭI~٫؇mR6SzbQQ $>OWsI5EfdOJ)e\,D3q+A~F|e @pNlf>ң&섺 \bD\JJ ;bIjoaAsΤ^7\*|Ml) +{>yy n&Q^^;Lՠ+~(ըG!|>UDmx=^/IJ.؋b4qΘŸ^My5Ycr=!_~Qo~`Ymd\w~6}r`ʺNr=GK]eUV> W*}^{DJKyZ@[%K&5¾g++d9Ь.hj~$hK|h7Wf0gωfn !P;~AT@t^S.㲛='5/Z$Zi:O*JkW箷 +-,gLjhJ32!I"(T5QA9ڪ-JFvhKl |lҮ-Nkփz8lH~(iUtr:>.5YP)Ԩ¬]]F!uπѐј}+vc??ʸW]$9{$ZAA.WCQW#g_bqqiqWΚ?ywLv\^m4d3tq!!L){9 \+4Z4{*Z ʔP 5{c-VzBrL-l2iZFҨ'4;8G@銻|wVJECr7~\:" dr[xګ1•Y_LoLg'~"?/^OuמKl_8PsfƔRQnF}9K{m>L'kmay}oŽ'JS?a_(tHk?sftH͛ +ōįJ +Ȋ*+.J7>!y[}B[{fżn#}Q,4$yfi[> +nCK7 zKZj=%@IGeqOeig/NX7 aT/V//d*:DI?53eN1/;nc*j&*:ZQ'yVgeE yv>ٝ:3 +K;)zq/$yuǥ/jI75V:w "p͸)1gjcVDO>ܜa֗w~9"x!?M8Ϧ>1W+z v6I0o}hْfQﳯi޶W;Qw \^zfa$7Hk,-^Emp>5`ӃxB/kܭFZrTgkr~5n5IpGZ2EŽ.y7ܬ?/XMe׃ 5a&39w8ј)v#;`(YoywoFЙuD[Y +_v?:-{vJc~ b7V\m,@XqSw +_(&4[|FTQ {qi:iL26f ~?t)=:5*-Ʒr[U@ƤxQ/eO-o;{;\{9+¢Qۿk׿Kq{=Xfvꓘ{I&ߵ&,F$KlvБTu/ʊŏ,Dެ_E=dn><,VV'Pi_z>>ޣ//>%ڳ/YTxi9m.̥.(RȐ: +*exJ#|{]8_A(gYȞ;!{SyN w7J>{ ykY$hpfқ)|XP7A&/xm㔎 &W>nk>0bo[~-j=AiZ @rJ.~Wzʼ1{[j!%ծ>,ڳ'ܾ6 Կ349v}hnuPq +2*JbuiMpQхbbpbu)Qf%}U>è?N +?oBADmG6髖j;ƈʺM\wYouSi*-- +6.c~RO?P#do\}k/'n̸Yk_糨wbw܌i + +t[aVPm_g: [c[mBh6>w.%.QOyż$h*!'바iL]MO_Qt6["nw=Stfyw=ү{;#l"㪝O4e$zrm5.ĺ5ŹDT;ݨN}#O/K.(t~Zq1,=PkVcS^YqQrTZTm-y_g--g#"kRHҠh犠B(Rx5/`_]Za_+!!r!~'÷kP$+*N4LjώJmť%ݪazYnz!49nՅЂ::w_:]L=8*Οr2}P[~9*-<=ByxSI3]o,;ɺed⇿o]_} 27fp'jO]O#[/S]ڰݝonEX< 3m6g>ToRzլ&+^(#dzl&;$L6JlXcZnw8~5:-:5ªfiv̠2^˳W ƒ^A}hK u?J m++5ݍw O +/^m[,,;s5vrYw]xn:Z`,Z[bٙiՑu1!٧;ٹ*$HKF[EAY}giUgNі̘Qў5>qOt t7;A5oiTl=M? KY?&ROTJk{a7 +\<}C6Z]e{ܹ.[է}8މu9{7.ߺ%[wq)]cX^}p&YE6 ?:(txwW += ˮ hL|/|/xh&LYg}8'wV.M;-;hZ~;ZV#5aօ2Dצ:9q{xHwDtWDJ{XPoEgD:B܉Ƹ(6+.gt{׈,"K= ~cnpjsKߞ[njliuxM} ZfHMB3?_:|Z5cڹStt䮊[1he4Mǿ5Q+@ ʤhzd`r\6es~a/q~|¼]X[!o B킟sM(u,TZ3SװwB_$j8|`mӹ|b=zu_Vv>Q&mݩ~Tꂃh*4w<2u>^4B4_WJh2RQORA6矍ֈퟥ| ~mzJ;|nNOig7.Vľ Os- |Gaۇ\)t 8N!BK\#zzۅa _\}m~r)2U#""wwFc;_?aU)UތA?M~4lq,05Mܰ*qw>jpӵ\vASnK\[߲YUQ^a,m0ͥW/Ⱦ/}^WVb*XAϙGy#+47XŸw;{6u4wZ}p?؝)J{q{<ǹQ bs|k\w^a.eAQ^"0*uQP!ʟ~ūjyޚaej*0*OS _)|kġ8}c44a|4SiZ0RHQЮ嵩 +\B #4>Q x%^^mUx0~{RkuP|KeOvRAfM{_`l`CɝH>:\r_n>5DB[kt<52ebër];G=-pkx)![Hl]" +<#y52f9[_߾ 4=߇8&N\,%:i,ϼվ{Ezk<<פBg.9IapYV^1l:"y͊HQ} ih,4nTOOcl4i4n|4Vq0b96u7Z挶x=D#_u9bĄpҠHb_G° }e#k_5>| 9C# S>,ܪ'C__ _/^ ֤4|6v1y >iRMMM +MMGASGߛ݋6e*os8߸6ysI~~%91>1xR^uNR& V_ûOؗFS/F-GF@GEӔw91vBkA +[};'~-ŝ_z{z1]KtIst"wNo8G}wa-U7G$}؟'~dğ4d.iHyz4]y@ ͚gML-4e.R^ĢAhC*! nys9QsfQagF b;Wq +Sf߳p¼u[plo p|I84Ea>r1>a9mZBm:殷a4s͜Ki )4m1fM~J9V,^z)R%hmF'nCJɓv)/6GhhZ怎 ❅G~^ApsTYlR g!1BbG%5 ms`ij +DE̐𹚄d{X5i<4erhtUl&Ќh 4gZ`䃖ph X>Qb~ ]x\~b7~1k{1qŅ-Q-eѹo"OWƆjN7޷{T9xM3=N#؁)(#?{7j̩;0ZvA 4l54Ke Bj4^K{7?&?}p9Gbk\ %^gc1O-@򐒏//Lߎ `F`?) llSmNAFCF_㗢TVX VhΚ#hv'4(E+n)l?WW8ʔW~ᷞrk+u +yRd&S]lo]bI}|B[γ+$=_d 5 +xkOHi*WJ4 Z+C?fO1B3!Hi6RZmfog=|GqC`ݘ-WjeZ2~?X_Uw1ΨWKclpkO讱O\s!)rIaMpͮmQ"~IJcm#:/9,3OS-G3q<^ +-նF 6ZKhh3'ļuHy&)ќ ZjB+J&?TqmPM~Q?oez<7 >Z~W^_>p5΢0ܹ:46&(=|wO18?m(Y#%?F?| rY(&KMMa||A=S_g=T{/ږ[º'm +옰ٹdo./Uk~O|⏋xZ< 8U|89`G]#s-.>)c ^$g,d4l&R]VDi*-еGs;|ǃW9djGopx7\:w1;+vVnjftyZ^dK x?2*0 +cg(cD0OCvALgߚ&>7hޢ]*=>E ihl--yo.KX_oX̛񌠌.{FZv|ley!ݭ"06н?R3\.a^Q̙h:SV(Hʹ=z$~wKgɧ'ԯevJ F󇴿:yw^f~p7w) c? {dtmoʴ:nvíL 6iV->閚|ڠ}&~%[ O}Ii#ZBq%![nI╰m ~};-%:N^wMuhXVShtks+Pi7<.ᵍx ARJˇKxu# +6!=m-QB#R΄Z#KS vʙd/33>0n72-_kf_rAaga5n1A֛%(loogr^|珚Ÿ0ӯþJx=ohR[x;vq =w0q38s}LO#o-hdj╭"@_y PAϗS/TUwRYI<_v .R{i`-^:KyvV]H)*gc`YVڠGnxtIߊϓϕvMi|_6p=Vޟջzmyccp%|fXyރo7x']lro;k?tE'NJ aCj6Kr/Z,#L3v}Ӣ24+ď?sΫ9sԜE;׷8f,_%B9A/*?r)^ k o~7tmNؤT7AF'N1߽[RdSFuCtcq/KR,Ç+(mIȓ&즟ԽStsQfMEQ$s`s,&n+{훑u9y~9aRVKLc& +3}=ъ$>O+6mZQ6}Gtx/׬wE?>\hq~{ +?ɝ9Gz-?&w7:q9}R6a>ݲBA xľ[%8=E宥2{=BQTc4#'wB.xYF #Sdc^\!^}L*g5 ]TtύGCm1$woS/A?-DOm'nu_3_^tb*߶sk`vk +qžv-]#sV93Ж36@;7"*:wU!I'[CGϳz HOaP%+Nl'uh/2 [qFRpJmjW=:S}ָ߫]k].yq8k\Oڝu +8NqT y5&_6Laz.f¶mh vL$ڭ;9a^391ӻ_-ee‡wUK{g"]oLz4wYCKr!m:Md0Thyv!乗cǥ'rkQXAG SU{V9L)uRX.G qlL հ@RҾïy}jݜs:|˕W&2 58zծ-BK(i٨IM4,2sgnM]mx%0ARHnFF孧Ž@wk[8YU^ú$΢ȞT2+.s,}kVT$˭<#֯UPB'4J-cwXz*.J)cTФ`x1pYGWW'k5k8L6=Y1'KiY fyZH{Fda9:dXv㟛b*S(ŌE쥻$z2.x w!` +<\DdbAOV?jʲZiW5DI3'YCy0{o#?167nH 2[wڊK1qӑv""hx=]L<^'M K!\jDӎt/F' +}GTN#!2ɚl3Zoڂu7DmcF0vSu[}TV l NU^.(fW5ƯFzR~P70xM yoDP#{+Kg7"=c#D=7=4fh OI{K{!@R|㮣Y)Tbv +3q.¾PȞvBM{e奮)Ύ+&5nf[l /t۴O:9R\8әu'(-37CZxMZHsڹy+^k1gՙЩ ꢓN#MrϤ h1ϢWjtnJsn*0C֠g򽶀>}y,e0^ +ϟȒ.Xd$^&ˮå1-E ؔ~->- e x(2a7?(rZ=Fm7C[:ÀCԌ + +Άҗ5nQE#huƶHW i$[*Zk<x6H 8rW[ , ^)rJ_IQ]V:9IxwnU_7ec,[MfZ7[=L7vdm3KF735Id7Et.hx uhhjUd}q8<d&,v**:CWrJ"_-42B&G +yqلLf2bN]J PpWk3:N=,\)&e' +Os}):=91rNSndwe?k\L^FOy-冎9J;"=椂ټB#I&!-BjgNj25qf`f-f,?;Yt8l7\.J5[2aV&}1SqƺΆgxrh2=M`0lw'+hSK +&{FL4VC6 Miğ]Hs6doLQÊD}Gx"?9nN s2K&9z.a*v W5LG(l=;M2'-TH*5em85fRzMs~2U]Y_v}nߘ%;Ets>菙H䍌G˕ecfHۀEClZL;?T(?3X}ځ-D:394J`aB|HًfAF::)sݔQ& +Sk0W:t`!sw>PIє=~geY`Žŧ+: zk.V񻿄B\^q/ꂈUv.3L|_h&ز +,Gw24+;an&{{!uo(jAJՄ'YuV碍Wע]v!]#k7mrHψAn=dby\Qd2fga"ciKJ}OEVv)0:<ɜ^k0,^80K|Z$ɩ.c*E c*7=M}ƽ[6?i+b_r=[gb">';c(Qll6} hbIlɚ}XcfhdY ޱ+ cv@MBBBXrssoy?Ωь4rtU9Uud}G\6ʦe3*CH z7#|uy52jץ?} ny|U{.WS/l5û]0_k䝼{/#fI^ޜy]ƻN߆n~z3z絿ﵞʳ0_F]C߂u,Yyu١@8r߇S1xbvߡO?=&b>A/+B/~U빿VԽ?޷ e"W /[/o:vK<;^".akr]{v_9Gv_ +-Ok׺Wzto lzy +|GX90 s_g}3}׳=1 {^=gKq]؅ޭ3}'?Qq珹#]akܷB\pm==kϺ{feճe^)k^ (dx+~YuM3GSs)|$#WcQKlkȗ|{mֿ㟹G3]W<7ο4?dY>x᳟#?qw{_PV"6/^w.p/ _>g.0g4B8ѿs=yaB}?fyϙss&'D̿O:ݝ d8F1'ucw®QE+>U0 lzƹMeoYV=/XiZ<208%VA ,Ok8ղxف90ŕ{sz#KOg?{ɥ7!Wo&RzS~3Ow)ֻ뿊~1䣀/ߕ\%W>{CbS]Gr{n[ڲ[oVfAL9ݼw9ՈG\^;?=bvMͦ2S(=ռw74;m1+ +qK0{xgݮoEo3s*y^:}ֿvmr Guhg1vOMc1G\3׌񷁮rAM.{k9`݁2̓L0`~H7'RyK-q!z9~Y +}"?-+\`#o๾%.$\DO +9worG3~Z: p8.==3}3s;92_gpZn:~Mp哗cfM_[~g=-ݣ6 \nɧ<11:-pA6M/Lr'6.u}XςjaM+ue'4: 1wb_"#kkehHĨL0:^@ia,G??s%0| nY\]DžKGHWV{޺ʷlxXZ:y^ub'b^@KbdGhXuѰ+1G=8lv2wa&\ܘow$??ˊ9G <~0uKӵ=1ꗞOkanЫS|G?-gGwwڱߟ^;?r Xs>=Xs-'XxK7s9ԧ3$Xnװ근D4bl,9`Ci.n/VzXÏ?Hb.Ywͭ\G>Qo煞t/\S99Hc8|%؋gx`nuGG[}~m9 ~IhׄVp +`"GyX `};$qO0= +q A^O}9;'Q=$c`l?rpz$c?|:Gs1.yF#Qg~ \\ͻo7 TMoy}/Öw]\ԽU:}?~cs!v7dfNZ?a:\}qH7ޭ#V + qޅӫ n;9V1/7u㙡a>?>~ bg-X;մxb˺3E1w}rݸ1qBr 9mWCr*?x F !74lbl ngo\;$VCLЃGsq6?{73e="7>3wO-8MO\@$o$xبzf/k8~; l6Z2r7쬰 ;Ua|[s=WY VI=zƱcg|Z89^oXX&X =Ck]m +<;]~&tZ73ۊ]a17?wq7v@x: f&xy$ggVˍG<ͯɪ𳿯o{yY7Du^][|'Zx]s:_vkqp12n&ͽQDj9=*f1yٯ [;!}_9c뗁Ӱc3] 4óhj 06xz;FD@|&M^?cF-_cryW׸|eO \[߈Wg?A|gJ\hGSHY|<_-ǜxw6]^J0>:c(v֮+~6;LD.wC0C%5K;+ձjU + k! _h۵leCqٖcYzϕK~9BPl2wX`ϥWsV'a.x~Goh<ݑ>K#yz;y%ox^|B"%pM½3_6qz3̍8jk_.7 +e.C~F| 8ApX;֝Kػ֟^ĕ];(^_wiK.{[qkf˪E\0κ׈&m(;KpOѥgշ.;>}v(yfeYS}YxW']lDFs跆=pSx;rWC|l[j37Vu"Y``(jt8r !X\-w+}iJ XN`3Ա~\<]cg unspWYY;k jx/ Ny(?ܱP֯y*gvߞK~)KvI|c#ay̷n.q|xk16C_cO?jM.¼ψL`7rK EX#voЃ@qS#+p=xe59PYmeƎ=.CL KΫͩUϭ+5Q졋=u c\NMl|y",؍/MxcVϋ;k. 4G,/PC,㨟v,x]^x{bk"Fc}992]^FɏnT>Zߡ*жw~q9+V칢~Ѫ1tᄡq9[M?[w B֋N&Y<`gz +ZΘs1X̕`S?ΪGdZCYI;k?ĖaEUO]}s刭@Cl,yixs˗`O6΅X&ؽ`y+_U`gax.;kyvz!xmGm+_/Ҹ5ߚc)Ґ\W@w_o;^_vп~4ݷ{'!]^܃2o8/ZvnlgooT}q/~ bN<#!yqbg6C[OQ?A_ +XW v'mx> K)~ɱ|2\o3CPK>}Wiܰ7& VE \ߤ.8T`COoL_xq|;F:C{d*(d26[f9E7͝8>G.ܯ tvПKW3[uvl<O|G㾔o׏XHU>܉YG^|?wm`+09;`g!Vg=s E쬵;+Eޟ*h#z+{$&}lA{k_Cx=Kz +!>@ '|X)-g'. >͸~/EdmWwz';AEb-oۃ?Ha@d(\ 8A /]ߵe|௫`ܷ"ͫx cW_Fnx4A|0s|tW v|UD̍gwL:ӚqerཫA?`X +m}f }Іn?)&ZZFB(WG^\YMsHn%}`0Cߠ vb4 E9 {!RƩ>6F}?@cb.XWfծ⼡ֶ`1O^{*B5,N`_б8x`>sU8H,8[1rޟ0Ym=> p` q`A,;B/ٞN3v~-'97ٽ˟"obaioz- ~4wU`QWxo:2ϺZW $f).r5,`X?pԁ[~?8p|CWײL/,ēg<'<+uo&w~%"bʑ ŵ(k|$bm7=?%Gn8I_zqw]|z7og zl0lP[3~4`~m'u{xs7Lsļ[_ QV?~9b`}*ޟ]1&=Mї +u;)?zY'ݻ'ѷ'|gyWm8WL+Dm؏gXn$A7Fo{is[x'.?ۍg~}bzqsij>wT zgzijõ+u*ٛDm,&G \ G"=#]uqc +{c#= |PY1/yBg}g'*AJG0Fmpw%\ m o|}R{wLmP7XB2!IJO?/~$"B-Ć[%g}пAxy;^-GV#fs9rų)]eH*[{۽O}<u^!%W m#{Y~c^=Zr &6_x q|{pdOg ;0(> G^J}}W9)30@*9οl;ؙ= yM?rW{'>Q5< {s'5xXrcG/M%?]T|وo>; ϐ<ܵ`C~b* !6O}fs{o_o*ԾtcP kY烘Pe5[|KƳ@n>b2ܧ[efF:;:bDenlT4K<|*Uli鈦>*j$XpmGRӗ'ф-9`сentn3%6utZ"MQtf(.d<ҵu,gv\@pt3Nle3Ԣ~ +Os:jiL-pG6K--rZ,lKJEco܆)AY[QLM,0 *܆.gE(#aPjnJE[LìGT,-0 KрYPr#]ebzd"Ћs(ʵ=/St褓ژAА!yR0W* ÅsTxe*]Z{euQ#iuZ~p ͣ}8jNX㕾4kbHS-Hωmc?lׂ|<,`1i h˶TWf~IZo4fES H4ؼ ;ߘ_$9WIeKӬDtGv~8qu~R=nHMg?Hk)+EYbpL,T:¥- ~fb`dH*^M[Υh,~fu?8Ȑϧzms)n߲1!C~7b*dۻ9f5q;""{-wSao:;g L沘 =@h<*xuAn(o<3۳\ILR17ZZ:;UD7 %@5d?>MVR ɝ⩭h41Ut*űd<6OM"v1F{^noъ>t$Xb R>pdi3hT p +g.+ks"|5-F"jTM՚b/ baI=ljv𛭬T0Tm}a&Ybp{4ZϘA`Sں]ҖȂeD+QJeD+QL+QJ ߆JT<` K [1mC;<3\N>g,dư B9y:3R"Ygtm$vmM ~lyB9..,l'+Q<Ng&9 m'  \ ".-Ǟo5PλQx^C;E=JᏛ &Q?8%%2JE&[Z:i>h5m_*p|*ߢOeu), /YP 5[9Ϩ>P- j؜q@1WΊ3gE@K|tQ37v~{fh.HĆ2o,pd/؋7M)t$0غ^o 鈕hvp9K ٰm5Mb+bejT0S`͏tDgvFMӦzbm`XU(KZRɘhB36[D.ɕZݱUD \|B++nAK*fat/;QW˂':dti)t B--0& vNONւ353*Ru%y5o@.}laqZZiPxSoT;:) tINg;:=aҎOԎOԎOK"AЋO %RiP#c'.#;H*24t`Kd<2?iZ4AIHS,=JyG;naX}+azMBk\輛CűWRvdYNAV I1O?eH~3i"R58Xy8pCykrv\w"wH +)H{U2ёz,8 +endstream endobj 46 0 obj <>stream +PZ/h>Ԣ*T*}w`7,_ʼn,uZ5 + T 0RIgI悩{tbDW[^djͯ(e?BM}d"jŀNouw%@qkvQA>ej: !ퟝ4u2bTc}p} o, Bm(_ [4GHh8TZ^>/U_¢~nqI ڢVpxJшYP!8~ #tm$zmM7l\*Tqqed 6Ƀ$[ЉI&mܒrNq/N:)r>9|( ~'@+4c#-3RބY,8=lƹD-BR>khk,aenC*3")T:cGь߅ݙTUUU`$D8YAVDUe%FeTI'ǜp)+2' +*$ENX *ɂȨ +ëG)!47^ 29P=hvƎ.g qX" + dUF۝ +rNp*8vUdH"SbVrTAArNIadG5,Pc |N d N^xB%u4fFԪ +'q%V d T''l99Q0 +U,+,Y zE$c*xy+sC\&R2rY`URI3~y.#o`àSMiJ:KX#yQ"FPBఓThʎ֤2 +>3DkR0p,ÐڱYu𬓗U $ zxH; ]UxA 8x8BPi@aEHQN0\YU"4A!J2%dNR6F 2>K}-<&4IPېIɗ(3(ML$m r8cfp,'fDP81#r;BvD]8`#1 + : %Ī(!g8nQ%F',K0EQtgx axhh4Li +zjDa0xsctUA +8fn̫ˀ\6h1E4,ZV3Ŭ&4l i7(W~ИOQuGh9stP.#@nE j<B<:ދzth>͇bt oBc'4'4&БXH&*'Q̈́Tİe_H}C@0$E%R'AO5( zl^^04*1z<$"RZCLIGcLguF$L2D;%C5= ;9W6& +M4+/1x9eYm#+`b tvO47!T@ +"L]XNd2&E#qz(K@Xbh*1cX(=.;QԢFuuSp[ Aɕd< \֟kւw`4BYJqhF~t:hL.0hN4c ="6("aўIo$|[ʩ=^ȚМBA~*i15Ty,qɗjozԊ=yN5 +`LIE^pPB'1W10p%q@QvCR%!+ YM1@I$Ed"eT\0Vd#Y-cLH4WkdDJ2}IR3 Ja`{@[B{\=wJ.*zzV!#i3_?vq 9X859WʾSf+/jVլ>yӗ8Ϩ~<,2&sJ4~ Lm2^8֎hcu=c~曣/ YN6N7\99k +=~PrKT- Gy 焎?y=XcnQ9!Hrgeq=Y%e5HK79'f +[ 2E:D:xωBoIm6 ͦͣ:Cs"I)Yb* + H +/Mʊ,=J&d쩙qgY=v ږe)gu#ae-;RC޺F/oCp;֫E„>K˂^-PtMp/ɮW2|? !ⶲHNƴ咪3r)/fBdBɹA@C'!!GLU6ACIp=CLDS[OTvz،eiJk_)xTkehkm+'ThalEP"WTr3T0mY 横,GeYnOU=nnWay_4$ZMSTN9VccVv}.LQ3tdc8ƪ#2 NJVOx[+u[&OBtOɊIO8EyF=]xAed^^ƀ^`w^TQ"[,nܠXpJMlFK0%;JUO4OSe `Y_yFwND/ՊL|4S-ƷT''q@NC +֩+:yHz>>n܉g!܄t +2ڻgGb3tP[ķk5;F;]٪$VVp/ W? ++2yD@{ tv"Dd0T(B^b/:J^)c\sPL Zes0P "/mQz" +2BXQmR2E MҊ|2Ӄdc+\vBwHB\["g+ ;v1UIQ[ +UdTb>C̴>VmF+]7SF Zf92b-Abei&Rf0}ț"laNQQr&D)d*+:UMC$IjTmHhYF[͒4X2f֪7 IHL$N4]Y4A3%ЊcN\6[怑M(2Y!oM4 FDVdi"YHg+ uMd"d4QOQir&)4袠:7d2=zm>K + E%i"f$EDV3ȴ$I&A< ״n.iQ!J=h6EE4LR2%CXbxy$Ik 34dѨYxfg2Қ׺rDl#cV%2PZVR';9^d'G"ͨTDIt' _M DeDgu 7NoIr:':kS>Ij:IgNwtu$5ݷ2 Mt&L$—^η`晤s ZM׺z%۟3IK'|)|~IZ7ep$!+կM2HaH-Vm&Y3JɨzB2X*5])l$+<|"t$,2JiQvmAD Z:PkIZ:F)i̵.r?ǩf .Ჱ|'<q+m:de1>yPى}Xl4!CDznqԤb0袰= +i wwm۸%CK [;=zɐԘ7};@=d] D [ "jQAdاn [*6"R0g(jaˈpiDEm`ϷԃleG S"vH}d ;Kܽ%B.jw)zmljO+;I]ͳԃla +ӻnA!ElB^η;M-| +ߏݡ·٣rTԂܝ*WGj}8&V6m,< E£\YB -Gf}$yvA<]ZĎ)M-x׋EwHS%fZ3٦e)micLoek*ɽflZ2γsfZ;32.j3g˸=-H0:[Eml!͖%\VqΖpQmydVCSgr%\n+rQ;r&O(fW. +򒳄\ĆCΒpqv&78Kme\,y+,qd[^͒k{_8KEʩ ?8KEl>pLxY2 q~DZV` +r|DZ.a͒i;6KEf,c+>GvSϛEWO/u 0^paU)JSH\NCE', $y"TY{ds,W i xbctFJ +)t{ +@;ТF(ljaeJ> }BF9 *ZfGc ظ'X E2Y ;eAi#k#h*RBG~ 8SIt/.]2/2l<×Dː{h.F>L;MoˑN*U5P M65*7Vi^%0_ʓKyr>&ϭ_G""r2UPFLEM@JYfýQT$9+P_Y5l! +\~745Đ,'].]M6$OLamh#Sr^aKEW4jeePs{K1sA5& +y} ImEpGb2:J +?0^i~XBsZi<1_1`-[k* \"'|`pN %$=pIm.ƓFGs4]M߹rMĄ+`6r^jo,wz|̀kkL+r*zz+Dg3D[v';ԧ6 S/ꝨcTFt^6A'+C7UT*%DP+@^(6/7Id&ZyfTfա ,McV@[?'cqi-r}c z~~<551FndtBrJF%ءb!3 +{*WӐ¬0=r`K((,H@en0Cr2(/EIJ<";y%@( *Ti3$ܼP3 #,s \#SOmJt% NFrRW,b>^|TyƔl0y5r3zyl͒+7Ki,܆R$0-:yYpODb +qr@VҿV j& ܟ.ǛdjDuUt868deϜx$#ӕ:CdsUdq'-C"eG6}E:YL/""MIdπ*G΂@"nsh-@wY&' 5*:9!Qs%2%rc$_xRL'pHc=K'w*Rqpɂ.LQ,/|F49"&d'O_ 4707fV'AF# Id9iCSdȻ<9}W$/0FsɆB \dU@ei0N*ݠQ'W E_4pYNѯS(['L$x΢QLNRZz=:ȘbN *SyP#uɱ<,EvX"=K # +tK*_; +&QʏLNU^$l䨊 +Xm}a)*3kM k^ dzn#*hEѪ.8A3[/Vm%ĊAéU:GΑgYCrjXrE;7@sdmBDdHJY"OȌ#!FQh5B.+Ɉbl*t):IoV6(/f3F) ;:K:Ak^ZkV{/Pux³!ziҨ]'hOKkmn{S?+Gpq Y>=̲;22*OGy6r6۴t qyrEz]D5&ȸƂBzAjӮa6{%RkZa,$ren3!™/4]IZr{^Xġ/ F\9yAgxm^Bۉ}+[MA%E5!9g?yj,kĵ!5`ͣ9h7fn#YG4 챜g D@;SAL7:STz?wȂ*s*EI-"S| +bD`C k5B[^fyHB-ڣf%24lu+z`,*/! jAZ*/\tJGP$5Vcysdr2ϕ~ї+5$,* I0)x$%ldvkt2Ϳ=j Mi=tGeccN$i5ft6A1W bMڭndhO;uD{'9KLEo(1#I[lZ3c^FWMfAV٪TS2w;jX\u(IQf_uDF&Or8n0ik2m6ט(1c2G+S03QRGul>*)5b4RkxK {=;0C&OvL$qFϲ7^۩dqTG3c'Z;9$HLxo ӛLN4zQȼYkT9__ƽfns{>Dp$>\$ǩH-:J=Oίy;v0g +)ŌG<;J+$&VĻ;:"3]G<QUɶv8YBUI,ᠷP2&k܂Ado#4O%tiJ*#REQ+UpxMJ++C2]mJo&5i8,l[rE[ScG;&"Ty\ H*˪d3.B-Io`ujRNd!I`O9Lt-B&CFMOGthقOЩ`VQDAYH"'*"BX.02D*L-.%-2GL̤(G㉕m߂G'PҤ"欉L}2e/6ubc菤<6eOHKz05֭ii鈦'~ +M[{?YD@!]tTL[ZlXHoms#f%: %abMg^g4:nz9i؎ҝX' }Y9EbX^bx5dևZUl{e+^rX3xI6w;F:D4џwd]oczԘ+} d줆G2:; ސ_6FRJ`!cxU|T`Hv*$Na%?$kʰDċښOA 7x hsQLdS h13eNN2)B W"qp ,#yyT/U=.&DyBUI'ٍYm{\k]Ѝ"}p=cWHHHQRzԞ +i2o2q{({M_ĨmJv;_GD+i'Y?himw0u}/[o\ Ĝ-tm 5ZLҍ79 xH\u`\`SͺaTv'W/ם8s9_8t6j8T +*fP}ﬧY:vg`#|2 + &'P1K\,**̒~@'sD$Ee|fOh?@8'w~R4SGv2؋*?gHv?LJ@СŽTh£_n}aw,:|>ʢcP"u^?EUfY9.y>}[t3lnKo뫸jĻ /`S"kk</.Gj'.WֲڠϏ`H?|,`!Q޸;x̐r胡PNcZ8] 09"YW&di +ݴ~#EώЃO~@V%rYV>9D@`0{KYB26B}>aʝ`}?`A l}>r"ؽ~]$I6Gɣ,R x}ªTV‡RVf52XGR ȂPKWhha쒴K4 +qnE,øhFD,eC4A䎊V) &# K@d ze?0{Q4 +lB/El(RT@F`HJh0-"D_ʯ} %qmMHn+qƃ! +P*b" 8,G S8(3~?I=dycRx-yJ95Xn4EID(0,2 p8/l BlcsأxwtW {;ɏF<$Pbs#ﱁ%*d)PxG@b?&?? `e +Ks֔`B +LAOOv0#_-oTH / IAs9yA.4fp Ki]/('pJFdx- +:%(AnG?yDx'N"Hz%@3`i`~ .Fp l +I-xlW0` } [\/~z ڻO ,!H)(%X4CF3Z}$qX(3^GnfL3 {R¨ 9Y=)sE$+(``d^zXr#NO!ΔEh#h(i8\3A$Pz@8YdIJ-\x,!aX& +Z $ a@@mN.ǚh*>D0HfTPBy?wl{|FFQ;x' d2HeNfIH@0EڏeH j`VFD +H& <31(IDdQ&* `Y> W"BxAIh$ VJ,H=*BFU9AIr*D%IRN=(TTX. Lc G{hAi`ǬP{ x9F \(0m ^@*d@O1 N4`n.> V#`ABpuI1!-U{eR`vHmX 4%c. A Ai *jvǙ^]= xWPvQ: ;ojҋfe\ 'ɰx;-hat\'+"9X k~JV40x ^P=Ku/4ڎ)VA3hQw31}dWvcJbQƇUUep5ٻK{9lvҍLAl$M&d;u" |@YgjHX}ް>^lzK|30G#t. +B3 _–/3 *XD_r +Y*nXhy>לJSq6tT}JZeO:7^>8cn>?xY5T(vMܚR=D .ZҪg&oZΟ>_Tw T(`57c 6r`b?2 t>dIb-MtrQQSh&ѥETxC>>) -`#X 8S`#3ɇOЇ| osfG?WS͇z;% + k )ld_b1#D^+V\Hd/,y4 +`5I +#P71},w^?"**TV/F6@t/dUE#\i04 +KwӄV /u +9RANr$<(op4r˲Xak+|a +I5Tw!3ŷe.@0|4ӿr +36XAGK" T> DhX83i~N# A +|~-l{It\?,m,AQ~Tr6=_xÄ>KČ(QveE; +5^':^VWF| QHe8RQ'dg_ +űm9@$Y\sVXla@O(}5@EglP^~trKsym7.ϪwRufB'|Guެ8P+?2r}M):. L7nKuڋ{#_Ŵc7[$[Wps2@MFSn;|t*Qwf8^$| CT4vܕ X޸^a鋃?M!$$߭>ʪӛl:=kz+YLʃ2h@L:~T>A!W]g5 +vz <0?n٬>k'# 6u魤 [fGS9Tx[%"(Bui`vgz L$;́k K1}`ޒHicn75v%]#aZO$AӜq1 +M'fA?:ovA dCަj ۯ0UꣾZn)P^ D'^wC \g XZd u>%i+~ +IoVgD|G'vMl@b zLh6tⰝX\+&Dr ]Qx!3GH"0Ɗxcp<1N-}1\] +?yn7K\j?#ыd!3ÐO .ޅZjvʀV ;v]^ +7l/6f8,+*W]^"v4Rg1uFk'w+5r:(oNsl}sĪ_bB1_w%?G^D"%lT`AF?, #JnםfY?Ѥ@x+Wf2:ҥ?y?V6u 9'rr]O]I:bٛ +CXttr]eM6 qO`tJL=h:YوuSt +`'ݖWп A'џ_Z͋&$iY!zQix~4xTu'x_z0(^}7$|Bm蒉΀ >c$#F Dг|C?[@mDm@)7d#:07u{V 7NMz{8]η] ؋3'AM$AM`mN> -diMc}0ӓ:9oY7A蟺dz>;@ie'O}|os`fZ_ֽA]P:@-1l;}.0c:tt;$rr*]r\hR%@5#HlFB}|Ȕf)f߱'\? v]Kp 'ܬF}1 ˍX,k/!5ACwkkx?+`5qkl`w _쨕y^O''}C5>H4GՕ~[ч Ը_`lo.4$f5Nw1NntgW5f ;7ВMwtc 㓆 +8&ØJ2y M_:0Ɉi_QaMu?h %O_EOoրD؆ߧCcc-Qk€YL4~wK-^ߙiɋGduێTtIOlLL}gyQmYy/GL80LLN"#].H2 }gZ(ciQ"9aoQwjc/L"zCCM]lz~9k*[,63me=!_cxGPإe345ɵKZqmL ]ot*Q.G _ѿ01e0H ~`H@pWآ f o=cbVw}3eK?rmG\ f4{ nYHŇv%Hd ֻ٫/ bmp<g4_we3xFW h@.m`ɏ+ӆ@ƛ3 mCCd[!-T w[C"hOCڼ1d1CvfWV&CѵJfH<=/iL^>"c5foφi6kuCiuc%xf"g ˊ;k ['3ѴJlatmn?D2dy``迷XtۘL53c-W1Ve6{X͆񙀎3n0vzS#t07ލ?l-X_ߘ޻6L:33٘Ÿ)Vz1 SNdL4b=xMlYLԩv֦>7}Q/cZԒc+w39q"T7hqeIHMnʎDnZ`Ӭtכa8~nv7m}k"t_u,o}gql9z a|[.Flu}6nn۳mk~}|VoWlwf˴`v+5*jMVc2͢4]fg;k |E6\żi,m-Л%Q\z-JoE,ueieːyvXe;!nhF V0deLoLZim,f[gV%,A0mhY͓tTmlݵ7n[;UӾl>mm 0gM};nμw9Ԟoc^fN_ǫf;('1t;S3Gvt[Wvl58aՙd&Jocr>WigN;ie\4.mb+~rgå{]okꮍ] v$^nܝc.6-wqw9y:qj]oy\S nicry.ahwN(l&9J#[K$m2R|(K"drI.r&u=ST⠢*&ԓݝ@dBRxmtHWD~5*zI7f³}l盍wCl|} X/L=oe'?sL&3Zά3;lfo^ $یRS^M rr+1_{ +*1*@ @\psp;v`0c4Up҈L8[l&JY6F +жC„ ǒ_puˇ;7[{q='#Q{{M1*3[\G#{hG?^ʈ|r%w]eUu2z <'giKn5*nhOtuOG֨5Y:%WK`Qw+1 {ߛ;n=Xyb)u9(JwGn=(EKB6y0fvV޲cPͶlp0yW N>d1m(j;/(Zz_rdanuaٽp!m(T,iVN {д\ ޻nE,Γ@+Dl1804c +Ѳ.Xzc]ͤzMI}FN&jsmB%ЪMrX;nV T6Ⱦd %ގQƴˤ70!Q*vl-HP4 nsGyX!|~Tv{ESpoc`ӜꑥXTJ#&OC]7N`_VG/=XcG(͎Ľ`g6 1Z)4q1$<v= Nڅ~bv{gqoxnFB1rW0{,YH9:2Aﲗ*l92`[4N,2YX]AbVKd>lj\SWe˶#]”݄i1 hbjc\{mKn*<9cp=h^pCty/fOVȵO:ӟ\o3 +~"hIF}GH8oG Uh(Cx@Z#5jſ F7Txot,7 >=+D55OcrJxB_Kt4ާx%j}$'_'4ߎ2ɡF?Y_ +y1^)ً_s5 lW]M);Pck7 +daOká:J/K ΤFNGbhK\#1؜Oh6bTJGtvB eiH1jAj^ْc6M;J@}D1]s3LdK = ՒC57kJ:7f.g7,領0Ff xqSQ3;͓*-϶UgLB9O.*L f)`\&=n2tb)ʞ٢7O7GwJ;Tb_4޳Nr20cSBAgbw܊?6vx:\a "ZWL8KZEfEĺIK[#-AȞ"n pP?jt!ɗ wx? ?c\ M>Jw 7F< ~ֽ[2F<*[d͆4"$MbS(d1H5y0BS3@|1_zz@O#I[-veX+U>^Vi$@80?(F7Aé1 W)7g4,+7o!ܛ!ar+_B Dlx":F)&,[>DRX,l%`D0y _qo< bʳ^n%U $C6P?/^ >W.ADY%ʶ,ib}IAs- d +ċ#dLbg#.=D{>P7ēy- I[MyxY\dbA!%稔,?3545Ap݀>ZQr{o3'D:o[U7wĺdI-3KS +js9\"ӒLtG=T.@v7{_YwbWdfSێ:xk\{! +tyo V~i\ O.49ΰX`gunӽTH` W@:+*& @uW{}Ue5XTMLQN؎9dx ȱY<ar4*DPe;3zŶ +c̓*v!=fWn yTlel,GdvWbޜmsz7CFq w7L؍@bZvRzDvC;1"OD?h +KId77 iMb'u9rx^lTHlOKF7Xna!. $1 $+D.%1H.A9k-팭\nszur j̇0Fscn{k=8>i.G+UX?vL7tkdIo1X$bͩL ~Jm, `-.f`FIY:~ ۘ8M߀,>}?՟#RɜN!U'1sH贾`?WN<dH]D`M/ŶʫTsM0ەh,!{tAŽ–iS,|hNׄ?ثx@G6&d-V`8%;@j p]i0SC@Ybq +V<v +07T/&3CCHc͛]em)SCk]g͆}"u_apby(gR9$|D5|}jմPE$4hۦ&i6#u_=;-lhɴRsM'w2ٟ} R`c"q uDaܰQ |©Y'(l aso|@J|/ ʲ +2^ Pb%Vc [ckW`8t,ξS'2ؚe( |- bGH EoaXwyD# D%i<݈'?RÇ ؏7g$L26n"d>LMXd©h8MX5ve*ώ h:;WNsG搼~ =ELohk;QQKk}di3߷ql& %K`yƃ#.ko6)g7nn"JwL4Fg@H+0=-p OjX=H 碭߼fo3blmjoSڭ@#:z$w XTׄ|q*`8m e~ o[<62$vF䏉hC-P_ +ѕI)|{D$6&M}b +Bʘ)-=@6|YluxƗU `W +J1 +4ֆ٠kJ6j[΍E2@ZDbs=sl*#Rx)b=1cc72d]𚟫WQوl"JnK-b~%=:taB=D,mSw@Mw%1{F,:rr ;W+as:4`1x E#J;*j-~,[ 4>@o³M{o@'0% 0J:Ѭ"3`=S0u23 اQ:n ݕ\$8߄Af]AGX7]]DߗN%k9#^S3&uPLl^5ɘ1@C- +po_H`Mv.$TEFFd w3K׾`XnŰ?wLǜA#eⵙ%aɝdL 4P)R⮇ ءDhF>v&L۞3 gz{yϦi6{jvDb1`$I_ih5T^M\d#򴀵&e>wc?'/8o' x\-XGk|mx>'MͱTmHm^o=섧{JklC@_`*-a2kElD~%rb|%,F>b"wQ ޵U4X^R̡s3%Ӄd5i-QxzʓͤI4~zVh(nJJ-n*Dy:P}YQY=[uR[~Ow|<55@.,SAavzlK?xuqy= LQ0j@NHYr>d8i|NT VJZm\p\YKPVlOLCAsykk||W=:^-ΗA SyJ'ցd75Z *oha1h)o-"@1ͻdG>aDT-SUh­x=(6+1"(ʺ]} +=^^{1d Pt7$Z`'=P8ޫXzirrnr?0z'|yɧ[H&BYtG1W=mk5x/5*!Joۺyƽٷݓ{KU_cEi !t=Zq$cɽNYEyn_5⸠ɹWrJu WvDO=eЈ4k/'{S>2%~q'٬#K=^kfҢhMsſlݣĊx>^.@}$@N.K]eu5(w->QC<}&R@v3"RHlt.bpy,(NKOmpu22Ex"V gHjSѥ޻gNi?_pgFq+6Q.rg/fzB^2ύL$8 <#Nj-I>}ӐN˸)YCR ͗lû%EYƔ۳g;S _3o~NPZڬסMs6 ! +_wן}cV+I.`*I{M;G4k0 PZ}1 %P#oPf^jPsY"@EH^7sUC &_ JP7Շ7w޲tOD3}憭F+%@Ek#̘nx\ '5<*Cͺb|WZ,CYC즥K~ +x]ݯH9^tAUYŸUYͷM%^1`9MV._PFš*4 Y^ߕ:¡\{*AJd"y{zPJd*Ts2bh}0(/}zuʮdP7.`iM76HDR>@#߬7qUJY0/]5m2ԕ-/E{AE`0zfޢW꫗h6m}E.+NIl'P{X jh-QT\ +XV"ԖiVڊ[C&5A@yg*B}˪P pv;UZqWvP?'*TsᄴtzNTP1ො=Esuc]`k 4kNd[\nx6]oAe8t>4x89Jԟn7O7i + r߫CZ* m>OAGJOH#MWG{4z՟Gw04h^-?T̎c}4M9:=4"RsӕFC]+UŞujo<"ԔL&BwECEv)4:@P~#pGf<1 fPwuL-ӤmLF >U>BE1fob@* htԠ{uPbS*R4u:OexH@)$ttI|ڃqv։0u {e\8@)IY#sO)`Hc2|Y; !FT5M1 + Q ++kצq/ƾVO t3n +";7CJBoҰ$cl4d"y=X{~l6:O)4I&T, T!sj -F5$ YFQ[bGaH=k e$_9R4f:՛._MV Σ[^4)?֗A0 q[.Bi \G+\KpȏXCLUS֎їFFw^|8; GY=i}(*q9&2*Mx{]19n/x/ #glZ{P[%DG џA{;M I!F{/Tv'{≔=s / +&ۀc!݀ ؀]j@Ng6 C0^[nUE~#Camp1>z֥mt1)izKGDPDHXN,Kv&B{ZA~'2JER6o%Eƍa흒UJUVεe ٚF5㇣kLHS +Nڊi@~8|5]&q&{h y9荸(!(_"m/ %VxtCt- AEͦS4iZ' y|p*iǓ ^`߬X*%w! %K܋ )e+v*nQX*,'Èi9$*AKD Xd!u6#+PW}Kzlϡ?<␭>&:C v$?oٞ:ҹzᣆ~}΁{9Bp[WrC[e(ͭdn-<?ꍸ5BUlJTZnݚc)p7P/js7QϹtUᠾ Aު+POn".wz9nv@:=wC<AtѴlQ+.+9\]om>,V{2 f7Z/C{6vM2ҹI=`ǽΌPtY'$>,彝rX>og*F!iI`(OSF~x:@C,Nǎm$7OKhr +b uk/q/m#&UC>j GIͥ : +G֐(/ⱉ*L5FvuMazؙ\~ަslˑv֥ ap.*)>38͞VBb)Lws/12;}uPwrY &{TIώvGKrt7Ї`TbтztET@hyn%d|!*Ypq6'-FU6<$y{uȦ^2MT>&Ju;t>\>Xy:v.|ڹ|W8|ڹ|Wşiˤi].0O;΋i].,O;Ot>\>فN{>O},{{<:m>m Uu869NQTG-pPթ(/n:2VpJ9@lLJ݈?7響ԊD{ Y: i90T> ar:NLbvw"py2Yڳ\i[q~Zm(tJ=G# [fɜ2u12Vr3&8Zkҝt9D*K#@Mz9=/c[O;3MAS]᫓9~gL'o[uI)g:1T$,l "^O#N!z^ifިvN^<7TRFi_p43vFI&nMS/FbIN}JQQCn{>N:P:[){"D?'xuFvc{0mGZל,T%ցBXrt8)TFTQi(N8]F1,aﴼS;<:5L3ӯR>O7W$Y[ Nzovp;+;³ Vػa:ziLȐ,ꍍGtY5⯯=t|8@ P]T* TF ;X+(@WY8wnp+d|u[d:wަ73Ľ۟Fɓ.JIfb0,ʩlfޯagth.KdkgXZo;iD~y<d2N':J㼵_^ʵNwGh.*?1d o +޾A<şMW EciV:f_nTVM}L,i_ [?WmqCn8%cn*'{ FOoqz8Θ٩WR: N*2N98b$jɈGhXE>'K=HDy=%X|uzQz?vۥGbAFBZx-C'P1ӮL#bS "A &ɇտ%)Y q>%ğv>%W5s>H')eIepw&O+Rpy>?IS:ٻ/اd]ˤ%Dy)T3V~'ޒ.ا""~ا1&)!o.wJPbRW?NSts.YHYb.اw;Kwb{.ا'HXdbR +ĽPbߩt/اJr{>//اEBTbt|gCK#7M-g9nFA G5*-O=rrq^TYT$>҃چ"N<8:h8gr63rjϢaPJ;:)a>=YW'㊪gqJz<ة·EfN_ql'P;/$:dvP\ovICOSrF)t\*\ّ\s2UPWn}SgX>5.y!3t$4t260bS6}3TL௚ڇ٦&ƀ;1<7?YJ~蟦$O#!?KIԛo~7~qJ|#KD -RO;%q?y(Vx%/{)V(#c?.V(5)/x넲KD^. VQms|(x(]x%).x`؟/KࡾT}e +js Z!Ru,q1Pn8tVsV"zN7_n/nc#5CQQs0iCƇ^㍙vL|5ʁ_eeuVٝRi߮{V9YϣiAiPiL_? &϶5f2ڥp7|!xbxo!Tn,\m̎h(ޝ/^ +P%7bL$]|ݭD||9`Rħcn~TL+uѐѧ+ev Y6TgoY'Y.Z}:J&n3+竿޻y_> BbKj(*/N:4=1OsDp'W*7/MX܌ N[%5toxP,^1ՓPJkaojmkW\=J"1*m%O;ҩC4\+mk32@ֶ6n+h;CY;Sh%D<&!XK8Z_um^ǫ(E3@Oޡ)FC,̵%v5q4'/V"iϚ}`c.f$*Ƨm,&9aavLҞCF\>y>m쥃_rvଭz/6żmws82sL|p;J!ڛ^nz>گҬ='e_aD/~=͏"F.}t2uvRb:t8C)A㖼0D]"sk0Jҁd:$0SYhY"Ff}:ئY3oC"W63$5t-#EKqo]8 dy,}dg8iűx|D{'EȒg;24mβX1>J1Cuv] h̬w?&.xh #}K_p6i#z!νpg98Y$__$(1 054$t%ƛcf-=F;RqR|7X;. It}|!H.jɎњֲ $t鷈!>Uⳬ%#S|y. C Ut~i@u~KW:ί<-7/i"Eƒ魛b9[vז/?G3a` 'm LG/z( Ɲ_( SRaA;'>b^5UcI60pܲx*&, ,/qR)v3h?ID&=R +?"ڬxyגi^qw-]zz-Q1Vnz;%_ƃo,',!ŀބMǂ\UeLCa\^7Wq/y_RoQtǯYw; +Lm.E0(- VDϔWNCR CJdiwN蝹9jdjDh7ʰr`rܦ-WLV?H.,Ok;J G+6+$.Rݮ?RX,6O⚲ @ 0gG4\GqvWg׭pAsG|"jXŝ3;/[wוv)~mc5n,JL9(N*f!rH쪰j+:иAew?FgsR[뻜6xQ'5I%_X pLG%,:P> V7\B9f6wl9)kEP>.J˦&lTB]Z ɺ^aQzYVS`S;jdd_؊xKH\!2%@u1ğVfp-pZQ֛mwǏs+~aGjK9*l8cS1]kP2 +ۈGm =R“[mcOBM$…DVNv6%[ 3fw;|\m&l^g;W(Tk QUJ[[ə+ƜS&2{6ׅws* +lWcv$žhmMO\ON|_ތmSi˷l9J5Iy.ɨ3ZRXɉ${F=ּw6dOv)2&B23WLv=6,D8Jt_Ͼm:4&twovYiޢC:wWC~>3SۖߒLr i)#H2Y iMz s3 jV>#ߞ~{:^4qE<>J͍s^Şmyx}\j7H޷`˫=&b6>L~OOs9[cd<=|?1E+o$[ Lpv#c\ Ƭ6s 9+m +\'S ie^>QxwxvrNq1AGRĸ}xxywWߴKҒCŞɯ}_fZ㓣06wxTQwSwl`z&VˆdPb 5Y"(x%OXmpnS}-CR*6n6FO?J1,>mʝVUQKvp[uӷ1kH8tCӪh6KQUqyf|J.t*(uQ+]^ertU|=%0mXHhacl;B +nuTSpn +6yZv:^%)dF^?mr\L]rRʢgWɒ(.vɀCf^1G_Z㋦uf@xP(}Vk,\<>4Z.kݕx>sg& +#&{Ff?}]?,D;hЕ-q<x+_.u\/׮R2WJh?n+J{<`KpB >,^Wjzp{aZЅ9%Osp;IA J; Or.'dPp'"pP.IZh s}庞oAT U +=H.1܀:q;@`&W^gsM[* !@6 xe.G1sIHZ,ψ|b6Z/'Lbp!|?, r2  4 +ϰ:]l |.B>1Zc=I[ڇq#!q\7K͒@hA/ 3xP4R寮FXln #sP[/ T_=b`?:z$AW}H )J`{eh1C( +| +XC܄*<q5x߹ @yw=+~/,9PHc,υqʃҞt WC:$z`"\R9/ްSTJ&@eDpua<MxC0 R@#zY(W@ +`ZE>L[Eq8$"KFysdK[*Ua2S$M])A^Dy +6 F 6Rh6?(bsx +`:@mm;m*@i +P1tBAa6t2CloB8oGG\vԒAa3s>_J?anrjՂ 9ƂWrI{\񀴆@A;`=|Hr xe(/ߧ+ܬ v}׌̓RBmHl<\K^ =Al+0=w0nWP bXhXA$4Jb䠓ɬB!.E 9H\ +T   A*c6UM)'V~° BOJto15HAĒ}0tS!J ! d}7 +8pM3P0\kWsT` +lui}7hmb( 6:~><V iIϋ5Q E +D,dW;.3A P% FKGD/G\n+:}n 0tEbyNTQp1t5+9V!F Cf(BT+Ŗ= A'}\°R1aÆڗ҇t(ȑ2XG03-/xm`]FP@|¾)h" $A uAPHC{pD0,GC`Y3fCC˷LJ.IU" ¤# q:Di 3A'!hkhN OC\0 DCYBWFNwf.l{gAR 7褁$lE~; tI?fBVp}DXpCCno_n<\5[kfgFl:߿z>jys% Tlm5n^|d˺o=>\ο +kݻO; ⓨO*~UIT'PEpwCě/a2x[z "JZyyim6ZY!}ldŒs|xyljBh]ngJȌ$#y3!3w3Y7MZݬd<͇XO'7uY~n/02}YN~f&RQ wS[{?u@N!\XJ pЅ:-ϹҦ:Iݍ.O?qo?KD]< A"0Z 844E%>L4`6 NRd95](+a( #a2LOUokl"LVQ_3QJV>Q9$o"`imrʦ(e{PW{<\qud yF.GقlBrm {< &է_S-:z)\*kf(i0B 1ih,{lQ0TpOΤ94p@~ƚhQU C/Nɟ |eHycݍ a 4d0Z fVo0Js#6ovmI$YTX5HlI5)OFMʷRUy=ҕ.h+Zp:GDk#qݩ`oB߀!`Y{dA8zK!'eh(Z@\iB'o$pTA7Cpb* `P)j023"c +)V*%vAinj $l%! +QG(}V, hޓS#=i޶!| J0%AO~l{T%Q,FCQG)Pf}Rd}Ue {`ũLL7(# +d]PAb 3LCB#aD@+X}*b$4 |+4!pR*؋m!ax8"p|a.P}r18dž@{5 IS; Kmk}MHC%e75 j@|k**j5 3+RQnPqbPpf +k@8(YmB p6bb jz>) ȹ`TeX#RD/CW)pgǠ˾@uc[%Qx>`® +UP(##@kMEiP&sq'F)aM-b@4dv\Xy`jK4ȶl<*</y<}nT)(kI>84i}_{$ˇ̘ <-AM`lO*u{GMˀJf +=hL63;pr|UPk( cp XK0{&.8Їr.˨SǪN/$. <o:a=r1`HަeX Ol;T]"Ie3@(2@D|eH&[ChK[KK9+y_&ܗ8t& -#WH F%{WGtΨ+,a 0)yEP&ICA&mKHP"c[.Lr9]r`IuҞI&BQ7-rxϐqM޵p%QGF>0<`aP)u2Ps0=8U2RpN23C E,tH=uBCib'xvˀX哳kwB + |[?FS +WPj~% -*Kb\uAPWИX+k)Z6/>Q(0]g? |Әl +ŜzK1)4I*oPz)wQ6~o+@~`ǨK\@i}?&+ D}P~7+>½2H52iɕ.+ҶZ(Nb7N0G]EY:Lvrr)s)w)dWJ1CSAqO4Oi^l%VB +%AZ f!Z$p$3r]Dfh#Ӈ' &Dg]:OʠSw1OG +A` W VU r|N$g:9fDdžu>/tK[XIk9ާTzV@!B!DМ5YA)oݳuD&I 3$I(%ad7u :Hv鈽ytZRABd>aʼTLh\7wZ;zs㿚4ppj><+Pz "7 +endstream endobj 5 0 obj <> endobj 15 0 obj [/View/Design] endobj 16 0 obj <>>> endobj 28 0 obj [27 0 R] endobj 47 0 obj <> endobj xref +0 48 +0000000004 65535 f +0000000016 00000 n +0000000159 00000 n +0000040046 00000 n +0000000000 00000 f +0000362235 00000 n +0000000000 00000 f +0000040097 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000362305 00000 n +0000362336 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000044021 00000 n +0000362421 00000 n +0000040497 00000 n +0000047005 00000 n +0000044321 00000 n +0000044208 00000 n +0000042844 00000 n +0000043459 00000 n +0000043507 00000 n +0000044092 00000 n +0000044123 00000 n +0000044356 00000 n +0000047079 00000 n +0000047341 00000 n +0000048520 00000 n +0000053840 00000 n +0000119429 00000 n +0000185018 00000 n +0000250607 00000 n +0000316196 00000 n +0000362446 00000 n +trailer +<<1FDE96F1CABB0B4F89FCA30211D923F0>]>> +startxref +362640 +%%EOF diff --git a/messenger/assets/lock.svg b/messenger/assets/lock.svg new file mode 100644 index 0000000..7e57b44 --- /dev/null +++ b/messenger/assets/lock.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/messenger/assets/mascot.svg b/messenger/assets/mascot.svg new file mode 100644 index 0000000..b0c637a --- /dev/null +++ b/messenger/assets/mascot.svg @@ -0,0 +1 @@ +mascot \ No newline at end of file diff --git a/messenger/assets/message-background.png b/messenger/assets/message-background.png new file mode 100644 index 0000000..92ccca8 Binary files /dev/null and b/messenger/assets/message-background.png differ diff --git a/messenger/assets/message-background.svg b/messenger/assets/message-background.svg new file mode 100644 index 0000000..fcc5c00 --- /dev/null +++ b/messenger/assets/message-background.svg @@ -0,0 +1 @@ +Hi \ No newline at end of file diff --git a/messenger/assets/message-background@2x.png b/messenger/assets/message-background@2x.png new file mode 100644 index 0000000..6acc805 Binary files /dev/null and b/messenger/assets/message-background@2x.png differ diff --git a/messenger/assets/message-background@4x.png b/messenger/assets/message-background@4x.png new file mode 100644 index 0000000..8f1366f Binary files /dev/null and b/messenger/assets/message-background@4x.png differ diff --git a/messenger/assets/messaging@4x.png b/messenger/assets/messaging@4x.png new file mode 100644 index 0000000..c4f3474 Binary files /dev/null and b/messenger/assets/messaging@4x.png differ diff --git a/messenger/assets/messenger-favicon_1.png b/messenger/assets/messenger-favicon_1.png new file mode 100644 index 0000000..79ea100 Binary files /dev/null and b/messenger/assets/messenger-favicon_1.png differ diff --git a/messenger/assets/messenger-illustration.svg b/messenger/assets/messenger-illustration.svg new file mode 100644 index 0000000..053bd91 --- /dev/null +++ b/messenger/assets/messenger-illustration.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/messenger/assets/new conversation.svg b/messenger/assets/new conversation.svg new file mode 100644 index 0000000..c010274 --- /dev/null +++ b/messenger/assets/new conversation.svg @@ -0,0 +1,8 @@ + +new conversation + + + + + + \ No newline at end of file diff --git a/messenger/assets/no-mails.svg b/messenger/assets/no-mails.svg new file mode 100644 index 0000000..7d43864 --- /dev/null +++ b/messenger/assets/no-mails.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/messenger/css/main.css b/messenger/css/main.css new file mode 100644 index 0000000..13f835f --- /dev/null +++ b/messenger/css/main.css @@ -0,0 +1,2655 @@ +* { + padding: 0; + margin: 0; + box-sizing: border-box; + font-family: "Roboto", sans-serif; +} + +:root { + font-size: clamp(1rem, 1.2vmax, 1.2rem); +} + +html, +body { + height: 100%; + view-transition-name: none; +} + +body { + --accent-color: #3d5afe; + --secondary-color: #ffac2e; + --text-color: 20, 20, 20; + --foreground-color: 252, 253, 255; + --background-color: 241, 243, 248; + --danger-color: rgb(255, 75, 75); + --green: #1cad59; + --yellow: rgb(220, 165, 0); + --dark-red: #d40e1e; + --red: #f50000; + --kinda-pink: #e40273; + --purple: #462191; + --shady-blue: #324de6; + --nice-blue: #3d5afe; + --maybe-cyan: #00b0ff; + --teal: #00bcd4; + --mint-green: #16c79a; + --yellowish-green: #66bb6a; + --greenish-yellow: #8bc34a; + --dark-teal: #11698e; + --tangerine: #ff6f00; + --orange: #ff9100; + --redish-orange: #ff3d00; + color: rgba(var(--text-color), 1); + background-color: rgba(var(--background-color), 1); + overflow-y: hidden; +} +body #scroll_to_bottom { + background: rgba(var(--foreground-color), 1); + box-shadow: 0 0.3rem 0.4rem rgba(0, 0, 0, 0.2); +} + +body[data-theme=dark] { + --accent-color: #6d83ff; + --secondary-color: #d60739; + --text-color: 220, 220, 220; + --foreground-color: 27, 28, 29; + --background-color: 21, 22, 22; + --danger-color: rgb(255, 106, 106); + --green: #00e676; + --yellow: rgb(255, 213, 5); + --dark-red: #ff5e7e; + --red: #ff6098; + --kinda-pink: #c44ae6; + --purple: #9565f7; + --shady-blue: #8295fb; + --nice-blue: #6d83ff; + --maybe-cyan: #66cfff; + --teal: #6aeeff; + --mint-green: #4dffd2; + --yellowish-green: #9effa2; + --greenish-yellow: #c7fc8b; + --dark-teal: #51cbff; + --tangerine: #ffac6d; + --orange: #ffbe68; + --redish-orange: #ff8560; +} +body[data-theme=dark] .initial { + box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.16); +} +body[data-theme=dark] .message { + color: rgba(var(--text-color), 1); +} +body[data-theme=dark] #scroll_to_bottom { + background: linear-gradient(rgba(var(--text-color), 0.1), rgba(var(--text-color), 0.1)), rgba(var(--foreground-color), 1); + box-shadow: 0 0.4rem 0.4rem rgba(0, 0, 0, 0.3); +} + +p, +strong { + font-size: 0.9rem; + max-width: 65ch; + line-height: 1.7; + color: rgba(var(--text-color), 0.9); +} + +.info { + line-height: normal; + padding: 1rem; + border-radius: 0.5rem; + font-weight: 500; + color: rgba(0, 0, 0, 0.7); +} +.info--warning { + background-color: khaki; +} +.info--error { + background-color: var(--danger-color); +} + +a { + text-decoration: none; + color: var(--accent-color); +} +a:focus-visible { + box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 1) inset; +} + +button, +.button { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + position: relative; + display: inline-flex; + border: none; + background-color: transparent; + overflow: hidden; + color: inherit; + -webkit-tap-highlight-color: transparent; + align-items: center; + font-size: 0.9rem; + font-weight: 500; + white-space: nowrap; + padding: 0.8rem; + border-radius: 0.5rem; + justify-content: center; +} +button:focus-visible, +.button:focus-visible { + outline: var(--accent-color) solid medium; +} +button:not(:disabled), +.button:not(:disabled) { + cursor: pointer; +} + +.button { + background-color: rgba(var(--text-color), 0.02); + border: solid thin rgba(var(--text-color), 0.06); +} +.button--primary { + padding: 0.6rem 1rem; + color: rgba(var(--background-color), 1); + background-color: var(--accent-color); +} +.button--primary .icon { + fill: rgba(var(--background-color), 1); +} +.button--colored { + color: var(--accent-color); +} +.button--colored .icon { + fill: var(--accent-color); +} +.button--danger { + background-color: rgba(255, 115, 115, 0.062745098); + color: var(--danger-color); +} +.button--danger .icon { + fill: var(--danger-color); +} +.button--small { + padding: 0.4rem 0.6rem; +} +.button--outlined { + border: solid var(--accent-color) 1px; + background-color: transparent; + color: var(--accent-color); +} +.button--outlined .icon { + fill: var(--accent-color); +} +.button--transparent { + background-color: transparent; +} + +button:disabled { + opacity: 0.4; + cursor: not-allowed; + filter: saturate(0); +} + +.cta { + text-transform: uppercase; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.05em; + padding: 0.8rem 1rem; +} + +.icon { + width: 1.2rem; + height: 1.2rem; + fill: rgba(var(--text-color), 0.8); + flex-shrink: 0; +} + +.icon-only { + padding: 0.5rem; + border-radius: 0.3rem; + background-color: transparent; + aspect-ratio: 1/1; + flex-shrink: 0; +} + +a:-webkit-any-link:focus-visible { + outline: rgba(var(--text-color), 1) 0.1rem solid; +} + +a:-moz-any-link:focus-visible { + outline: rgba(var(--text-color), 1) 0.1rem solid; +} + +a:any-link:focus-visible { + outline: rgba(var(--text-color), 1) 0.1rem solid; +} + +details[open] summary { + margin-bottom: 1rem; +} +details[open] > summary .down-arrow { + transform: rotate(180deg); +} +details summary { + display: flex; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + cursor: pointer; + align-items: center; + justify-content: space-between; +} +details .down-arrow { + margin-left: 0.5rem; +} + +fieldset { + border: none; +} + +input { + accent-color: var(--accent-color); +} +input[type=range]:active { + cursor: -webkit-grab; + cursor: grab; +} + +sm-copy { + font-size: 0.9rem; +} + +sm-input, +sm-textarea { + font-size: 0.9rem; + --border-radius: 0.5rem; + --background-color: rgba(var(--foreground-color), 1); +} +sm-input button .icon, +sm-textarea button .icon { + fill: var(--accent-color); +} + +sm-textarea { + --max-height: 32ch; +} + +sm-spinner { + --size: 1rem; + --stroke-width: 0.1rem; +} + +sm-form { + --gap: 1rem; +} + +sm-select { + --padding: 0.8rem; + font-size: 0.9rem; +} + +sm-option { + font-size: 0.9rem; +} + +sm-chips { + --gap: 0.3rem; +} + +sm-chip { + position: relative; + font-size: 0.9rem; + --border-radius: 0.5rem; + --padding: 0.5rem 0.6rem; + --background: rgba(var(--text-color), 0.06); + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} +sm-chip[selected] { + color: rgba(var(--background-color), 1); + --background: rgba(var(--text-color), 0.9); +} + +sm-popup::part(popup) { + max-height: calc(100vh - 2rem); + background-color: rgba(var(--foreground-color), 1); +} + +ul, +ol { + list-style: none; +} + +ol { + counter-reset: item; +} +ol li { + position: relative; + display: flex; + align-items: flex-start; + counter-increment: item; +} +ol li:not(:last-of-type) { + padding-bottom: 1.5rem; +} +ol li:not(:last-of-type)::after { + content: ""; + position: absolute; + width: 0.1rem; + height: calc(100% - 2.2rem); + background: var(--accent-color); + margin-left: 0.7rem; + margin-top: 2rem; +} +ol li::before { + content: counter(item); + display: flex; + align-items: center; + justify-content: center; + text-align: center; + font-size: 0.8rem; + font-weight: 500; + margin-top: 0.15rem; + margin-right: 1rem; + line-height: 1; + width: 1.5rem; + height: 1.5rem; + border-radius: 100%; + flex-shrink: 0; + color: rgba(var(--text-color), 0.8); + background: rgba(var(--text-color), 0.1); +} + +.overflow-ellipsis { + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.wrap-around { + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.full-bleed { + grid-column: 1/-1; +} + +.uppercase { + text-transform: uppercase; +} + +.capitalize { + text-transform: capitalize; +} + +.sticky { + position: -webkit-sticky; + position: sticky; +} + +.top-0 { + top: 0; +} + +.flex { + display: flex; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.flex-1 { + flex: 1; +} + +.grid { + display: grid; +} + +.flow-column { + grid-auto-flow: column; +} + +.gap-0-3 { + gap: 0.3rem; +} + +.gap-0-5 { + gap: 0.5rem; +} + +.gap-1 { + gap: 1rem; +} + +.gap-1-5 { + gap: 1.5rem; +} + +.gap-2 { + gap: 2rem; +} + +.gap-3 { + gap: 3rem; +} + +.text-align-right { + text-align: right; +} + +.align-start { + align-content: flex-start; +} + +.align-center { + align-items: center; +} + +.align-end { + align-items: flex-end; +} + +.text-center { + text-align: center; +} + +.justify-start { + justify-items: start; +} + +.justify-center { + justify-content: center; +} + +.justify-right { + margin-left: auto; +} + +.align-self-center { + align-self: center; +} + +.align-self-end { + align-self: end; +} + +.justify-self-center { + justify-self: center; +} + +.justify-self-start { + justify-self: start; +} + +.justify-self-end { + justify-self: end; +} + +.flex-direction-column { + flex-direction: column; +} + +.space-between { + justify-content: space-between; +} + +.w-100 { + width: 100%; +} + +.h-100 { + height: 100%; +} + +.margin-right-0-3 { + margin-right: 0.3rem; +} + +.margin-right-0-5 { + margin-right: 0.5rem; +} + +.margin-left-0-5 { + margin-left: 0.5rem; +} + +.margin-left-auto { + margin-left: auto; +} + +.margin-right-auto { + margin-right: auto; +} + +.margin-top-1 { + margin-top: 1rem; +} + +.margin-bottom-0-5 { + margin-bottom: 0.5rem; +} + +.margin-bottom-1 { + margin-bottom: 1rem; +} + +.margin-block-1 { + margin-block: 1rem; +} + +.margin-block-1-5 { + margin-block: 1.5rem; +} + +.margin-inline-1 { + margin-inline: 1rem; +} + +.margin-inline-1-5 { + margin-inline: 1.5rem; +} + +.label { + font-size: 0.8rem; + color: rgba(var(--text-color), 0.8); + font-weight: 500; + margin-bottom: 0.2rem; +} + +.button--primary .ripple, +.button--danger .ripple { + background: radial-gradient(circle, rgba(var(--background-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%); +} + +.ripple { + height: 8rem; + width: 8rem; + position: absolute; + border-radius: 50%; + transform: scale(0); + background: radial-gradient(circle, rgba(var(--text-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%); + pointer-events: none; +} + +.interactive { + position: relative; + overflow: hidden; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.empty-state { + display: grid; + width: 100%; + padding: 1.5rem 0; +} + +.observe-empty-state:empty { + display: none !important; +} + +.observe-empty-state:not(:empty) + .empty-state { + display: none !important; +} + +.bullet-point { + display: flex; + align-items: center; + justify-content: center; + margin: 0 0.8ch; +} +.bullet-point::after { + content: ""; + height: 0.4ch; + width: 0.4ch; + border-radius: 0.5em; + background-color: var(--accent-color); +} + +.icon-button { + padding: 0.6rem; + border-radius: 0.8rem; + background-color: rgba(var(--text-color), 0.1); + height: -webkit-max-content; + height: -moz-max-content; + height: max-content; +} +.icon-button .icon { + fill: var(--accent-color); +} + +.fab { + position: absolute; + right: 0; + bottom: 0; + margin: 1.5rem; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.2); + z-index: 2; + border-radius: 0.5rem; + padding: 0.8rem 1rem; +} +.fab .icon { + margin-right: 0.5rem; +} + +.multi-state-button { + display: grid; + text-align: center; + align-items: center; + justify-items: center; +} +.multi-state-button > * { + grid-area: 1/1/2/2; +} +.multi-state-button button { + z-index: 1; + width: 100%; +} + +.password-field label { + display: flex; + justify-content: center; +} +.password-field label input:checked ~ .visible { + display: none; +} +.password-field label input:not(:checked) ~ .invisible { + display: none; +} + +.page { + height: 100%; +} +.page__header { + display: flex; + justify-content: space-between; + margin-bottom: 1.5rem; + min-height: 8rem; +} +.page__header .grid { + margin-top: auto; +} +.page__header h1 { + margin-top: auto; + font-size: 2rem; +} + +.card { + background-color: rgba(var(--text-color), 0.06); + border-radius: 0.5rem; + padding: 1rem; +} + +.page-layout { + display: grid; + gap: 1.5rem 0; + grid-template-columns: 1.5rem minmax(0, 1fr) 1.5rem; + align-content: flex-start; +} +.page-layout > * { + grid-column: 2/3; +} + +#confirmation_popup, +#prompt_popup { + flex-direction: column; +} +#confirmation_popup h4, +#prompt_popup h4 { + font-size: 1.2rem; + margin-bottom: 1rem; +} +#confirmation_popup .flex, +#prompt_popup .flex { + margin-top: 1rem; +} +#confirmation_popup sm-input, +#prompt_popup sm-input { + margin-top: 2rem; +} + +.popup__header { + position: relative; + display: grid; + gap: 0.5rem; + width: 100%; + padding: 0 1.5rem; + align-items: center; +} +.popup__header > * { + grid-row: 1; +} +.popup__header h3, +.popup__header h4 { + grid-column: 1/-1; + justify-self: center; + align-self: center; +} +.popup__header__close { + grid-column: 1; + margin-left: -1rem; + justify-self: flex-start; +} + +#secondary_pages { + display: flex; + flex-direction: column; + width: 100%; +} +#secondary_pages header { + padding: 1.5rem; +} + +.inner-page { + display: grid; + position: relative; + grid-template-columns: minmax(0, 1fr); + height: 100%; + width: 100%; +} + +#sign_in { + justify-items: center; + align-content: center; + padding: 1.5rem; + margin: 0; +} +#sign_in section { + margin-top: -8rem; + width: min(26rem, 100%); +} +#sign_in sm-form { + margin: 2rem 0; +} + +#sign_up { + justify-items: center; + align-content: center; +} +#sign_up keys-generator { + width: min(26rem, 100%); + margin-top: -4rem; +} +#sign_up .h2 { + margin-bottom: 0.5rem; +} + +.tip { + font-size: 0.9rem; + color: rgba(var(--text-color), 0.8); +} + +.danger { + color: var(--error-color); +} + +.app-brand { + display: flex; + gap: 0.3rem; + align-items: center; +} +.app-brand .icon { + height: 1.7rem; + width: 1.7rem; +} + +.app-name__company { + font-size: 0.8rem; + font-weight: 500; + color: rgba(var(--text-color), 0.8); +} + +.select-file input[type=file] { + display: none; +} + +#landing { + display: grid; + border-radius: 0.6rem; + width: 100%; + height: 100%; + padding: 0 1.5rem; + align-items: center; +} +#landing .logo-section { + padding: 1.5rem; + display: flex; +} +#landing .title-font { + line-height: 1.2; + font-weight: 700; + font-size: 2.5rem; +} +#landing .left { + display: grid; + flex-direction: column; + padding-bottom: 1.5rem; + z-index: 1; +} +#landing .left h4 { + color: var(--accent-color); + margin-bottom: 0.5rem; + font-weight: 500; +} +#landing .left h3 { + margin-bottom: 1rem; + font-weight: 500; +} +#landing .left p { + font-size: 1.1rem; + color: rgba(var(--text-color), 0.8); +} +#landing .left .button { + padding: 0.8rem 1.5rem; +} +#landing .left .flex .button:first-of-type { + margin-right: 0.5rem; +} + +#landing_illustration { + position: relative; + width: 100%; +} + +@-webkit-keyframes slide-down { + from { + transform: translateY(-1rem); + } + to { + transform: none; + } +} + +@keyframes slide-down { + from { + transform: translateY(-1rem); + } + to { + transform: none; + } +} +#loading { + height: 100%; + display: grid; + place-content: center; + justify-items: center; +} + +.page__loader { + z-index: 1; + transform-origin: bottom; + height: 6rem; + width: 6rem; + -webkit-animation: bounce 0.5s infinite alternate ease-in; + animation: bounce 0.5s infinite alternate ease-in; +} + +.shadow { + margin-top: -1rem; + width: 5rem; + height: 2rem; + background: rgba(var(--text-color), 0.1); + border-radius: 50%; + -webkit-animation: scale 0.5s infinite alternate ease-in; + animation: scale 0.5s infinite alternate ease-in; + margin-left: 1rem; +} + +.page__tag-line { + margin-top: 2rem; +} + +@-webkit-keyframes bounce { + 0% { + transform: scaleY(1) translateY(-4rem); + } + 90% { + transform: scaleY(1) translateY(0); + } + 100% { + transform: scaleY(0.8); + } +} + +@keyframes bounce { + 0% { + transform: scaleY(1) translateY(-4rem); + } + 90% { + transform: scaleY(1) translateY(0); + } + 100% { + transform: scaleY(0.8); + } +} +@-webkit-keyframes scale { + 0% { + transform: scale(0.5); + } + 90% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + } +} +@keyframes scale { + 0% { + transform: scale(0.5); + } + 90% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + } +} +#background_overlay, +#background_image { + position: fixed; + height: 100%; + width: 100%; +} + +#background_overlay { + background-color: red; + z-index: -1; + background-color: rgba(var(--foreground-color), var(--opacity, 0.6)); + transition: background-color 0.3s; +} + +#background_image { + z-index: -2; + -o-object-fit: cover; + object-fit: cover; + filter: blur(var(--blur, 1rem)); + transform: scale(calc(1 + var(--scale, 1.1) / 10)); +} +#background_image[src=""], #background_image:not([src]) { + display: none; +} + +#main_page { + display: grid; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: 1fr auto; +} + +.prime-action { + display: flex; + gap: 0.5rem; + justify-content: flex-start; +} + +.initial { + position: relative; + justify-content: center; + font-size: 1.1rem; + font-weight: 700; + width: 2.4rem; + height: 2.4rem; + aspect-ratio: 1/1; + color: rgba(var(--foreground-color), 1); + box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.06); + border-radius: 2rem; + text-transform: uppercase; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + overflow: hidden; + background-color: var(--contact-color, --accent-color); + flex-shrink: 0; + transition: background-color 0.3s; +} +.initial::after { + content: ""; + position: absolute; + background-color: rgba(255, 255, 255, 0.2); + width: 100%; + height: 200%; + margin-left: 50%; + margin-top: 50%; + transform: rotate(45deg); + transform-origin: left center; +} + +.group-icon { + height: 1.6rem; + width: 1.6rem; + fill: rgba(var(--foreground-color), 1); +} + +.contact { + position: relative; + display: grid; + gap: 0 1rem; + align-items: center; + flex-shrink: 0; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + overflow: hidden; +} +.contact > *:empty { + display: none; +} +.contact.highlight { + -webkit-animation: highlight 0.5s ease-in-out 4 alternate; + animation: highlight 0.5s ease-in-out 4 alternate; +} +.contact:not(.chat) { + grid-template-columns: auto 1fr; + grid-template-areas: "dp name" "dp ."; +} +.contact.chat, .contact.group { + grid-template-columns: auto 1fr auto; + grid-template-areas: "dp name time" "dp . menu"; +} +.contact.pipeline { + grid-template-columns: auto 1fr auto; + grid-template-areas: "dp tag time" "dp name name" "dp . menu"; +} +.contact.pipeline .name { + margin: 0.5rem 0 0.3rem 0; +} +.contact.admin { + grid-template-columns: auto 1fr auto; +} +.contact.selected { + background-color: rgba(var(--text-color), 0.06); +} +.contact .initial { + grid-area: dp; +} +.contact .name, +.contact .last-message { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + color: rgba(var(--text-color), 0.8); +} +.contact .tag { + grid-area: tag; + font-size: 0.8rem; + padding: 0.2rem 0.4rem; + background-color: rgba(var(--text-color), 0.6); + color: rgba(var(--foreground-color), 1); + border-radius: 0.3rem; + margin-right: auto; + font-weight: 500; + justify-self: flex-start; +} +.contact.collapsed.chat, .contact.collapsed.group { + grid-template-areas: "dp name menu"; + grid-template-rows: 1fr; +} +.contact.collapsed.pipeline { + grid-template-areas: "dp tag tag" "dp name menu"; +} +.contact.collapsed .name { + margin-bottom: 0; +} +.contact .name { + grid-area: name; + margin-bottom: 0.3rem; +} +.contact__flo-address { + font-size: 0.8rem; + color: rgba(var(--text-color), 0.6); +} +.contact .span-2 { + display: flex; + justify-content: space-between; + grid-column: 2/4; + width: 100%; + gap: 0.5rem; +} +.contact .last-message { + font-weight: 400; + font-size: 0.9em; + opacity: 0.9; +} +.contact .menu { + grid-area: menu; + flex-shrink: 0; + justify-self: flex-end; + padding: 0.2rem; + fill: rgba(var(--text-color), 1); +} +.contact .time { + color: rgba(var(--text-color), 0.7); + grid-area: time; + font-size: 0.8rem; +} + +.name { + width: 100%; + font-size: 1em; + font-weight: 500; +} + +.selectable-contact, +.group-member, +.blocked-id, +.contact-list__item { + gap: 1rem; + padding: 0.5rem 0; +} + +.selectable-contact { + margin: 0 -0.3rem; + padding: 0.5rem 0.3rem; + border-radius: 0.5rem; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} +.selectable-contact:not(:last-of-type) { + margin-bottom: 0.5rem; +} +.selectable-contact input { + margin-left: auto; + height: 1.3em; + width: 1.3em; +} +.selectable-contact button { + margin-left: auto; + min-width: -webkit-fit-content; + min-width: -moz-fit-content; + min-width: fit-content; +} + +.group-member { + display: flex; + align-items: center; + font-size: 0.9rem; +} +.group-member .admin-tag { + white-space: nowrap; + margin-left: auto; + padding: 0.1rem 0.6rem; + font-size: 0.8rem; + border-radius: 3rem; + background-color: rgba(var(--text-color), 0.1); +} + +.contact-list__item { + display: flex; + text-align: left; + justify-content: flex-start; + width: 100%; + padding: 0.5rem; +} +.contact-list__item:focus { + background-color: rgba(var(--text-color), 0.03); +} + +#selected_contacts_container { + display: flex; + overflow: auto hidden; +} +#selected_contacts_container:not(:empty) { + padding: 0.5rem 0; + margin-bottom: 0.5rem; + border-bottom: solid thin rgba(var(--text-color), 0.1); +} +#selected_contacts_container .contact-preview { + display: flex; + flex-shrink: 0; + align-items: center; + cursor: pointer; + margin-right: 0.3rem; + background: rgba(var(--text-color), 0.1); + padding-left: 0.2rem; + border-radius: 2rem; + transform-origin: left; + overflow: hidden; +} +#selected_contacts_container .contact-preview .initial { + width: 1.6rem; + height: 1.6rem; + font-size: 0.9rem; +} +#selected_contacts_container .contact-preview .name { + font-size: 0.9rem; + color: rgba(var(--text-color), 0.8); + margin-left: 0.5rem; +} + +#contact_details_popup .popup-section { + margin: 1.5rem 0; +} +#contact_details_popup h5 { + font-weight: 500; + opacity: 0.8; +} +#contact_details_popup .group-icon { + padding: 0.2rem; + height: 3rem; + width: 3rem; +} + +#contact_initial { + height: 4.6rem; + width: 4.6rem; + font-size: 2.4rem; + border-radius: 4rem; + margin-top: 3rem; + margin-bottom: 0.5rem; + background-color: var(--contact-color, --accent-color); +} + +#contact_name { + margin: 0.5rem 0; +} +#contact_name::part(text) { + font-size: 1.2rem; + font-weight: 500; +} + +#search_contacts { + position: -webkit-sticky; + position: sticky; + top: -1rem; + z-index: 1; +} +#search_contacts::after { + content: ""; + position: absolute; + left: 0; + bottom: 0; + height: calc(100% + 1rem); + width: 100%; + background: rgba(var(--foreground-color), 1); + z-index: -1; +} + +.event-card { + padding: 0.4rem 0.6rem; + font-weight: 500; + font-size: 0.85rem; + background-color: rgba(var(--text-color), 0.04); + border-radius: 0.5rem; + color: rgba(var(--text-color), 0.8); + margin: 1rem 0; + justify-self: center; + align-self: center; + text-align: center; +} +.event-card + .event-card { + margin-top: 0; +} + +.group-event-card { + font-weight: 400; +} + +#warn_no_encryption { + background: rgb(255, 253, 141); + color: #111; +} + +.pipeline-event { + padding: 0.8rem 1rem; + border: solid thin rgba(var(--text-color), 0.2); + text-align: start; +} +.pipeline-event--signed .icon { + fill: var(--green); +} +.pipeline-event .time { + font-size: 0.8rem; + color: rgba(var(--text-color), 0.7); +} + +.contact, +.mail-card { + padding: 0.8rem; + margin: 0 0.2rem; + border-radius: 0.5rem; +} + +.contact::before, +.mail-card::before { + content: ""; + position: absolute; + top: 0; + margin: 0.5rem; + padding: 0.3rem; + background: var(--accent-color); + border-radius: 100%; + border: solid rgba(var(--foreground-color), 1) 1px; + transform: scale(0); + transition: transform 0.3s; +} + +.contact.unread::before, +.mail-card.unread::before { + transform: scale(1); +} + +.mail-card.unread .time, +.mail-card.unread .date, +.contact.unread .time, +.contact.unread .date { + color: var(--accent-color); +} +.mail-card.unread h4, +.contact.unread h4 { + color: rgba(var(--text-color), 1); +} +.mail-card.unread h4, +.mail-card.unread h5, +.mail-card.unread p, +.contact.unread h4, +.contact.unread h5, +.contact.unread p { + font-weight: 700; +} + +.mail-card { + position: relative; + display: grid; + gap: 0 1rem; + align-items: center; + flex-shrink: 0; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + grid-template-columns: auto 1fr auto; + grid-template-areas: "dp sender date" "dp subject subject" "dp desc desc"; +} +.mail-card .initial { + grid-area: dp; + align-self: flex-start; + font-size: 1rem; +} +.mail-card .sender { + width: 100%; + font-size: 0.8rem; + font-weight: 500; + color: rgba(var(--text-color), 0.8); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} +.mail-card .subject { + grid-area: subject; + font-size: 1em; + margin-top: 0.3rem; + font-weight: 500; +} +.mail-card .description { + grid-area: desc; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + font-size: 0.9em; + margin-top: 0.2rem; + color: rgba(var(--text-color), 0.8); +} + +.date { + grid-area: date; + margin-left: auto; + white-space: nowrap; + font-size: 0.8rem; + color: rgba(var(--text-color), 0.8); +} + +@-webkit-keyframes slide { + from { + opacity: 0; + transform: translateX(-1rem); + } + to { + opacity: 1; + transform: none; + } +} + +@keyframes slide { + from { + opacity: 0; + transform: translateX(-1rem); + } + to { + opacity: 1; + transform: none; + } +} +#mail_container { + width: 100%; +} + +.mail { + position: relative; + padding: 1.5rem; +} +.mail:not(:first-of-type) { + margin-top: 1.5rem; + background-color: rgba(var(--text-color), 0.03); + padding-bottom: 2rem; + margin-inline: 1rem; + border-radius: 0.5rem; + overflow: hidden; +} +.mail:not(:first-of-type)::before { + content: ""; + position: absolute; + left: 0; + top: 0; + width: 0.2rem; + height: 100%; + background: rgba(var(--text-color), 0.2); +} +.mail .mail-header { + align-self: start; + margin-bottom: 1.5rem; + gap: 1rem; +} +.mail .initial { + background-color: var(--contact-color, --accent-color); + margin: 0; + height: 2rem; + width: 2rem; +} +.mail .sender-name { + font-weight: 500; + margin-bottom: 0.5rem; +} +.mail .flo-id { + font-weight: 400; + max-width: 90%; +} +.mail .mail-subject, +.mail .mail-content { + overflow-wrap: break-word; + word-wrap: break-word; +} +.mail .mail-subject { + margin-bottom: 0.4em; +} +.mail .mail-content { + height: -webkit-max-content; + height: -moz-max-content; + height: max-content; + max-width: 60ch; + white-space: pre-wrap; +} + +.logo-section { + display: grid; + grid-template-columns: auto 1fr; +} + +#main_navbar { + display: flex; + background: rgba(var(--foreground-color), 1); +} +#main_navbar.hide-away { + position: absolute; +} +#main_navbar ul { + display: flex; + height: 100%; + width: 100%; +} +#main_navbar ul li { + width: 100%; +} + +.nav-item { + position: relative; + display: grid; + width: 100%; + align-items: center; + justify-items: center; + padding: 0.5rem 0.4rem; + color: var(--text-color); + font-size: 0.8rem; + border-radius: 0.5rem; + font-weight: 500; + aspect-ratio: 1/1; +} +.nav-item .icon { + grid-area: 1/1/2/2; + transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} +.nav-item .filled { + opacity: 0; +} +.nav-item__title { + line-height: 1; + transition: opacity 0.2s, transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} +.nav-item--active { + color: var(--accent-color); +} +.nav-item--active .icon { + fill: var(--accent-color); + transform: translateY(50%); +} +.nav-item--active .icon.filled { + opacity: 1; +} +.nav-item--active .icon:not(.filled) { + opacity: 0; +} +.nav-item--active .nav-item__title { + transform: translateY(100%); + opacity: 0; +} +.nav-item__indicator { + position: absolute; + bottom: 0; + width: 2rem; + height: 0.3rem; + background: var(--accent-color); + border-radius: 1rem 1rem 0 0; + z-index: 1; +} +.nav-item:last-of-type { + margin-top: auto; +} + +.badge { + display: flex; + align-items: center; + justify-content: center; + content: attr(data-notifications); + position: absolute; + top: 0; + right: 0; + min-width: 2.7ch; + font-size: 0.8rem; + padding: 0.2rem; + background: var(--danger-color); + color: rgba(var(--background-color), 1); + line-height: 1; + font-weight: 700; + border-radius: 1rem; + margin: 0.3rem; +} + +#contacts, +#mails, +#settings { + height: 100%; + overflow-y: hidden; + background-color: rgba(var(--foreground-color), 0.3); +} +#contacts .header, +#mails .header, +#settings .header { + padding: 1rem; + position: relative; + gap: 0.5rem; +} +#contacts .header h4, +#mails .header h4, +#settings .header h4 { + text-transform: capitalize; + font-weight: 500; +} +#contacts sm-menu, +#mails sm-menu, +#settings sm-menu { + margin-right: -0.7rem; +} + +#auto_complete_contact { + position: relative; +} + +#mail_contact_list { + max-height: 40vh; + overflow-y: auto; + position: absolute; + top: 100%; + background: rgba(var(--foreground-color), 1); + z-index: 1; + border-radius: 0.4rem; + box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.2rem 0.5rem rgba(0, 0, 0, 0.16); + width: 100%; +} +#mail_contact_list .contact { + grid-template-columns: auto 1fr; + grid-template-areas: "dp ." "dp ."; +} +#mail_contact_list sm-menu { + display: none; +} + +#contacts { + position: relative; + overflow-x: hidden; + grid-template-rows: -webkit-max-content 1fr; + grid-template-rows: max-content 1fr; + grid-template-columns: minmax(0, 1fr); +} +#contacts .header { + display: flex; + justify-content: space-between; + padding: 0.5rem 1rem; +} +#contacts .scrolling-wrapper { + height: 100%; + flex: 1; + overflow-y: auto; +} +#contacts .scrolling-wrapper .empty-state { + padding: 1.5rem; + text-align: center; +} + +.user-profile-button { + background-color: rgba(var(--text-color), 0.06); + border-radius: 2rem; + font-size: 0.8rem; + padding: 0.6rem 0.8rem; + flex: 1; +} + +sm-chip .badge { + position: relative; + margin: -0.2rem 0 -0.2rem 0.5rem; + height: 1.5rem; + min-width: 3.3ch; + background-color: var(--accent-color); + color: rgba(var(--background-color), 1); +} + +#contacts_container .contact { + padding: 0.5rem 0; +} +#contacts_container .contact:not(:last-of-type) { + margin-bottom: 0.5rem; +} + +#chat_sections { + display: grid; + grid-template-columns: minmax(0, 1fr); + overflow-y: auto; + height: 100%; + overflow-x: hidden; +} +#chat_sections > * { + grid-area: 1/1/2/2; +} + +#notifications_wrapper { + width: 100%; + height: 100%; + padding: 1rem; +} +#notifications_wrapper .icon-only { + margin-left: -0.5rem; +} + +#notifications_list { + margin-top: 0.5rem; +} + +.notification { + padding: 1rem; + background-color: rgba(var(--text-color), 0.06); + border-radius: 0.5rem; + margin: 0 -0.8rem; + gap: 0.8rem; +} +.notification:not(:last-of-type) { + margin-bottom: 0.3rem; +} +.notification__message { + line-height: normal; +} +.notification__time { + font-size: 0.8rem; + color: rgba(var(--text-color), 0.7); +} + +#creation_process { + overflow-x: hidden; +} +#creation_process .group-icon { + background-color: var(--accent-color); + justify-self: center; + height: 8rem; + width: 8rem; + margin-bottom: 1rem; + padding: 2rem; + border-radius: 50%; + font-size: 4rem; +} + +#mails { + position: relative; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: -webkit-max-content 1fr; + grid-template-rows: max-content 1fr; +} + +#mail_sections { + overflow-y: auto; +} + +#mail_type_selector { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} +#mail_type_selector sm-chip .badge { + margin: 0rem; +} + +#search_chats { + --min-height: 2.5rem; + width: 100%; +} + +#chat_page, +#mail_page, +#settings { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + overflow: hidden; +} + +#group_members_list { + margin-top: 0.5rem; +} +#group_members_list .contact { + padding: 0.5rem 0; +} + +#chat_view, +#mail { + background-color: rgba(var(--foreground-color), 0.3); +} + +#chat_view { + position: relative; + display: flex; + flex-direction: column; + height: 100%; + overflow-y: auto; +} + +#transaction_details { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 2; + background-color: rgba(var(--foreground-color), 1); + border: solid thin rgba(var(--text-color), 0.2); + color: rgba(var(--text-color), 0.8); + margin: 1.5rem auto; + -webkit-backdrop-filter: blur(1rem); + backdrop-filter: blur(1rem); + box-shadow: 0 1rem 1.5rem rgba(0, 0, 0, 0.1); + view-transition-name: transaction-details; + padding: 1rem; + border-radius: 0.85rem; +} +#transaction_details[open] { + padding: 1.5rem; +} + +::view-transition-new(transaction-details), +::view-transition-old(transaction-details) { + width: 100%; + height: 100%; +} + +.message { + position: relative; + display: flex; + flex-wrap: wrap; + width: auto; + font-size: 0.92rem; + max-width: -webkit-max-content; + max-width: -moz-max-content; + max-width: max-content; + margin-bottom: 0.2rem; + margin-top: 0.8rem; + padding: 0.5em 0.8em; + transition: opacity 0.3s, transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} +.message .sender-name { + font-size: 0.85rem; + font-weight: 500; + margin-bottom: 0.3rem; +} +.message .message-body { + display: flex; + align-items: center; + flex-wrap: wrap; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-all; + word-break: break-word; + -webkit-hyphens: auto; + hyphens: auto; + white-space: pre-wrap; + line-height: 1.5; +} +.message .message-body a { + color: inherit; + text-decoration: underline; +} +.message .message-body .text-emoji { + align-self: center; + font-size: 1.4em; + letter-spacing: 0; +} +.message .time { + white-space: nowrap; + font-size: 0.8em; + opacity: 0.8; + justify-self: flex-end; + padding-left: 1rem; + align-self: flex-end; + margin-top: 0.2rem; + margin-left: auto; +} + +.sent { + margin-left: auto; + background: var(--accent-color); + border-radius: 0.5rem 0 0.5rem 0.5rem; +} +.sent > * { + color: rgba(var(--background-color), 1); +} +.sent::after { + content: ""; + position: absolute; + left: 100%; + top: 0; + width: 0; + height: 0; + border-style: solid; + border-width: 0.5em 0.3em 0 0; + border-color: var(--accent-color) transparent transparent transparent; +} + +.received { + margin-right: auto; + border-radius: 0 0.5rem 0.5rem 0.5rem; + background-color: rgba(var(--text-color), 0.1); +} +.received::after { + content: ""; + position: absolute; + left: -0.5em; + top: 0; + width: 0; + height: 0; + border-style: solid; + border-width: 0 0.5em 0.5em 0; + border-color: transparent rgba(var(--text-color), 0.1) transparent transparent; +} + +.sent + .sent, +.received + .received { + margin-top: 0; +} + +.sent + .sent::after, +.received + .received::after { + display: none; +} + +.sent + .sent, +.received + .received { + border-radius: 0.5rem; +} + +.distinct-sender { + display: grid; + border-radius: 0 0.5rem 0.5rem 0.5rem !important; + margin-top: 0.8rem !important; +} +.distinct-sender::after { + display: flex !important; + content: ""; + position: absolute; + left: -0.5em; + top: 0; + width: 0; + height: 0; + border-style: solid; + border-width: 0 0.5em 0.5em 0; + border-color: transparent rgba(var(--text-color), 0.1) transparent transparent; +} + +.unconfirmed { + opacity: 0.7; + transform-origin: left; + -webkit-animation: pop 0.3s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275); + animation: pop 0.3s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +@-webkit-keyframes pop { + 0% { + transform: translate(-0.5rem, 1rem); + } + 100% { + transform: translate(0, 0); + } +} + +@keyframes pop { + 0% { + transform: translate(-0.5rem, 1rem); + } + 100% { + transform: translate(0, 0); + } +} +.back-button { + padding: 0.5rem; + margin-left: -0.8rem; +} + +#chat_header { + padding: 0.5rem 1rem; + align-content: center; + grid-template-columns: minmax(0, 1fr); + z-index: 1; +} +#chat_header .flex { + height: 100%; +} +#chat_header .initial { + cursor: pointer; + height: 1.8rem; + width: 1.8rem; + flex-shrink: 0; + font-size: 1rem; +} +#chat_header .group-icon { + padding: 0.1rem; +} + +#chat_details_button { + position: relative; + background-color: rgba(var(--text-color), 0.06); + border-radius: 2rem; + justify-self: center; + padding: 0.3rem; + view-transition-name: chat-button; +} + +::view-transition-old(chat-button), +::view-transition-new(chat-button) { + height: 100%; + width: 100%; +} + +#receiver_name { + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} +#receiver_name:has(span) { + display: grid; + place-content: center; + place-items: center; +} +#receiver_name > * { + grid-area: 1/1/2/2; +} + +#scroll_to_bottom { + position: absolute; + display: flex; + right: 0; + bottom: 3rem; + border-radius: 4rem; + z-index: 1; + aspect-ratio: 1/1; + margin: 1.5rem; + transform: scale(0); + transition: transform 0.2s; +} +#scroll_to_bottom button { + border-radius: 4rem; +} +#scroll_to_bottom.new-message::after { + position: absolute; + content: ""; + top: 0; + right: 0; + z-index: 2; + padding: 0.5rem; + border-radius: 50%; + background: #00e676; +} + +.no-transformations { + transform: none !important; +} + +#chat_footer .flex { + align-items: flex-end; + padding: 0.5rem 1rem 0.5rem 0.5rem; +} + +#emoji_toggle { + align-self: center; + padding: 0.6rem; + margin-right: 0.5rem; + width: 2.6rem; + height: 2.6rem; + border-radius: 2rem; + cursor: pointer; +} +#emoji_toggle path { + fill: rgba(var(--text-color), 0.5); +} +#emoji_toggle.active path { + fill: var(--accent-color); +} + +#send_message_button { + align-self: center; + padding: 0.5rem 0.8rem; + margin-left: 0.5rem; + opacity: 0.5; + font-weight: 500; +} +#send_message_button:not(:disabled) { + opacity: 1; + color: var(--accent-color); +} + +#type_message { + margin: 0; + --background: rgba(var(--text-color), 0.1); +} + +#messages_container { + flex: 1; + padding: 0 1rem; +} + +#emoji_picker { + --background: rgba(var(--text-color), 0.06); + --border-size: 0; + --input-border-color: none; + --input-padding: 0.4rem 1rem; + --outline-color: var(--accent-color); + --input-font-color: rgba(var(--text-color), 1); + --input-placeholder-color: rgba(var(--text-color), 0.6); + --indicator-color: var(--accent-color); + --button-hover-background: rgba(var(--text-color), 0.2); + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + width: 100%; + max-height: 40vh; +} + +.emoji { + font-size: 1.6rem; + cursor: pointer; + padding: 0.4rem; + border-radius: 0.6rem; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + text-align: center; +} + +.icon--medium, +.icon--big { + margin-bottom: 1.5rem; + justify-self: center; +} + +.icon--medium { + height: 4rem; + width: 4rem; +} + +.icon--big { + height: 8rem; + width: 8rem; +} + +#messages_container, +#chats_list, +#inbox_mail_container, +#sent_mail_container, +#mail { + width: 100%; + flex-direction: column; + height: 100%; + overflow-y: auto; +} + +#chats_list { + gap: 0.2rem; + padding-bottom: 6rem; +} + +.mail-container { + height: 100%; + flex-direction: column; + overflow-y: auto; +} + +#inbox_mail_container, +#sent_mail_container { + gap: 0.2rem; + padding-bottom: 6rem; +} + +.has-bg-image .received::after { + border-color: transparent rgba(var(--foreground-color), 0.6) transparent transparent; +} +.has-bg-image #emoji_picker { + --background: rgba(var(--foreground-color), 0.6); +} +.has-bg-image #emoji_toggle path { + fill: rgba(var(--text-color), 0.8); +} +.has-bg-image sm-textarea { + --background: rgba(var(--foreground-color), 0.6); +} + +#mail { + align-items: flex-start; + padding-bottom: 1.5rem; +} +#mail > .flex { + padding: 0 1.5rem; + margin-top: 2rem; +} +#mail > .flex button:first-of-type { + margin-right: 0.5rem; +} + +.sidebar-item { + display: flex; + align-items: center; + padding: 1rem 0.8rem; + text-transform: capitalize; + font-weight: 500; + opacity: 0.9; + color: inherit; + margin: 0 0.2rem; + border-radius: 0.5rem; +} +.sidebar-item:not(:last-of-type) { + margin-bottom: 0.2rem; +} +.sidebar-item .icon { + margin-right: 1em; + width: 1em; +} + +#settings .card { + display: flex; + flex-direction: column; + margin: 0 0.5rem; + width: calc(min(36rem, 100%) - 1rem); +} +#settings #settings_title { + text-transform: capitalize; +} +#settings #settings_sidebar { + height: 100%; +} +#settings #settings_panel { + overflow-y: auto; + height: 100%; +} +#settings .panel { + padding-bottom: 6rem; +} +#settings .panel > * { + margin: 0 auto; +} +#settings #sign_out::part(button) { + color: var(--error-color); +} +#settings #bg_preview_container { + padding: 1rem 0; + gap: 0.5rem; + flex-wrap: wrap; +} +#settings .bg-preview { + position: relative; + display: grid; + place-items: center; + height: 8rem; + width: 8rem; + cursor: pointer; + border-radius: 1rem; + overflow: hidden; + background: rgba(var(--text-color), 0.1); +} +#settings .bg-preview--selected::after { + content: ""; + position: absolute; + height: 100%; + width: 100%; + border-radius: inherit; + box-shadow: 0 0 0 0.5rem var(--accent-color) inset; +} +#settings .bg-preview input { + display: none; +} +#settings .bg-preview__image { + -o-object-fit: cover; + object-fit: cover; + height: 100%; + width: 100%; +} + +.option { + width: 100%; + justify-content: flex-start; + padding: 0.8rem 0; + color: var(--accent-color); +} +.option .icon { + fill: var(--accent-color); + margin-right: 0.5rem; +} +.option--danger { + color: var(--danger-color); +} +.option--danger .icon { + fill: var(--danger-color); +} + +#contact_options .option { + justify-content: center; + background-color: rgba(var(--text-color), 0.04); +} + +.multisig-option { + position: relative; + padding: 1rem; + font-size: 0.9rem; + border-bottom: thin solid rgba(var(--text-color), 0.3); +} +.multisig-option .wrap-around { + font-weight: 500; +} +.multisig-option__label { + font-weight: 500; + font-size: 1rem; +} +.multisig-option__balance { + color: rgba(var(--text-color), 0.8); +} +.multisig-option .icon-only { + padding: 0.3rem; +} +.multisig-option.highlight { + -webkit-animation: highlight 0.5s ease-in-out infinite alternate; + animation: highlight 0.5s ease-in-out infinite alternate; +} + +@-webkit-keyframes highlight { + 0% { + background-color: rgba(var(--text-color), 0.1); + } + 100% { + background-color: transparent; + } +} + +@keyframes highlight { + 0% { + background-color: rgba(var(--text-color), 0.1); + } + 100% { + background-color: transparent; + } +} +.multisig-type-button { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 1rem; + justify-items: flex-start; + background-color: rgba(var(--text-color), 0.04); + padding: 1.5rem 1.2rem; + font-size: 1rem; + text-align: start; +} + +.remove-card-wrapper { + min-height: 2rem; +} + +.receiver-card { + display: grid; + gap: 0.5rem; + padding: 0.5rem 0; + border: none; +} +.receiver-card:not(:last-of-type) { + border-bottom: solid thin rgba(var(--text-color), 0.3); +} + +#selected_fee_tip, +#error_section { + font-weight: 500; +} + +.error { + color: var(--danger-color); +} +.error .icon { + fill: var(--danger-color); +} + +#send_fee_wrapper { + display: grid; +} +#send_fee_wrapper > * { + grid-area: 1/1; +} + +#send_fee_loader { + background-color: rgba(var(--foreground-color), 1); + z-index: 1; + width: 100%; +} + +@media screen and (max-width: 640px) { + sm-popup { + --border-radius: 1rem 1rem 0 0; + } + #landing { + grid-template-areas: "illustration" "."; + align-items: flex-start; + } + #landing .title-font { + font-size: 2rem; + font-weight: 500; + } + #landing_illustration { + grid-area: illustration; + } + .inner-page { + margin-bottom: 5rem; + } + #main_navbar { + background-color: transparent; + -webkit-backdrop-filter: blur(0.5rem); + backdrop-filter: blur(0.5rem); + background-color: rgba(var(--foreground-color), 0.9); + } + #main_navbar ul { + background-color: rgba(var(--text-color), 0.05); + border-radius: inherit; + box-shadow: 0 1rem 1.5rem rgba(0, 0, 0, 0.16); + } + #main_navbar.hide-away { + bottom: 0; + left: 0; + right: 0; + } + #main_navbar .nav-item { + height: 3.8rem; + width: 4.5rem; + margin: 0 auto; + } + #chats_list, +#contact_container { + gap: 0.2rem; + } + #chat_view .message { + width: auto; + max-width: 90%; + } + #chat_header { + grid-template-columns: auto minmax(0, 1fr); + } + #chat_header #chat_details_button { + max-width: calc(100% - 2rem); + margin-left: -1.7rem; + } + #settings { + overflow-x: hidden; + } + #settings #settings_header { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1; + padding: 1rem 1.5rem; + margin-bottom: 0.5rem; + background: linear-gradient(rgba(var(--background-color), 0.8), rgba(var(--background-color), 0)); + -webkit-backdrop-filter: blur(0.5rem); + backdrop-filter: blur(0.5rem); + align-items: flex-start; + } + .hide-on-mobile { + display: none !important; + } +} +@media screen and (min-width: 40rem) { + .hide-on-desktop { + display: none !important; + } + .page { + padding-bottom: 0; + } + .card { + padding: 1.5rem; + } + .card button, +.card .button { + align-self: flex-start; + } + .popup__header { + grid-column: 1/-1; + padding: 1rem 1.5rem 0 1.5rem; + } + .logo-section { + padding: 2rem 3rem 0 3rem; + margin: 0.5rem 0; + } + sm-popup { + --width: 25rem; + --min-width: 25rem; + --border-radius: 0.5rem; + } + #multisig_tx_popup { + --width: 28rem; + } + #landing { + align-items: center; + gap: 4vw; + grid-template-columns: 1fr 1fr; + padding: 0 4vw; + } + #main_page { + grid-template-columns: -webkit-min-content 1fr; + grid-template-columns: min-content 1fr; + grid-template-rows: 1fr; + grid-template-areas: "nav ."; + overflow: hidden; + box-shadow: 0 0.1rem 0.2rem rgba(0, 0, 0, 0.05), 0 1rem 3rem rgba(0, 0, 0, 0.2); + } + #main_navbar { + grid-area: nav; + border-top: none; + flex-direction: column; + border-right: solid thin rgba(var(--text-color), 0.1); + background-color: rgba(var(--foreground-color), 0.3); + } + #main_navbar ul { + flex-direction: column; + gap: 0.5rem; + } + #main_navbar ul li:last-of-type { + margin-top: auto; + } + .nav-item__indicator { + width: 0.25rem; + height: 50%; + left: 0; + border-radius: 0 1rem 1rem 0; + bottom: auto; + } + #profile_popup { + --width: 32rem; + } + #add_contact_popup { + --min-width: 24rem; + } + #compose_mail_popup, +#reply_mail_popup { + --min-width: 36rem; + } + #chat_details_button { + background-color: transparent; + } + #pseudo_background { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(var(--text-color), 0.06); + border-radius: inherit; + } + #emoji_picker { + max-height: 18rem; + } + #chat_view .message { + width: auto; + align-self: flex-start; + max-width: 55ch; + } + #chat_page, +#mail_page { + display: grid; + grid-template-columns: 19rem 1fr; + } + #chat_page > :first-child, +#mail_page > :first-child { + border-right: solid thin rgba(var(--text-color), 0.1); + } + #settings { + display: grid; + grid-template-columns: 14rem 1fr; + } + #settings > :first-child { + border-right: solid thin rgba(var(--text-color), 0.1); + } + #settings .active { + background: rgba(var(--text-color), 0.1); + } + #settings .panel { + padding: 1.5rem; + } + .contact.active, +.mail-card.active { + background: rgba(var(--text-color), 0.06); + } + #contact_details_popup.is-group { + --width: 52rem; + } + #contact_details_popup.is-group #contact_details_section { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; + } + #contact_details_popup.is-group #contact_details_section > :first-child { + padding-right: 1.5rem; + border-right: thin solid rgba(var(--text-color), 0.3); + } +} +@media only screen and (max-width: 1280px) { + .hide-on-medium { + display: none !important; + } +} +@media only screen and (min-width: 1080px) { + #chat_view #messages_container { + padding: 1rem 4rem; + padding-top: 0; + } + #chat_page, +#mail_page { + grid-template-columns: 21rem 1fr; + } +} +@media only screen and (min-width: 1280px) { + #landing { + gap: 4vw; + padding: 0 8vw; + } + #landing .title-font { + font-size: 3rem; + } + #emoji_picker { + --num-columns: 16; + } + .contact.active, +.mail-card.active { + background: rgba(var(--text-color), 0.06); + } +} +@media (hover: hover) { + ::-webkit-scrollbar { + width: 0.5rem; + height: 0.5rem; + } + ::-webkit-scrollbar-thumb { + background: rgba(var(--text-color), 0.3); + border-radius: 1rem; + } + ::-webkit-scrollbar-thumb:hover { + background: rgba(var(--text-color), 0.5); + } + .interactive:hover { + background-color: rgba(var(--text-color), 0.06); + } + .emoji:hover { + cursor: pointer; + background: rgba(var(--text-color), 0.06); + } + .contact .menu { + opacity: 0; + transition: opacity 0.3s; + } + .contact:hover .menu { + opacity: 1; + } +} +@media (hover: none) { + .contact-preview, +.contact { + -webkit-tap-highlight-color: transparent; + } + .contact .menu { + display: none; + } +} +@supports (overflow: overlay) { + body { + overflow: overlay; + } +} +.hidden { + display: none !important; +} \ No newline at end of file diff --git a/messenger/css/main.min.css b/messenger/css/main.min.css new file mode 100644 index 0000000..406b88c --- /dev/null +++ b/messenger/css/main.min.css @@ -0,0 +1 @@ +*{padding:0;margin:0;box-sizing:border-box;font-family:"Roboto",sans-serif}:root{font-size:clamp(1rem,1.2vmax,1.2rem)}html,body{height:100%;view-transition-name:none}body{--accent-color: #3d5afe;--secondary-color: #ffac2e;--text-color: 20, 20, 20;--foreground-color: 252, 253, 255;--background-color: 241, 243, 248;--danger-color: rgb(255, 75, 75);--green: #1cad59;--yellow: rgb(220, 165, 0);--dark-red: #d40e1e;--red: #f50000;--kinda-pink: #e40273;--purple: #462191;--shady-blue: #324de6;--nice-blue: #3d5afe;--maybe-cyan: #00b0ff;--teal: #00bcd4;--mint-green: #16c79a;--yellowish-green: #66bb6a;--greenish-yellow: #8bc34a;--dark-teal: #11698e;--tangerine: #ff6f00;--orange: #ff9100;--redish-orange: #ff3d00;color:rgba(var(--text-color), 1);background-color:rgba(var(--background-color), 1);overflow-y:hidden}body #scroll_to_bottom{background:rgba(var(--foreground-color), 1);box-shadow:0 .3rem .4rem rgba(0,0,0,.2)}body[data-theme=dark]{--accent-color: #6d83ff;--secondary-color: #d60739;--text-color: 220, 220, 220;--foreground-color: 27, 28, 29;--background-color: 21, 22, 22;--danger-color: rgb(255, 106, 106);--green: #00e676;--yellow: rgb(255, 213, 5);--dark-red: #ff5e7e;--red: #ff6098;--kinda-pink: #c44ae6;--purple: #9565f7;--shady-blue: #8295fb;--nice-blue: #6d83ff;--maybe-cyan: #66cfff;--teal: #6aeeff;--mint-green: #4dffd2;--yellowish-green: #9effa2;--greenish-yellow: #c7fc8b;--dark-teal: #51cbff;--tangerine: #ffac6d;--orange: #ffbe68;--redish-orange: #ff8560}body[data-theme=dark] .initial{box-shadow:0 .1rem .1rem rgba(0,0,0,.16)}body[data-theme=dark] .message{color:rgba(var(--text-color), 1)}body[data-theme=dark] #scroll_to_bottom{background:linear-gradient(rgba(var(--text-color), 0.1), rgba(var(--text-color), 0.1)),rgba(var(--foreground-color), 1);box-shadow:0 .4rem .4rem rgba(0,0,0,.3)}p,strong{font-size:.9rem;max-width:65ch;line-height:1.7;color:rgba(var(--text-color), 0.9)}.info{line-height:normal;padding:1rem;border-radius:.5rem;font-weight:500;color:rgba(0,0,0,.7)}.info--warning{background-color:khaki}.info--error{background-color:var(--danger-color)}a{text-decoration:none;color:var(--accent-color)}a:focus-visible{box-shadow:0 0 0 .1rem rgba(var(--text-color), 1) inset}button,.button{-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative;display:inline-flex;border:none;background-color:rgba(0,0,0,0);overflow:hidden;color:inherit;-webkit-tap-highlight-color:rgba(0,0,0,0);align-items:center;font-size:.9rem;font-weight:500;white-space:nowrap;padding:.8rem;border-radius:.5rem;justify-content:center}button:focus-visible,.button:focus-visible{outline:var(--accent-color) solid medium}button:not(:disabled),.button:not(:disabled){cursor:pointer}.button{background-color:rgba(var(--text-color), 0.02);border:solid thin rgba(var(--text-color), 0.06)}.button--primary{padding:.6rem 1rem;color:rgba(var(--background-color), 1);background-color:var(--accent-color)}.button--primary .icon{fill:rgba(var(--background-color), 1)}.button--colored{color:var(--accent-color)}.button--colored .icon{fill:var(--accent-color)}.button--danger{background-color:rgba(255,115,115,.062745098);color:var(--danger-color)}.button--danger .icon{fill:var(--danger-color)}.button--small{padding:.4rem .6rem}.button--outlined{border:solid var(--accent-color) 1px;background-color:rgba(0,0,0,0);color:var(--accent-color)}.button--outlined .icon{fill:var(--accent-color)}.button--transparent{background-color:rgba(0,0,0,0)}button:disabled{opacity:.4;cursor:not-allowed;filter:saturate(0)}.cta{text-transform:uppercase;font-size:.8rem;font-weight:700;letter-spacing:.05em;padding:.8rem 1rem}.icon{width:1.2rem;height:1.2rem;fill:rgba(var(--text-color), 0.8);flex-shrink:0}.icon-only{padding:.5rem;border-radius:.3rem;background-color:rgba(0,0,0,0);aspect-ratio:1/1;flex-shrink:0}a:-webkit-any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}a:-moz-any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}a:any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}details[open] summary{margin-bottom:1rem}details[open]>summary .down-arrow{transform:rotate(180deg)}details summary{display:flex;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer;align-items:center;justify-content:space-between}details .down-arrow{margin-left:.5rem}fieldset{border:none}input{accent-color:var(--accent-color)}input[type=range]:active{cursor:-webkit-grab;cursor:grab}sm-copy{font-size:.9rem}sm-input,sm-textarea{font-size:.9rem;--border-radius: 0.5rem;--background-color: rgba(var(--foreground-color), 1)}sm-input button .icon,sm-textarea button .icon{fill:var(--accent-color)}sm-textarea{--max-height: 32ch}sm-spinner{--size: 1rem;--stroke-width: 0.1rem}sm-form{--gap: 1rem}sm-select{--padding: 0.8rem;font-size:.9rem}sm-option{font-size:.9rem}sm-chips{--gap: 0.3rem}sm-chip{position:relative;font-size:.9rem;--border-radius: 0.5rem;--padding: 0.5rem 0.6rem;--background: rgba(var(--text-color), 0.06);-webkit-user-select:none;-moz-user-select:none;user-select:none}sm-chip[selected]{color:rgba(var(--background-color), 1);--background: rgba(var(--text-color), 0.9)}sm-popup::part(popup){max-height:calc(100vh - 2rem);background-color:rgba(var(--foreground-color), 1)}ul,ol{list-style:none}ol{counter-reset:item}ol li{position:relative;display:flex;align-items:flex-start;counter-increment:item}ol li:not(:last-of-type){padding-bottom:1.5rem}ol li:not(:last-of-type)::after{content:"";position:absolute;width:.1rem;height:calc(100% - 2.2rem);background:var(--accent-color);margin-left:.7rem;margin-top:2rem}ol li::before{content:counter(item);display:flex;align-items:center;justify-content:center;text-align:center;font-size:.8rem;font-weight:500;margin-top:.15rem;margin-right:1rem;line-height:1;width:1.5rem;height:1.5rem;border-radius:100%;flex-shrink:0;color:rgba(var(--text-color), 0.8);background:rgba(var(--text-color), 0.1)}.overflow-ellipsis{width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.wrap-around{overflow-wrap:break-word;word-wrap:break-word;word-break:break-word}.full-bleed{grid-column:1/-1}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.sticky{position:-webkit-sticky;position:sticky}.top-0{top:0}.flex{display:flex}.flex-wrap{flex-wrap:wrap}.flex-1{flex:1}.grid{display:grid}.flow-column{grid-auto-flow:column}.gap-0-3{gap:.3rem}.gap-0-5{gap:.5rem}.gap-1{gap:1rem}.gap-1-5{gap:1.5rem}.gap-2{gap:2rem}.gap-3{gap:3rem}.text-align-right{text-align:right}.align-start{align-content:flex-start}.align-center{align-items:center}.align-end{align-items:flex-end}.text-center{text-align:center}.justify-start{justify-items:start}.justify-center{justify-content:center}.justify-right{margin-left:auto}.align-self-center{align-self:center}.align-self-end{align-self:end}.justify-self-center{justify-self:center}.justify-self-start{justify-self:start}.justify-self-end{justify-self:end}.flex-direction-column{flex-direction:column}.space-between{justify-content:space-between}.w-100{width:100%}.h-100{height:100%}.margin-right-0-3{margin-right:.3rem}.margin-right-0-5{margin-right:.5rem}.margin-left-0-5{margin-left:.5rem}.margin-left-auto{margin-left:auto}.margin-right-auto{margin-right:auto}.margin-top-1{margin-top:1rem}.margin-bottom-0-5{margin-bottom:.5rem}.margin-bottom-1{margin-bottom:1rem}.margin-block-1{margin-block:1rem}.margin-block-1-5{margin-block:1.5rem}.margin-inline-1{margin-inline:1rem}.margin-inline-1-5{margin-inline:1.5rem}.label{font-size:.8rem;color:rgba(var(--text-color), 0.8);font-weight:500;margin-bottom:.2rem}.button--primary .ripple,.button--danger .ripple{background:radial-gradient(circle, rgba(var(--background-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%)}.ripple{height:8rem;width:8rem;position:absolute;border-radius:50%;transform:scale(0);background:radial-gradient(circle, rgba(var(--text-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%);pointer-events:none}.interactive{position:relative;overflow:hidden;cursor:pointer;-webkit-tap-highlight-color:rgba(0,0,0,0)}.empty-state{display:grid;width:100%;padding:1.5rem 0}.observe-empty-state:empty{display:none !important}.observe-empty-state:not(:empty)+.empty-state{display:none !important}.bullet-point{display:flex;align-items:center;justify-content:center;margin:0 .8ch}.bullet-point::after{content:"";height:.4ch;width:.4ch;border-radius:.5em;background-color:var(--accent-color)}.icon-button{padding:.6rem;border-radius:.8rem;background-color:rgba(var(--text-color), 0.1);height:-webkit-max-content;height:-moz-max-content;height:max-content}.icon-button .icon{fill:var(--accent-color)}.fab{position:absolute;right:0;bottom:0;margin:1.5rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.2);z-index:2;border-radius:.5rem;padding:.8rem 1rem}.fab .icon{margin-right:.5rem}.multi-state-button{display:grid;text-align:center;align-items:center;justify-items:center}.multi-state-button>*{grid-area:1/1/2/2}.multi-state-button button{z-index:1;width:100%}.password-field label{display:flex;justify-content:center}.password-field label input:checked~.visible{display:none}.password-field label input:not(:checked)~.invisible{display:none}.page{height:100%}.page__header{display:flex;justify-content:space-between;margin-bottom:1.5rem;min-height:8rem}.page__header .grid{margin-top:auto}.page__header h1{margin-top:auto;font-size:2rem}.card{background-color:rgba(var(--text-color), 0.06);border-radius:.5rem;padding:1rem}.page-layout{display:grid;gap:1.5rem 0;grid-template-columns:1.5rem minmax(0, 1fr) 1.5rem;align-content:flex-start}.page-layout>*{grid-column:2/3}#confirmation_popup,#prompt_popup{flex-direction:column}#confirmation_popup h4,#prompt_popup h4{font-size:1.2rem;margin-bottom:1rem}#confirmation_popup .flex,#prompt_popup .flex{margin-top:1rem}#confirmation_popup sm-input,#prompt_popup sm-input{margin-top:2rem}.popup__header{position:relative;display:grid;gap:.5rem;width:100%;padding:0 1.5rem;align-items:center}.popup__header>*{grid-row:1}.popup__header h3,.popup__header h4{grid-column:1/-1;justify-self:center;align-self:center}.popup__header__close{grid-column:1;margin-left:-1rem;justify-self:flex-start}#secondary_pages{display:flex;flex-direction:column;width:100%}#secondary_pages header{padding:1.5rem}.inner-page{display:grid;position:relative;grid-template-columns:minmax(0, 1fr);height:100%;width:100%}#sign_in{justify-items:center;align-content:center;padding:1.5rem;margin:0}#sign_in section{margin-top:-8rem;width:min(26rem,100%)}#sign_in sm-form{margin:2rem 0}#sign_up{justify-items:center;align-content:center}#sign_up keys-generator{width:min(26rem,100%);margin-top:-4rem}#sign_up .h2{margin-bottom:.5rem}.tip{font-size:.9rem;color:rgba(var(--text-color), 0.8)}.danger{color:var(--error-color)}.app-brand{display:flex;gap:.3rem;align-items:center}.app-brand .icon{height:1.7rem;width:1.7rem}.app-name__company{font-size:.8rem;font-weight:500;color:rgba(var(--text-color), 0.8)}.select-file input[type=file]{display:none}#landing{display:grid;border-radius:.6rem;width:100%;height:100%;padding:0 1.5rem;align-items:center}#landing .logo-section{padding:1.5rem;display:flex}#landing .title-font{line-height:1.2;font-weight:700;font-size:2.5rem}#landing .left{display:grid;flex-direction:column;padding-bottom:1.5rem;z-index:1}#landing .left h4{color:var(--accent-color);margin-bottom:.5rem;font-weight:500}#landing .left h3{margin-bottom:1rem;font-weight:500}#landing .left p{font-size:1.1rem;color:rgba(var(--text-color), 0.8)}#landing .left .button{padding:.8rem 1.5rem}#landing .left .flex .button:first-of-type{margin-right:.5rem}#landing_illustration{position:relative;width:100%}@-webkit-keyframes slide-down{from{transform:translateY(-1rem)}to{transform:none}}@keyframes slide-down{from{transform:translateY(-1rem)}to{transform:none}}#loading{height:100%;display:grid;place-content:center;justify-items:center}.page__loader{z-index:1;transform-origin:bottom;height:6rem;width:6rem;-webkit-animation:bounce .5s infinite alternate ease-in;animation:bounce .5s infinite alternate ease-in}.shadow{margin-top:-1rem;width:5rem;height:2rem;background:rgba(var(--text-color), 0.1);border-radius:50%;-webkit-animation:scale .5s infinite alternate ease-in;animation:scale .5s infinite alternate ease-in;margin-left:1rem}.page__tag-line{margin-top:2rem}@-webkit-keyframes bounce{0%{transform:scaleY(1) translateY(-4rem)}90%{transform:scaleY(1) translateY(0)}100%{transform:scaleY(0.8)}}@keyframes bounce{0%{transform:scaleY(1) translateY(-4rem)}90%{transform:scaleY(1) translateY(0)}100%{transform:scaleY(0.8)}}@-webkit-keyframes scale{0%{transform:scale(0.5)}90%{transform:scale(1.05)}100%{transform:scale(1)}}@keyframes scale{0%{transform:scale(0.5)}90%{transform:scale(1.05)}100%{transform:scale(1)}}#background_overlay,#background_image{position:fixed;height:100%;width:100%}#background_overlay{background-color:red;z-index:-1;background-color:rgba(var(--foreground-color), var(--opacity, 0.6));transition:background-color .3s}#background_image{z-index:-2;-o-object-fit:cover;object-fit:cover;filter:blur(var(--blur, 1rem));transform:scale(calc(1 + var(--scale, 1.1) / 10))}#background_image[src=""],#background_image:not([src]){display:none}#main_page{display:grid;grid-template-columns:minmax(0, 1fr);grid-template-rows:1fr auto}.prime-action{display:flex;gap:.5rem;justify-content:flex-start}.initial{position:relative;justify-content:center;font-size:1.1rem;font-weight:700;width:2.4rem;height:2.4rem;aspect-ratio:1/1;color:rgba(var(--foreground-color), 1);box-shadow:0 .1rem .1rem rgba(0,0,0,.06);border-radius:2rem;text-transform:uppercase;-webkit-user-select:none;-moz-user-select:none;user-select:none;overflow:hidden;background-color:var(--contact-color, --accent-color);flex-shrink:0;transition:background-color .3s}.initial::after{content:"";position:absolute;background-color:rgba(255,255,255,.2);width:100%;height:200%;margin-left:50%;margin-top:50%;transform:rotate(45deg);transform-origin:left center}.group-icon{height:1.6rem;width:1.6rem;fill:rgba(var(--foreground-color), 1)}.contact{position:relative;display:grid;gap:0 1rem;align-items:center;flex-shrink:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;overflow:hidden}.contact>*:empty{display:none}.contact.highlight{-webkit-animation:highlight .5s ease-in-out 4 alternate;animation:highlight .5s ease-in-out 4 alternate}.contact:not(.chat){grid-template-columns:auto 1fr;grid-template-areas:"dp name" "dp ."}.contact.chat,.contact.group{grid-template-columns:auto 1fr auto;grid-template-areas:"dp name time" "dp . menu"}.contact.pipeline{grid-template-columns:auto 1fr auto;grid-template-areas:"dp tag time" "dp name name" "dp . menu"}.contact.pipeline .name{margin:.5rem 0 .3rem 0}.contact.admin{grid-template-columns:auto 1fr auto}.contact.selected{background-color:rgba(var(--text-color), 0.06)}.contact .initial{grid-area:dp}.contact .name,.contact .last-message{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;color:rgba(var(--text-color), 0.8)}.contact .tag{grid-area:tag;font-size:.8rem;padding:.2rem .4rem;background-color:rgba(var(--text-color), 0.6);color:rgba(var(--foreground-color), 1);border-radius:.3rem;margin-right:auto;font-weight:500;justify-self:flex-start}.contact.collapsed.chat,.contact.collapsed.group{grid-template-areas:"dp name menu";grid-template-rows:1fr}.contact.collapsed.pipeline{grid-template-areas:"dp tag tag" "dp name menu"}.contact.collapsed .name{margin-bottom:0}.contact .name{grid-area:name;margin-bottom:.3rem}.contact__flo-address{font-size:.8rem;color:rgba(var(--text-color), 0.6)}.contact .span-2{display:flex;justify-content:space-between;grid-column:2/4;width:100%;gap:.5rem}.contact .last-message{font-weight:400;font-size:.9em;opacity:.9}.contact .menu{grid-area:menu;flex-shrink:0;justify-self:flex-end;padding:.2rem;fill:rgba(var(--text-color), 1)}.contact .time{color:rgba(var(--text-color), 0.7);grid-area:time;font-size:.8rem}.name{width:100%;font-size:1em;font-weight:500}.selectable-contact,.group-member,.blocked-id,.contact-list__item{gap:1rem;padding:.5rem 0}.selectable-contact{margin:0 -0.3rem;padding:.5rem .3rem;border-radius:.5rem;-webkit-user-select:none;-moz-user-select:none;user-select:none}.selectable-contact:not(:last-of-type){margin-bottom:.5rem}.selectable-contact input{margin-left:auto;height:1.3em;width:1.3em}.selectable-contact button{margin-left:auto;min-width:-webkit-fit-content;min-width:-moz-fit-content;min-width:fit-content}.group-member{display:flex;align-items:center;font-size:.9rem}.group-member .admin-tag{white-space:nowrap;margin-left:auto;padding:.1rem .6rem;font-size:.8rem;border-radius:3rem;background-color:rgba(var(--text-color), 0.1)}.contact-list__item{display:flex;text-align:left;justify-content:flex-start;width:100%;padding:.5rem}.contact-list__item:focus{background-color:rgba(var(--text-color), 0.03)}#selected_contacts_container{display:flex;overflow:auto hidden}#selected_contacts_container:not(:empty){padding:.5rem 0;margin-bottom:.5rem;border-bottom:solid thin rgba(var(--text-color), 0.1)}#selected_contacts_container .contact-preview{display:flex;flex-shrink:0;align-items:center;cursor:pointer;margin-right:.3rem;background:rgba(var(--text-color), 0.1);padding-left:.2rem;border-radius:2rem;transform-origin:left;overflow:hidden}#selected_contacts_container .contact-preview .initial{width:1.6rem;height:1.6rem;font-size:.9rem}#selected_contacts_container .contact-preview .name{font-size:.9rem;color:rgba(var(--text-color), 0.8);margin-left:.5rem}#contact_details_popup .popup-section{margin:1.5rem 0}#contact_details_popup h5{font-weight:500;opacity:.8}#contact_details_popup .group-icon{padding:.2rem;height:3rem;width:3rem}#contact_initial{height:4.6rem;width:4.6rem;font-size:2.4rem;border-radius:4rem;margin-top:3rem;margin-bottom:.5rem;background-color:var(--contact-color, --accent-color)}#contact_name{margin:.5rem 0}#contact_name::part(text){font-size:1.2rem;font-weight:500}#search_contacts{position:-webkit-sticky;position:sticky;top:-1rem;z-index:1}#search_contacts::after{content:"";position:absolute;left:0;bottom:0;height:calc(100% + 1rem);width:100%;background:rgba(var(--foreground-color), 1);z-index:-1}.event-card{padding:.4rem .6rem;font-weight:500;font-size:.85rem;background-color:rgba(var(--text-color), 0.04);border-radius:.5rem;color:rgba(var(--text-color), 0.8);margin:1rem 0;justify-self:center;align-self:center;text-align:center}.event-card+.event-card{margin-top:0}.group-event-card{font-weight:400}#warn_no_encryption{background:#fffd8d;color:#111}.pipeline-event{padding:.8rem 1rem;border:solid thin rgba(var(--text-color), 0.2);text-align:start}.pipeline-event--signed .icon{fill:var(--green)}.pipeline-event .time{font-size:.8rem;color:rgba(var(--text-color), 0.7)}.contact,.mail-card{padding:.8rem;margin:0 .2rem;border-radius:.5rem}.contact::before,.mail-card::before{content:"";position:absolute;top:0;margin:.5rem;padding:.3rem;background:var(--accent-color);border-radius:100%;border:solid rgba(var(--foreground-color), 1) 1px;transform:scale(0);transition:transform .3s}.contact.unread::before,.mail-card.unread::before{transform:scale(1)}.mail-card.unread .time,.mail-card.unread .date,.contact.unread .time,.contact.unread .date{color:var(--accent-color)}.mail-card.unread h4,.contact.unread h4{color:rgba(var(--text-color), 1)}.mail-card.unread h4,.mail-card.unread h5,.mail-card.unread p,.contact.unread h4,.contact.unread h5,.contact.unread p{font-weight:700}.mail-card{position:relative;display:grid;gap:0 1rem;align-items:center;flex-shrink:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;grid-template-columns:auto 1fr auto;grid-template-areas:"dp sender date" "dp subject subject" "dp desc desc"}.mail-card .initial{grid-area:dp;align-self:flex-start;font-size:1rem}.mail-card .sender{width:100%;font-size:.8rem;font-weight:500;color:rgba(var(--text-color), 0.8);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.mail-card .subject{grid-area:subject;font-size:1em;margin-top:.3rem;font-weight:500}.mail-card .description{grid-area:desc;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:.9em;margin-top:.2rem;color:rgba(var(--text-color), 0.8)}.date{grid-area:date;margin-left:auto;white-space:nowrap;font-size:.8rem;color:rgba(var(--text-color), 0.8)}@-webkit-keyframes slide{from{opacity:0;transform:translateX(-1rem)}to{opacity:1;transform:none}}@keyframes slide{from{opacity:0;transform:translateX(-1rem)}to{opacity:1;transform:none}}#mail_container{width:100%}.mail{position:relative;padding:1.5rem}.mail:not(:first-of-type){margin-top:1.5rem;background-color:rgba(var(--text-color), 0.03);padding-bottom:2rem;margin-inline:1rem;border-radius:.5rem;overflow:hidden}.mail:not(:first-of-type)::before{content:"";position:absolute;left:0;top:0;width:.2rem;height:100%;background:rgba(var(--text-color), 0.2)}.mail .mail-header{align-self:start;margin-bottom:1.5rem;gap:1rem}.mail .initial{background-color:var(--contact-color, --accent-color);margin:0;height:2rem;width:2rem}.mail .sender-name{font-weight:500;margin-bottom:.5rem}.mail .flo-id{font-weight:400;max-width:90%}.mail .mail-subject,.mail .mail-content{overflow-wrap:break-word;word-wrap:break-word}.mail .mail-subject{margin-bottom:.4em}.mail .mail-content{height:-webkit-max-content;height:-moz-max-content;height:max-content;max-width:60ch;white-space:pre-wrap}.logo-section{display:grid;grid-template-columns:auto 1fr}#main_navbar{display:flex;background:rgba(var(--foreground-color), 1)}#main_navbar.hide-away{position:absolute}#main_navbar ul{display:flex;height:100%;width:100%}#main_navbar ul li{width:100%}.nav-item{position:relative;display:grid;width:100%;align-items:center;justify-items:center;padding:.5rem .4rem;color:var(--text-color);font-size:.8rem;border-radius:.5rem;font-weight:500;aspect-ratio:1/1}.nav-item .icon{grid-area:1/1/2/2;transition:transform .2s cubic-bezier(0.175, 0.885, 0.32, 1.275)}.nav-item .filled{opacity:0}.nav-item__title{line-height:1;transition:opacity .2s,transform .2s cubic-bezier(0.175, 0.885, 0.32, 1.275)}.nav-item--active{color:var(--accent-color)}.nav-item--active .icon{fill:var(--accent-color);transform:translateY(50%)}.nav-item--active .icon.filled{opacity:1}.nav-item--active .icon:not(.filled){opacity:0}.nav-item--active .nav-item__title{transform:translateY(100%);opacity:0}.nav-item__indicator{position:absolute;bottom:0;width:2rem;height:.3rem;background:var(--accent-color);border-radius:1rem 1rem 0 0;z-index:1}.nav-item:last-of-type{margin-top:auto}.badge{display:flex;align-items:center;justify-content:center;content:attr(data-notifications);position:absolute;top:0;right:0;min-width:2.7ch;font-size:.8rem;padding:.2rem;background:var(--danger-color);color:rgba(var(--background-color), 1);line-height:1;font-weight:700;border-radius:1rem;margin:.3rem}#contacts,#mails,#settings{height:100%;overflow-y:hidden;background-color:rgba(var(--foreground-color), 0.3)}#contacts .header,#mails .header,#settings .header{padding:1rem;position:relative;gap:.5rem}#contacts .header h4,#mails .header h4,#settings .header h4{text-transform:capitalize;font-weight:500}#contacts sm-menu,#mails sm-menu,#settings sm-menu{margin-right:-0.7rem}#auto_complete_contact{position:relative}#mail_contact_list{max-height:40vh;overflow-y:auto;position:absolute;top:100%;background:rgba(var(--foreground-color), 1);z-index:1;border-radius:.4rem;box-shadow:0 .1rem .1rem rgba(0,0,0,.1),0 .2rem .5rem rgba(0,0,0,.16);width:100%}#mail_contact_list .contact{grid-template-columns:auto 1fr;grid-template-areas:"dp ." "dp ."}#mail_contact_list sm-menu{display:none}#contacts{position:relative;overflow-x:hidden;grid-template-rows:-webkit-max-content 1fr;grid-template-rows:max-content 1fr;grid-template-columns:minmax(0, 1fr)}#contacts .header{display:flex;justify-content:space-between;padding:.5rem 1rem}#contacts .scrolling-wrapper{height:100%;flex:1;overflow-y:auto}#contacts .scrolling-wrapper .empty-state{padding:1.5rem;text-align:center}.user-profile-button{background-color:rgba(var(--text-color), 0.06);border-radius:2rem;font-size:.8rem;padding:.6rem .8rem;flex:1}sm-chip .badge{position:relative;margin:-0.2rem 0 -0.2rem .5rem;height:1.5rem;min-width:3.3ch;background-color:var(--accent-color);color:rgba(var(--background-color), 1)}#contacts_container .contact{padding:.5rem 0}#contacts_container .contact:not(:last-of-type){margin-bottom:.5rem}#chat_sections{display:grid;grid-template-columns:minmax(0, 1fr);overflow-y:auto;height:100%;overflow-x:hidden}#chat_sections>*{grid-area:1/1/2/2}#notifications_wrapper{width:100%;height:100%;padding:1rem}#notifications_wrapper .icon-only{margin-left:-0.5rem}#notifications_list{margin-top:.5rem}.notification{padding:1rem;background-color:rgba(var(--text-color), 0.06);border-radius:.5rem;margin:0 -0.8rem;gap:.8rem}.notification:not(:last-of-type){margin-bottom:.3rem}.notification__message{line-height:normal}.notification__time{font-size:.8rem;color:rgba(var(--text-color), 0.7)}#creation_process{overflow-x:hidden}#creation_process .group-icon{background-color:var(--accent-color);justify-self:center;height:8rem;width:8rem;margin-bottom:1rem;padding:2rem;border-radius:50%;font-size:4rem}#mails{position:relative;grid-template-columns:minmax(0, 1fr);grid-template-rows:-webkit-max-content 1fr;grid-template-rows:max-content 1fr}#mail_sections{overflow-y:auto}#mail_type_selector{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}#mail_type_selector sm-chip .badge{margin:0rem}#search_chats{--min-height: 2.5rem;width:100%}#chat_page,#mail_page,#settings{display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden}#group_members_list{margin-top:.5rem}#group_members_list .contact{padding:.5rem 0}#chat_view,#mail{background-color:rgba(var(--foreground-color), 0.3)}#chat_view{position:relative;display:flex;flex-direction:column;height:100%;overflow-y:auto}#transaction_details{position:-webkit-sticky;position:sticky;top:0;z-index:2;background-color:rgba(var(--foreground-color), 1);border:solid thin rgba(var(--text-color), 0.2);color:rgba(var(--text-color), 0.8);margin:1.5rem auto;-webkit-backdrop-filter:blur(1rem);backdrop-filter:blur(1rem);box-shadow:0 1rem 1.5rem rgba(0,0,0,.1);view-transition-name:transaction-details;padding:1rem;border-radius:.85rem}#transaction_details[open]{padding:1.5rem}::view-transition-new(transaction-details),::view-transition-old(transaction-details){width:100%;height:100%}.message{position:relative;display:flex;flex-wrap:wrap;width:auto;font-size:.92rem;max-width:-webkit-max-content;max-width:-moz-max-content;max-width:max-content;margin-bottom:.2rem;margin-top:.8rem;padding:.5em .8em;transition:opacity .3s,transform .3s cubic-bezier(0.175, 0.885, 0.32, 1.275)}.message .sender-name{font-size:.85rem;font-weight:500;margin-bottom:.3rem}.message .message-body{display:flex;align-items:center;flex-wrap:wrap;overflow-wrap:break-word;word-wrap:break-word;word-break:break-all;word-break:break-word;-webkit-hyphens:auto;hyphens:auto;white-space:pre-wrap;line-height:1.5}.message .message-body a{color:inherit;text-decoration:underline}.message .message-body .text-emoji{align-self:center;font-size:1.4em;letter-spacing:0}.message .time{white-space:nowrap;font-size:.8em;opacity:.8;justify-self:flex-end;padding-left:1rem;align-self:flex-end;margin-top:.2rem;margin-left:auto}.sent{margin-left:auto;background:var(--accent-color);border-radius:.5rem 0 .5rem .5rem}.sent>*{color:rgba(var(--background-color), 1)}.sent::after{content:"";position:absolute;left:100%;top:0;width:0;height:0;border-style:solid;border-width:.5em .3em 0 0;border-color:var(--accent-color) rgba(0,0,0,0) rgba(0,0,0,0) rgba(0,0,0,0)}.received{margin-right:auto;border-radius:0 .5rem .5rem .5rem;background-color:rgba(var(--text-color), 0.1)}.received::after{content:"";position:absolute;left:-0.5em;top:0;width:0;height:0;border-style:solid;border-width:0 .5em .5em 0;border-color:rgba(0,0,0,0) rgba(var(--text-color), 0.1) rgba(0,0,0,0) rgba(0,0,0,0)}.sent+.sent,.received+.received{margin-top:0}.sent+.sent::after,.received+.received::after{display:none}.sent+.sent,.received+.received{border-radius:.5rem}.distinct-sender{display:grid;border-radius:0 .5rem .5rem .5rem !important;margin-top:.8rem !important}.distinct-sender::after{display:flex !important;content:"";position:absolute;left:-0.5em;top:0;width:0;height:0;border-style:solid;border-width:0 .5em .5em 0;border-color:rgba(0,0,0,0) rgba(var(--text-color), 0.1) rgba(0,0,0,0) rgba(0,0,0,0)}.unconfirmed{opacity:.7;transform-origin:left;-webkit-animation:pop .3s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275);animation:pop .3s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275)}@-webkit-keyframes pop{0%{transform:translate(-0.5rem, 1rem)}100%{transform:translate(0, 0)}}@keyframes pop{0%{transform:translate(-0.5rem, 1rem)}100%{transform:translate(0, 0)}}.back-button{padding:.5rem;margin-left:-0.8rem}#chat_header{padding:.5rem 1rem;align-content:center;grid-template-columns:minmax(0, 1fr);z-index:1}#chat_header .flex{height:100%}#chat_header .initial{cursor:pointer;height:1.8rem;width:1.8rem;flex-shrink:0;font-size:1rem}#chat_header .group-icon{padding:.1rem}#chat_details_button{position:relative;background-color:rgba(var(--text-color), 0.06);border-radius:2rem;justify-self:center;padding:.3rem;view-transition-name:chat-button}::view-transition-old(chat-button),::view-transition-new(chat-button){height:100%;width:100%}#receiver_name{font-weight:500;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}#receiver_name:has(span){display:grid;place-content:center;place-items:center}#receiver_name>*{grid-area:1/1/2/2}#scroll_to_bottom{position:absolute;display:flex;right:0;bottom:3rem;border-radius:4rem;z-index:1;aspect-ratio:1/1;margin:1.5rem;transform:scale(0);transition:transform .2s}#scroll_to_bottom button{border-radius:4rem}#scroll_to_bottom.new-message::after{position:absolute;content:"";top:0;right:0;z-index:2;padding:.5rem;border-radius:50%;background:#00e676}.no-transformations{transform:none !important}#chat_footer .flex{align-items:flex-end;padding:.5rem 1rem .5rem .5rem}#emoji_toggle{align-self:center;padding:.6rem;margin-right:.5rem;width:2.6rem;height:2.6rem;border-radius:2rem;cursor:pointer}#emoji_toggle path{fill:rgba(var(--text-color), 0.5)}#emoji_toggle.active path{fill:var(--accent-color)}#send_message_button{align-self:center;padding:.5rem .8rem;margin-left:.5rem;opacity:.5;font-weight:500}#send_message_button:not(:disabled){opacity:1;color:var(--accent-color)}#type_message{margin:0;--background: rgba(var(--text-color), 0.1)}#messages_container{flex:1;padding:0 1rem}#emoji_picker{--background: rgba(var(--text-color), 0.06);--border-size: 0;--input-border-color: none;--input-padding: 0.4rem 1rem;--outline-color: var(--accent-color);--input-font-color: rgba(var(--text-color), 1);--input-placeholder-color: rgba(var(--text-color), 0.6);--indicator-color: var(--accent-color);--button-hover-background: rgba(var(--text-color), 0.2);-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%;max-height:40vh}.emoji{font-size:1.6rem;cursor:pointer;padding:.4rem;border-radius:.6rem;-webkit-user-select:none;-moz-user-select:none;user-select:none;text-align:center}.icon--medium,.icon--big{margin-bottom:1.5rem;justify-self:center}.icon--medium{height:4rem;width:4rem}.icon--big{height:8rem;width:8rem}#messages_container,#chats_list,#inbox_mail_container,#sent_mail_container,#mail{width:100%;flex-direction:column;height:100%;overflow-y:auto}#chats_list{gap:.2rem;padding-bottom:6rem}.mail-container{height:100%;flex-direction:column;overflow-y:auto}#inbox_mail_container,#sent_mail_container{gap:.2rem;padding-bottom:6rem}.has-bg-image .received::after{border-color:rgba(0,0,0,0) rgba(var(--foreground-color), 0.6) rgba(0,0,0,0) rgba(0,0,0,0)}.has-bg-image #emoji_picker{--background: rgba(var(--foreground-color), 0.6)}.has-bg-image #emoji_toggle path{fill:rgba(var(--text-color), 0.8)}.has-bg-image sm-textarea{--background: rgba(var(--foreground-color), 0.6)}#mail{align-items:flex-start;padding-bottom:1.5rem}#mail>.flex{padding:0 1.5rem;margin-top:2rem}#mail>.flex button:first-of-type{margin-right:.5rem}.sidebar-item{display:flex;align-items:center;padding:1rem .8rem;text-transform:capitalize;font-weight:500;opacity:.9;color:inherit;margin:0 .2rem;border-radius:.5rem}.sidebar-item:not(:last-of-type){margin-bottom:.2rem}.sidebar-item .icon{margin-right:1em;width:1em}#settings .card{display:flex;flex-direction:column;margin:0 .5rem;width:calc(min(36rem,100%) - 1rem)}#settings #settings_title{text-transform:capitalize}#settings #settings_sidebar{height:100%}#settings #settings_panel{overflow-y:auto;height:100%}#settings .panel{padding-bottom:6rem}#settings .panel>*{margin:0 auto}#settings #sign_out::part(button){color:var(--error-color)}#settings #bg_preview_container{padding:1rem 0;gap:.5rem;flex-wrap:wrap}#settings .bg-preview{position:relative;display:grid;place-items:center;height:8rem;width:8rem;cursor:pointer;border-radius:1rem;overflow:hidden;background:rgba(var(--text-color), 0.1)}#settings .bg-preview--selected::after{content:"";position:absolute;height:100%;width:100%;border-radius:inherit;box-shadow:0 0 0 .5rem var(--accent-color) inset}#settings .bg-preview input{display:none}#settings .bg-preview__image{-o-object-fit:cover;object-fit:cover;height:100%;width:100%}.option{width:100%;justify-content:flex-start;padding:.8rem 0;color:var(--accent-color)}.option .icon{fill:var(--accent-color);margin-right:.5rem}.option--danger{color:var(--danger-color)}.option--danger .icon{fill:var(--danger-color)}#contact_options .option{justify-content:center;background-color:rgba(var(--text-color), 0.04)}.multisig-option{position:relative;padding:1rem;font-size:.9rem;border-bottom:thin solid rgba(var(--text-color), 0.3)}.multisig-option .wrap-around{font-weight:500}.multisig-option__label{font-weight:500;font-size:1rem}.multisig-option__balance{color:rgba(var(--text-color), 0.8)}.multisig-option .icon-only{padding:.3rem}.multisig-option.highlight{-webkit-animation:highlight .5s ease-in-out infinite alternate;animation:highlight .5s ease-in-out infinite alternate}@-webkit-keyframes highlight{0%{background-color:rgba(var(--text-color), 0.1)}100%{background-color:rgba(0,0,0,0)}}@keyframes highlight{0%{background-color:rgba(var(--text-color), 0.1)}100%{background-color:rgba(0,0,0,0)}}.multisig-type-button{display:grid;grid-template-columns:auto 1fr auto;gap:1rem;justify-items:flex-start;background-color:rgba(var(--text-color), 0.04);padding:1.5rem 1.2rem;font-size:1rem;text-align:start}.remove-card-wrapper{min-height:2rem}.receiver-card{display:grid;gap:.5rem;padding:.5rem 0;border:none}.receiver-card:not(:last-of-type){border-bottom:solid thin rgba(var(--text-color), 0.3)}#selected_fee_tip,#error_section{font-weight:500}.error{color:var(--danger-color)}.error .icon{fill:var(--danger-color)}#send_fee_wrapper{display:grid}#send_fee_wrapper>*{grid-area:1/1}#send_fee_loader{background-color:rgba(var(--foreground-color), 1);z-index:1;width:100%}@media screen and (max-width: 640px){sm-popup{--border-radius: 1rem 1rem 0 0}#landing{grid-template-areas:"illustration" ".";align-items:flex-start}#landing .title-font{font-size:2rem;font-weight:500}#landing_illustration{grid-area:illustration}.inner-page{margin-bottom:5rem}#main_navbar{background-color:rgba(0,0,0,0);-webkit-backdrop-filter:blur(0.5rem);backdrop-filter:blur(0.5rem);background-color:rgba(var(--foreground-color), 0.9)}#main_navbar ul{background-color:rgba(var(--text-color), 0.05);border-radius:inherit;box-shadow:0 1rem 1.5rem rgba(0,0,0,.16)}#main_navbar.hide-away{bottom:0;left:0;right:0}#main_navbar .nav-item{height:3.8rem;width:4.5rem;margin:0 auto}#chats_list,#contact_container{gap:.2rem}#chat_view .message{width:auto;max-width:90%}#chat_header{grid-template-columns:auto minmax(0, 1fr)}#chat_header #chat_details_button{max-width:calc(100% - 2rem);margin-left:-1.7rem}#settings{overflow-x:hidden}#settings #settings_header{position:-webkit-sticky;position:sticky;top:0;z-index:1;padding:1rem 1.5rem;margin-bottom:.5rem;background:linear-gradient(rgba(var(--background-color), 0.8), rgba(var(--background-color), 0));-webkit-backdrop-filter:blur(0.5rem);backdrop-filter:blur(0.5rem);align-items:flex-start}.hide-on-mobile{display:none !important}}@media screen and (min-width: 40rem){.hide-on-desktop{display:none !important}.page{padding-bottom:0}.card{padding:1.5rem}.card button,.card .button{align-self:flex-start}.popup__header{grid-column:1/-1;padding:1rem 1.5rem 0 1.5rem}.logo-section{padding:2rem 3rem 0 3rem;margin:.5rem 0}sm-popup{--width: 25rem;--min-width: 25rem;--border-radius: 0.5rem}#multisig_tx_popup{--width: 28rem}#landing{align-items:center;gap:4vw;grid-template-columns:1fr 1fr;padding:0 4vw}#main_page{grid-template-columns:-webkit-min-content 1fr;grid-template-columns:min-content 1fr;grid-template-rows:1fr;grid-template-areas:"nav .";overflow:hidden;box-shadow:0 .1rem .2rem rgba(0,0,0,.05),0 1rem 3rem rgba(0,0,0,.2)}#main_navbar{grid-area:nav;border-top:none;flex-direction:column;border-right:solid thin rgba(var(--text-color), 0.1);background-color:rgba(var(--foreground-color), 0.3)}#main_navbar ul{flex-direction:column;gap:.5rem}#main_navbar ul li:last-of-type{margin-top:auto}.nav-item__indicator{width:.25rem;height:50%;left:0;border-radius:0 1rem 1rem 0;bottom:auto}#profile_popup{--width: 32rem}#add_contact_popup{--min-width: 24rem}#compose_mail_popup,#reply_mail_popup{--min-width: 36rem}#chat_details_button{background-color:rgba(0,0,0,0)}#pseudo_background{position:absolute;width:100%;height:100%;background-color:rgba(var(--text-color), 0.06);border-radius:inherit}#emoji_picker{max-height:18rem}#chat_view .message{width:auto;align-self:flex-start;max-width:55ch}#chat_page,#mail_page{display:grid;grid-template-columns:19rem 1fr}#chat_page>:first-child,#mail_page>:first-child{border-right:solid thin rgba(var(--text-color), 0.1)}#settings{display:grid;grid-template-columns:14rem 1fr}#settings>:first-child{border-right:solid thin rgba(var(--text-color), 0.1)}#settings .active{background:rgba(var(--text-color), 0.1)}#settings .panel{padding:1.5rem}.contact.active,.mail-card.active{background:rgba(var(--text-color), 0.06)}#contact_details_popup.is-group{--width: 52rem}#contact_details_popup.is-group #contact_details_section{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}#contact_details_popup.is-group #contact_details_section>:first-child{padding-right:1.5rem;border-right:thin solid rgba(var(--text-color), 0.3)}}@media only screen and (max-width: 1280px){.hide-on-medium{display:none !important}}@media only screen and (min-width: 1080px){#chat_view #messages_container{padding:1rem 4rem;padding-top:0}#chat_page,#mail_page{grid-template-columns:21rem 1fr}}@media only screen and (min-width: 1280px){#landing{gap:4vw;padding:0 8vw}#landing .title-font{font-size:3rem}#emoji_picker{--num-columns: 16}.contact.active,.mail-card.active{background:rgba(var(--text-color), 0.06)}}@media(hover: hover){::-webkit-scrollbar{width:.5rem;height:.5rem}::-webkit-scrollbar-thumb{background:rgba(var(--text-color), 0.3);border-radius:1rem}::-webkit-scrollbar-thumb:hover{background:rgba(var(--text-color), 0.5)}.interactive:hover{background-color:rgba(var(--text-color), 0.06)}.emoji:hover{cursor:pointer;background:rgba(var(--text-color), 0.06)}.contact .menu{opacity:0;transition:opacity .3s}.contact:hover .menu{opacity:1}}@media(hover: none){.contact-preview,.contact{-webkit-tap-highlight-color:rgba(0,0,0,0)}.contact .menu{display:none}}@supports(overflow: overlay){body{overflow:overlay}}.hidden{display:none !important} \ No newline at end of file diff --git a/messenger/css/main.scss b/messenger/css/main.scss new file mode 100644 index 0000000..aef8892 --- /dev/null +++ b/messenger/css/main.scss @@ -0,0 +1,2773 @@ +* { + padding: 0; + margin: 0; + box-sizing: border-box; + font-family: "Roboto", sans-serif; +} + +:root { + font-size: clamp(1rem, 1.2vmax, 1.2rem); +} + +html, +body { + height: 100%; + view-transition-name: none; +} + +body { + --accent-color: #3d5afe; + --secondary-color: #ffac2e; + --text-color: 20, 20, 20; + --foreground-color: 252, 253, 255; + --background-color: 241, 243, 248; + --danger-color: rgb(255, 75, 75); + --green: #1cad59; + --yellow: rgb(220, 165, 0); + // Accent colors + --dark-red: #d40e1e; + --red: #f50000; + --kinda-pink: #e40273; + --purple: #462191; + --shady-blue: #324de6; + --nice-blue: #3d5afe; + --maybe-cyan: #00b0ff; + --teal: #00bcd4; + --mint-green: #16c79a; + --yellowish-green: #66bb6a; + --greenish-yellow: #8bc34a; + --dark-teal: #11698e; + --tangerine: #ff6f00; + --orange: #ff9100; + --redish-orange: #ff3d00; + color: rgba(var(--text-color), 1); + background-color: rgba(var(--background-color), 1); + overflow-y: hidden; + + #scroll_to_bottom { + background: rgba(var(--foreground-color), 1); + box-shadow: 0 0.3rem 0.4rem rgba(0, 0, 0, 0.2); + } +} + +body[data-theme="dark"] { + --accent-color: #6d83ff; + --secondary-color: #d60739; + --text-color: 220, 220, 220; + --foreground-color: 27, 28, 29; + --background-color: 21, 22, 22; + --danger-color: rgb(255, 106, 106); + --green: #00e676; + --yellow: rgb(255, 213, 5); + // Accent colors + --dark-red: #ff5e7e; + --red: #ff6098; + --kinda-pink: #c44ae6; + --purple: #9565f7; + --shady-blue: #8295fb; + --nice-blue: #6d83ff; + --maybe-cyan: #66cfff; + --teal: #6aeeff; + --mint-green: #4dffd2; + --yellowish-green: #9effa2; + --greenish-yellow: #c7fc8b; + --dark-teal: #51cbff; + --tangerine: #ffac6d; + --orange: #ffbe68; + --redish-orange: #ff8560; + + .initial { + box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.16); + } + + .message { + color: rgba(var(--text-color), 1); + } + + #scroll_to_bottom { + background: linear-gradient( + rgba(var(--text-color), 0.1), + rgba(var(--text-color), 0.1) + ), + rgba(var(--foreground-color), 1); + box-shadow: 0 0.4rem 0.4rem rgba(0, 0, 0, 0.3); + } +} + +p, +strong { + font-size: 0.9rem; + max-width: 65ch; + line-height: 1.7; + color: rgba(var(--text-color), 0.9); +} + +.info { + line-height: normal; + padding: 1rem; + border-radius: 0.5rem; + font-weight: 500; + color: rgba(0, 0, 0, 0.7); + &--warning { + background-color: khaki; + } + &--error { + background-color: var(--danger-color); + } +} + +a { + text-decoration: none; + color: var(--accent-color); + + &:focus-visible { + box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 1) inset; + } +} + +button, +.button { + user-select: none; + position: relative; + display: inline-flex; + border: none; + background-color: transparent; + overflow: hidden; + color: inherit; + -webkit-tap-highlight-color: transparent; + align-items: center; + font-size: 0.9rem; + font-weight: 500; + white-space: nowrap; + padding: 0.8rem; + border-radius: 0.5rem; + justify-content: center; + &:focus-visible { + outline: var(--accent-color) solid medium; + } + + &:not(:disabled) { + cursor: pointer; + } +} + +.button { + background-color: rgba(var(--text-color), 0.02); + border: solid thin rgba(var(--text-color), 0.06); + &--primary { + padding: 0.6rem 1rem; + color: rgba(var(--background-color), 1); + background-color: var(--accent-color); + + .icon { + fill: rgba(var(--background-color), 1); + } + } + &--colored { + color: var(--accent-color); + .icon { + fill: var(--accent-color); + } + } + &--danger { + background-color: #ff737310; + color: var(--danger-color); + .icon { + fill: var(--danger-color); + } + } + + &--small { + padding: 0.4rem 0.6rem; + } + + &--outlined { + border: solid var(--accent-color) 1px; + background-color: transparent; + color: var(--accent-color); + .icon { + fill: var(--accent-color); + } + } + &--transparent { + background-color: transparent; + } +} +button:disabled { + opacity: 0.4; + cursor: not-allowed; + filter: saturate(0); +} + +.cta { + text-transform: uppercase; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.05em; + padding: 0.8rem 1rem; +} + +.icon { + width: 1.2rem; + height: 1.2rem; + fill: rgba(var(--text-color), 0.8); + flex-shrink: 0; +} + +.icon-only { + padding: 0.5rem; + border-radius: 0.3rem; + background-color: transparent; + aspect-ratio: 1/1; + flex-shrink: 0; +} + +a:any-link:focus-visible { + outline: rgba(var(--text-color), 1) 0.1rem solid; +} + +details { + &[open] { + & summary { + margin-bottom: 1rem; + } + + & > summary .down-arrow { + transform: rotate(180deg); + } + } + summary { + display: flex; + user-select: none; + cursor: pointer; + align-items: center; + justify-content: space-between; + } + .down-arrow { + margin-left: 0.5rem; + } +} + +fieldset { + border: none; +} + +input { + accent-color: var(--accent-color); + + &[type="range"] { + &:active { + cursor: grab; + } + } +} + +sm-copy { + font-size: 0.9rem; +} + +sm-input, +sm-textarea { + font-size: 0.9rem; + --border-radius: 0.5rem; + --background-color: rgba(var(--foreground-color), 1); + button { + .icon { + fill: var(--accent-color); + } + } +} + +sm-textarea { + --max-height: 32ch; +} + +sm-spinner { + --size: 1rem; + --stroke-width: 0.1rem; +} + +sm-form { + --gap: 1rem; +} + +sm-select { + --padding: 0.8rem; + font-size: 0.9rem; +} + +sm-option { + font-size: 0.9rem; +} + +sm-chips { + --gap: 0.3rem; +} + +sm-chip { + position: relative; + font-size: 0.9rem; + --border-radius: 0.5rem; + --padding: 0.5rem 0.6rem; + --background: rgba(var(--text-color), 0.06); + user-select: none; + &[selected] { + color: rgba(var(--background-color), 1); + --background: rgba(var(--text-color), 0.9); + } +} +sm-popup { + &::part(popup) { + max-height: calc(100vh - 2rem); + background-color: rgba(var(--foreground-color), 1); + } +} + +ul, +ol { + list-style: none; +} + +ol { + counter-reset: item; + + li { + position: relative; + display: flex; + align-items: flex-start; + counter-increment: item; + + &:not(:last-of-type) { + padding-bottom: 1.5rem; + + &::after { + content: ""; + position: absolute; + width: 0.1rem; + height: calc(100% - 2.2rem); + background: var(--accent-color); + margin-left: 0.7rem; + margin-top: 2rem; + } + } + } + + li::before { + content: counter(item); + display: flex; + align-items: center; + justify-content: center; + text-align: center; + font-size: 0.8rem; + font-weight: 500; + margin-top: 0.15rem; + margin-right: 1rem; + line-height: 1; + width: 1.5rem; + height: 1.5rem; + border-radius: 100%; + flex-shrink: 0; + color: rgba(var(--text-color), 0.8); + background: rgba(var(--text-color), 0.1); + } +} + +.overflow-ellipsis { + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.wrap-around { + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.full-bleed { + grid-column: 1/-1; +} + +.uppercase { + text-transform: uppercase; +} + +.capitalize { + text-transform: capitalize; +} + +.sticky { + position: sticky; +} + +.top-0 { + top: 0; +} + +.flex { + display: flex; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.flex-1 { + flex: 1; +} + +.grid { + display: grid; +} + +.flow-column { + grid-auto-flow: column; +} + +.gap-0-3 { + gap: 0.3rem; +} + +.gap-0-5 { + gap: 0.5rem; +} + +.gap-1 { + gap: 1rem; +} + +.gap-1-5 { + gap: 1.5rem; +} + +.gap-2 { + gap: 2rem; +} + +.gap-3 { + gap: 3rem; +} + +.text-align-right { + text-align: right; +} + +.align-start { + align-content: flex-start; +} + +.align-center { + align-items: center; +} + +.align-end { + align-items: flex-end; +} + +.text-center { + text-align: center; +} + +.justify-start { + justify-items: start; +} + +.justify-center { + justify-content: center; +} + +.justify-right { + margin-left: auto; +} + +.align-self-center { + align-self: center; +} + +.align-self-end { + align-self: end; +} + +.justify-self-center { + justify-self: center; +} + +.justify-self-start { + justify-self: start; +} + +.justify-self-end { + justify-self: end; +} + +.flex-direction-column { + flex-direction: column; +} + +.space-between { + justify-content: space-between; +} + +.w-100 { + width: 100%; +} + +.h-100 { + height: 100%; +} + +.margin-right-0-3 { + margin-right: 0.3rem; +} +.margin-right-0-5 { + margin-right: 0.5rem; +} + +.margin-left-0-5 { + margin-left: 0.5rem; +} + +.margin-left-auto { + margin-left: auto; +} +.margin-right-auto { + margin-right: auto; +} +.margin-top-1 { + margin-top: 1rem; +} +.margin-bottom-0-5 { + margin-bottom: 0.5rem; +} +.margin-bottom-1 { + margin-bottom: 1rem; +} + +.margin-block-1 { + margin-block: 1rem; +} + +.margin-block-1-5 { + margin-block: 1.5rem; +} + +.margin-inline-1 { + margin-inline: 1rem; +} + +.margin-inline-1-5 { + margin-inline: 1.5rem; +} + +.label { + font-size: 0.8rem; + color: rgba(var(--text-color), 0.8); + font-weight: 500; + margin-bottom: 0.2rem; +} + +.button--primary, +.button--danger { + .ripple { + background: radial-gradient( + circle, + rgba(var(--background-color), 0.3) 0%, + rgba(0, 0, 0, 0) 50% + ); + } +} + +.ripple { + height: 8rem; + width: 8rem; + position: absolute; + border-radius: 50%; + transform: scale(0); + background: radial-gradient( + circle, + rgba(var(--text-color), 0.3) 0%, + rgba(0, 0, 0, 0) 50% + ); + pointer-events: none; +} + +.interactive { + position: relative; + overflow: hidden; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.empty-state { + display: grid; + width: 100%; + padding: 1.5rem 0; +} + +.observe-empty-state:empty { + display: none !important; +} + +.observe-empty-state:not(:empty) + .empty-state { + display: none !important; +} + +.bullet-point { + display: flex; + align-items: center; + justify-content: center; + margin: 0 0.8ch; + + &::after { + content: ""; + height: 0.4ch; + width: 0.4ch; + border-radius: 0.5em; + background-color: var(--accent-color); + } +} + +.icon-button { + padding: 0.6rem; + border-radius: 0.8rem; + background-color: rgba(var(--text-color), 0.1); + height: max-content; + + .icon { + fill: var(--accent-color); + } +} + +.fab { + position: absolute; + right: 0; + bottom: 0; + margin: 1.5rem; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.2); + z-index: 2; + border-radius: 0.5rem; + padding: 0.8rem 1rem; + + .icon { + margin-right: 0.5rem; + } +} + +.multi-state-button { + display: grid; + text-align: center; + align-items: center; + justify-items: center; + & > * { + grid-area: 1/1/2/2; + } + button { + z-index: 1; + width: 100%; + } +} +.password-field { + label { + display: flex; + justify-content: center; + input:checked ~ .visible { + display: none; + } + input:not(:checked) ~ .invisible { + display: none; + } + } +} + +.page { + height: 100%; + + &__header { + display: flex; + justify-content: space-between; + margin-bottom: 1.5rem; + min-height: 8rem; + + .grid { + margin-top: auto; + } + + h1 { + margin-top: auto; + font-size: 2rem; + } + } +} +.card { + background-color: rgba(var(--text-color), 0.06); + border-radius: 0.5rem; + padding: 1rem; +} + +.page-layout { + display: grid; + gap: 1.5rem 0; + grid-template-columns: 1.5rem minmax(0, 1fr) 1.5rem; + align-content: flex-start; + + & > * { + grid-column: 2/3; + } +} + +#confirmation_popup, +#prompt_popup { + flex-direction: column; + h4 { + font-size: 1.2rem; + margin-bottom: 1rem; + } + + .flex { + margin-top: 1rem; + } + sm-input { + margin-top: 2rem; + } +} + +.popup__header { + position: relative; + display: grid; + gap: 0.5rem; + width: 100%; + padding: 0 1.5rem; + align-items: center; + & > * { + grid-row: 1; + } + h3, + h4 { + grid-column: 1/-1; + justify-self: center; + align-self: center; + } + &__close { + grid-column: 1; + margin-left: -1rem; + justify-self: flex-start; + } +} + +#secondary_pages { + display: flex; + flex-direction: column; + width: 100%; + + header { + padding: 1.5rem; + } +} + +.inner-page { + display: grid; + position: relative; + grid-template-columns: minmax(0, 1fr); + height: 100%; + width: 100%; +} + +#sign_in { + justify-items: center; + align-content: center; + padding: 1.5rem; + margin: 0; + section { + margin-top: -8rem; + width: min(26rem, 100%); + } + + sm-form { + margin: 2rem 0; + } +} + +#sign_up { + justify-items: center; + align-content: center; + keys-generator { + width: min(26rem, 100%); + margin-top: -4rem; + } + .h2 { + margin-bottom: 0.5rem; + } +} + +.tip { + font-size: 0.9rem; + color: rgba(var(--text-color), 0.8); +} + +.danger { + color: var(--error-color); +} + +.app-brand { + display: flex; + gap: 0.3rem; + align-items: center; + .icon { + height: 1.7rem; + width: 1.7rem; + } +} +.app-name { + &__company { + font-size: 0.8rem; + font-weight: 500; + color: rgba(var(--text-color), 0.8); + } +} + +.select-file { + input[type="file"] { + display: none; + } +} + +#landing { + display: grid; + border-radius: 0.6rem; + width: 100%; + height: 100%; + padding: 0 1.5rem; + align-items: center; + + .logo-section { + padding: 1.5rem; + display: flex; + } + + .title-font { + line-height: 1.2; + font-weight: 700; + font-size: 2.5rem; + } + + .left { + display: grid; + flex-direction: column; + padding-bottom: 1.5rem; + z-index: 1; + + h4 { + color: var(--accent-color); + margin-bottom: 0.5rem; + font-weight: 500; + } + + h3 { + margin-bottom: 1rem; + font-weight: 500; + } + + p { + font-size: 1.1rem; + color: rgba(var(--text-color), 0.8); + } + + .button { + padding: 0.8rem 1.5rem; + } + + .flex { + .button:first-of-type { + margin-right: 0.5rem; + } + } + } +} + +#landing_illustration { + position: relative; + width: 100%; +} + +@keyframes slide-down { + from { + transform: translateY(-1rem); + } + + to { + transform: none; + } +} + +#loading { + height: 100%; + display: grid; + place-content: center; + justify-items: center; +} + +.page__loader { + z-index: 1; + transform-origin: bottom; + height: 6rem; + width: 6rem; + animation: bounce 0.5s infinite alternate ease-in; +} + +.shadow { + margin-top: -1rem; + width: 5rem; + height: 2rem; + background: rgba(var(--text-color), 0.1); + border-radius: 50%; + animation: scale 0.5s infinite alternate ease-in; + margin-left: 1rem; +} + +.page__tag-line { + margin-top: 2rem; +} + +@keyframes bounce { + 0% { + transform: scaleY(1) translateY(-4rem); + } + + 90% { + transform: scaleY(1) translateY(0); + } + + 100% { + transform: scaleY(0.8); + } +} + +@keyframes scale { + 0% { + transform: scale(0.5); + } + + 90% { + transform: scale(1.05); + } + + 100% { + transform: scale(1); + } +} + +#background_overlay, +#background_image { + position: fixed; + height: 100%; + width: 100%; +} + +#background_overlay { + background-color: red; + z-index: -1; + background-color: rgba(var(--foreground-color), var(--opacity, 0.6)); + transition: background-color 0.3s; +} + +#background_image { + z-index: -2; + object-fit: cover; + filter: blur(var(--blur, 1rem)); + transform: scale(calc(1 + (var(--scale, 1.1) / 10))); + + &[src=""], + &:not([src]) { + display: none; + } +} + +#main_page { + display: grid; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: 1fr auto; +} + +.prime-action { + display: flex; + gap: 0.5rem; + justify-content: flex-start; +} + +.initial { + position: relative; + justify-content: center; + font-size: 1.1rem; + font-weight: 700; + width: 2.4rem; + height: 2.4rem; + aspect-ratio: 1/1; + color: rgba(var(--foreground-color), 1); + box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.06); + border-radius: 2rem; + text-transform: uppercase; + user-select: none; + overflow: hidden; + background-color: var(--contact-color, --accent-color); + flex-shrink: 0; + transition: background-color 0.3s; + + &::after { + content: ""; + position: absolute; + background-color: rgba(255 255 255/ 0.2); + width: 100%; + height: 200%; + margin-left: 50%; + margin-top: 50%; + transform: rotate(45deg); + transform-origin: left center; + } +} + +.group-icon { + height: 1.6rem; + width: 1.6rem; + fill: rgba(var(--foreground-color), 1); +} + +.contact { + position: relative; + display: grid; + gap: 0 1rem; + align-items: center; + flex-shrink: 0; + user-select: none; + overflow: hidden; + & > * { + &:empty { + display: none; + } + } + &.highlight { + animation: highlight 0.5s ease-in-out 4 alternate; + } + &:not(.chat) { + grid-template-columns: auto 1fr; + grid-template-areas: "dp name" "dp ."; + } + + &.chat, + &.group { + grid-template-columns: auto 1fr auto; + grid-template-areas: + "dp name time" + "dp . menu"; + } + &.pipeline { + grid-template-columns: auto 1fr auto; + grid-template-areas: + "dp tag time" + "dp name name" + "dp . menu"; + .name { + margin: 0.5rem 0 0.3rem 0; + } + } + + &.admin { + grid-template-columns: auto 1fr auto; + } + + &.selected { + background-color: rgba(var(--text-color), 0.06); + } + + .initial { + grid-area: dp; + } + + .name, + .last-message { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + color: rgba(var(--text-color), 0.8); + } + .tag { + grid-area: tag; + font-size: 0.8rem; + padding: 0.2rem 0.4rem; + background-color: rgba(var(--text-color), 0.6); + color: rgba(var(--foreground-color), 1); + border-radius: 0.3rem; + margin-right: auto; + font-weight: 500; + justify-self: flex-start; + } + &.collapsed { + &.chat, + &.group { + grid-template-areas: "dp name menu"; + grid-template-rows: 1fr; + } + &.pipeline { + grid-template-areas: + "dp tag tag" + "dp name menu"; + } + .name { + margin-bottom: 0; + } + } + .name { + grid-area: name; + margin-bottom: 0.3rem; + } + &__flo-address { + font-size: 0.8rem; + color: rgba(var(--text-color), 0.6); + } + + .span-2 { + display: flex; + justify-content: space-between; + grid-column: 2/4; + width: 100%; + gap: 0.5rem; + } + + .last-message { + font-weight: 400; + font-size: 0.9em; + opacity: 0.9; + } + + .menu { + grid-area: menu; + flex-shrink: 0; + justify-self: flex-end; + padding: 0.2rem; + fill: rgba(var(--text-color), 1); + } + + .time { + color: rgba(var(--text-color), 0.7); + grid-area: time; + font-size: 0.8rem; + } +} +.name { + width: 100%; + font-size: 1em; + font-weight: 500; +} + +.selectable-contact, +.group-member, +.blocked-id, +.contact-list__item { + gap: 1rem; + padding: 0.5rem 0; +} + +.selectable-contact { + margin: 0 -0.3rem; + padding: 0.5rem 0.3rem; + border-radius: 0.5rem; + user-select: none; + &:not(:last-of-type) { + margin-bottom: 0.5rem; + } + input { + margin-left: auto; + height: 1.3em; + width: 1.3em; + } + button { + margin-left: auto; + min-width: fit-content; + } +} + +.group-member { + display: flex; + align-items: center; + font-size: 0.9rem; + .admin-tag { + white-space: nowrap; + margin-left: auto; + padding: 0.1rem 0.6rem; + font-size: 0.8rem; + border-radius: 3rem; + background-color: rgba(var(--text-color), 0.1); + } +} + +.contact-list__item { + display: flex; + text-align: left; + justify-content: flex-start; + width: 100%; + padding: 0.5rem; + + &:focus { + background-color: rgba(var(--text-color), 0.03); + } +} + +#selected_contacts_container { + display: flex; + overflow: auto hidden; + + &:not(:empty) { + padding: 0.5rem 0; + margin-bottom: 0.5rem; + border-bottom: solid thin rgba(var(--text-color), 0.1); + } + + .contact-preview { + display: flex; + flex-shrink: 0; + align-items: center; + cursor: pointer; + margin-right: 0.3rem; + background: rgba(var(--text-color), 0.1); + padding-left: 0.2rem; + border-radius: 2rem; + transform-origin: left; + overflow: hidden; + + .initial { + width: 1.6rem; + height: 1.6rem; + font-size: 0.9rem; + } + + .name { + font-size: 0.9rem; + color: rgba(var(--text-color), 0.8); + margin-left: 0.5rem; + } + } +} + +#contact_details_popup { + .popup-section { + margin: 1.5rem 0; + } + + h5 { + font-weight: 500; + opacity: 0.8; + } + + .group-icon { + padding: 0.2rem; + height: 3rem; + width: 3rem; + } +} + +#contact_initial { + height: 4.6rem; + width: 4.6rem; + font-size: 2.4rem; + border-radius: 4rem; + margin-top: 3rem; + margin-bottom: 0.5rem; + background-color: var(--contact-color, --accent-color); +} + +#contact_name { + margin: 0.5rem 0; + + &::part(text) { + font-size: 1.2rem; + font-weight: 500; + } +} + +#new_message_popup { +} +#search_contacts { + position: sticky; + top: -1rem; + z-index: 1; + &::after { + content: ""; + position: absolute; + left: 0; + bottom: 0; + height: calc(100% + 1rem); + width: 100%; + background: rgba(var(--foreground-color), 1); + z-index: -1; + } +} + +.event-card { + padding: 0.4rem 0.6rem; + font-weight: 500; + font-size: 0.85rem; + background-color: rgba(var(--text-color), 0.04); + border-radius: 0.5rem; + color: rgba(var(--text-color), 0.8); + margin: 1rem 0; + justify-self: center; + align-self: center; + text-align: center; + + & + & { + margin-top: 0; + } +} + +.group-event-card { + font-weight: 400; +} + +#warn_no_encryption { + background: rgb(255, 253, 141); + color: #111; +} +.pipeline-event { + padding: 0.8rem 1rem; + border: solid thin rgba(var(--text-color), 0.2); + text-align: start; + &--signed { + .icon { + fill: var(--green); + } + } + .time { + font-size: 0.8rem; + color: rgba(var(--text-color), 0.7); + } +} + +.contact, +.mail-card { + padding: 0.8rem; + margin: 0 0.2rem; + border-radius: 0.5rem; +} + +.contact::before, +.mail-card::before { + content: ""; + position: absolute; + top: 0; + margin: 0.5rem; + padding: 0.3rem; + background: var(--accent-color); + border-radius: 100%; + border: solid rgba(var(--foreground-color), 1) 1px; + transform: scale(0); + transition: transform 0.3s; +} + +.contact.unread::before, +.mail-card.unread::before { + transform: scale(1); +} + +.mail-card.unread, +.contact.unread { + .time, + .date { + color: var(--accent-color); + } + + h4 { + color: rgba(var(--text-color), 1); + } + + h4, + h5, + p { + font-weight: 700; + } +} + +.mail-card { + position: relative; + display: grid; + gap: 0 1rem; + align-items: center; + flex-shrink: 0; + user-select: none; + grid-template-columns: auto 1fr auto; + grid-template-areas: + "dp sender date" + "dp subject subject" + "dp desc desc"; + + .initial { + grid-area: dp; + align-self: flex-start; + font-size: 1rem; + } + + .sender { + width: 100%; + font-size: 0.8rem; + font-weight: 500; + color: rgba(var(--text-color), 0.8); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .subject { + grid-area: subject; + font-size: 1em; + margin-top: 0.3rem; + font-weight: 500; + } + + .description { + grid-area: desc; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + font-size: 0.9em; + margin-top: 0.2rem; + color: rgba(var(--text-color), 0.8); + } +} + +.date { + grid-area: date; + margin-left: auto; + white-space: nowrap; + font-size: 0.8rem; + color: rgba(var(--text-color), 0.8); +} + +@keyframes slide { + from { + opacity: 0; + transform: translateX(-1rem); + } + + to { + opacity: 1; + transform: none; + } +} + +#mail_container { + width: 100%; +} + +.mail { + position: relative; + padding: 1.5rem; + + &:not(:first-of-type) { + margin-top: 1.5rem; + background-color: rgba(var(--text-color), 0.03); + padding-bottom: 2rem; + margin-inline: 1rem; + border-radius: 0.5rem; + overflow: hidden; + } + + &:not(:first-of-type)::before { + content: ""; + position: absolute; + left: 0; + top: 0; + width: 0.2rem; + height: 100%; + background: rgba(var(--text-color), 0.2); + } + + .mail-header { + align-self: start; + margin-bottom: 1.5rem; + gap: 1rem; + } + + .initial { + background-color: var(--contact-color, --accent-color); + margin: 0; + height: 2rem; + width: 2rem; + } + + .sender-name { + font-weight: 500; + margin-bottom: 0.5rem; + } + + .flo-id { + font-weight: 400; + max-width: 90%; + } + + .mail-subject, + .mail-content { + overflow-wrap: break-word; + word-wrap: break-word; + } + + .mail-subject { + margin-bottom: 0.4em; + } + + .mail-content { + height: max-content; + max-width: 60ch; + white-space: pre-wrap; + } +} + +.logo-section { + display: grid; + grid-template-columns: auto 1fr; +} + +#main_navbar { + display: flex; + background: rgba(var(--foreground-color), 1); + + &.hide-away { + position: absolute; + } + + ul { + display: flex; + height: 100%; + width: 100%; + + li { + width: 100%; + } + } +} + +.nav-item { + position: relative; + display: grid; + width: 100%; + align-items: center; + justify-items: center; + padding: 0.5rem 0.4rem; + color: var(--text-color); + font-size: 0.8rem; + border-radius: 0.5rem; + font-weight: 500; + aspect-ratio: 1/1; + + .icon { + grid-area: 1/1/2/2; + transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); + } + + .filled { + opacity: 0; + } + + &__title { + line-height: 1; + transition: opacity 0.2s, + transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); + } + + &--active { + color: var(--accent-color); + + .icon { + fill: var(--accent-color); + transform: translateY(50%); + + &.filled { + opacity: 1; + } + + &:not(.filled) { + opacity: 0; + } + } + + .nav-item__title { + transform: translateY(100%); + opacity: 0; + } + } + + &__indicator { + position: absolute; + bottom: 0; + width: 2rem; + height: 0.3rem; + background: var(--accent-color); + border-radius: 1rem 1rem 0 0; + z-index: 1; + } + + &:last-of-type { + margin-top: auto; + } +} + +.badge { + display: flex; + align-items: center; + justify-content: center; + content: attr(data-notifications); + position: absolute; + top: 0; + right: 0; + min-width: 2.7ch; + font-size: 0.8rem; + padding: 0.2rem; + background: var(--danger-color); + color: rgba(var(--background-color), 1); + line-height: 1; + font-weight: 700; + border-radius: 1rem; + margin: 0.3rem; +} + +#contacts, +#mails, +#settings { + height: 100%; + overflow-y: hidden; + background-color: rgba(var(--foreground-color), 0.3); + .header { + padding: 1rem; + position: relative; + gap: 0.5rem; + + h4 { + text-transform: capitalize; + font-weight: 500; + } + } + sm-menu { + margin-right: -0.7rem; + } +} + +#auto_complete_contact { + position: relative; +} + +#mail_contact_list { + max-height: 40vh; + overflow-y: auto; + position: absolute; + top: 100%; + background: rgba(var(--foreground-color), 1); + z-index: 1; + border-radius: 0.4rem; + box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), + 0 0.2rem 0.5rem rgba(0, 0, 0, 0.16); + width: 100%; + + .contact { + grid-template-columns: auto 1fr; + grid-template-areas: "dp ." "dp ."; + } + + sm-menu { + display: none; + } +} + +#contacts { + position: relative; + overflow-x: hidden; + grid-template-rows: max-content 1fr; + grid-template-columns: minmax(0, 1fr); + .header { + display: flex; + justify-content: space-between; + padding: 0.5rem 1rem; + } + + .scrolling-wrapper { + height: 100%; + flex: 1; + overflow-y: auto; + + .empty-state { + padding: 1.5rem; + text-align: center; + } + } +} +.user-profile-button { + background-color: rgba(var(--text-color), 0.06); + border-radius: 2rem; + font-size: 0.8rem; + padding: 0.6rem 0.8rem; + flex: 1; +} +sm-chip { + .badge { + position: relative; + margin: -0.2rem 0 -0.2rem 0.5rem; + height: 1.5rem; + min-width: 3.3ch; + background-color: var(--accent-color); + color: rgba(var(--background-color), 1); + } +} + +#contacts_container { + .contact { + padding: 0.5rem 0; + &:not(:last-of-type) { + margin-bottom: 0.5rem; + } + } +} +#chat_sections { + display: grid; + grid-template-columns: minmax(0, 1fr); + overflow-y: auto; + height: 100%; + overflow-x: hidden; + & > * { + grid-area: 1/1/2/2; + } +} + +#notifications_wrapper { + width: 100%; + height: 100%; + padding: 1rem; + .icon-only { + margin-left: -0.5rem; + } +} +#notifications_list { + margin-top: 0.5rem; +} +.notification { + padding: 1rem; + background-color: rgba(var(--text-color), 0.06); + border-radius: 0.5rem; + margin: 0 -0.8rem; + gap: 0.8rem; + &:not(:last-of-type) { + margin-bottom: 0.3rem; + } + &__message { + line-height: normal; + } + &__time { + font-size: 0.8rem; + color: rgba(var(--text-color), 0.7); + } +} + +#creation_process { + overflow-x: hidden; + .group-icon { + background-color: var(--accent-color); + justify-self: center; + height: 8rem; + width: 8rem; + margin-bottom: 1rem; + padding: 2rem; + border-radius: 50%; + font-size: 4rem; + } +} + +#mails { + position: relative; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: max-content 1fr; +} + +#mail_sections { + overflow-y: auto; +} + +#mail_type_selector { + width: fit-content; + + sm-chip { + .badge { + margin: 0rem; + } + } +} + +#search_chats { + --min-height: 2.5rem; + width: 100%; +} + +#chat_page, +#mail_page, +#settings { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + overflow: hidden; +} + +#group_members_list { + margin-top: 0.5rem; + + .contact { + padding: 0.5rem 0; + } +} + +#chat_view, +#mail { + background-color: rgba(var(--foreground-color), 0.3); +} + +#chat_view { + position: relative; + display: flex; + flex-direction: column; + height: 100%; + overflow-y: auto; +} +#transaction_details { + position: sticky; + top: 0; + z-index: 2; + background-color: rgba(var(--foreground-color), 1); + border: solid thin rgba(var(--text-color), 0.2); + color: rgba(var(--text-color), 0.8); + margin: 1.5rem auto; + backdrop-filter: blur(1rem); + box-shadow: 0 1rem 1.5rem rgba(0 0 0 / 0.1); + view-transition-name: transaction-details; + padding: 1rem; + border-radius: 0.85rem; + &[open] { + padding: 1.5rem; + } +} +::view-transition-new(transaction-details), +::view-transition-old(transaction-details) { + width: 100%; + height: 100%; +} + +.message { + position: relative; + display: flex; + flex-wrap: wrap; + width: auto; + font-size: 0.92rem; + max-width: max-content; + margin-bottom: 0.2rem; + margin-top: 0.8rem; + padding: 0.5em 0.8em; + transition: opacity 0.3s, + transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); + + .sender-name { + font-size: 0.85rem; + font-weight: 500; + margin-bottom: 0.3rem; + } + + .message-body { + display: flex; + align-items: center; + flex-wrap: wrap; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-all; + word-break: break-word; + -moz-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; + white-space: pre-wrap; + line-height: 1.5; + + a { + color: inherit; + text-decoration: underline; + } + + .text-emoji { + align-self: center; + font-size: 1.4em; + letter-spacing: 0; + } + } + + .time { + white-space: nowrap; + font-size: 0.8em; + opacity: 0.8; + justify-self: flex-end; + padding-left: 1rem; + align-self: flex-end; + margin-top: 0.2rem; + margin-left: auto; + } +} + +.sent { + margin-left: auto; + background: var(--accent-color); + border-radius: 0.5rem 0 0.5rem 0.5rem; + + & > * { + color: rgba(var(--background-color), 1); + } + + &::after { + content: ""; + position: absolute; + left: 100%; + top: 0; + width: 0; + height: 0; + border-style: solid; + border-width: 0.5em 0.3em 0 0; + border-color: var(--accent-color) transparent transparent transparent; + } +} + +.received { + margin-right: auto; + border-radius: 0 0.5rem 0.5rem 0.5rem; + background-color: rgba(var(--text-color), 0.1); + &::after { + content: ""; + position: absolute; + left: -0.5em; + top: 0; + width: 0; + height: 0; + border-style: solid; + border-width: 0 0.5em 0.5em 0; + border-color: transparent rgba(var(--text-color), 0.1) transparent + transparent; + } +} + +.sent + .sent, +.received + .received { + margin-top: 0; +} + +.sent + .sent::after, +.received + .received::after { + display: none; +} + +.sent + .sent, +.received + .received { + border-radius: 0.5rem; +} + +.distinct-sender { + display: grid; + border-radius: 0 0.5rem 0.5rem 0.5rem !important; + margin-top: 0.8rem !important; + + &::after { + display: flex !important; + content: ""; + position: absolute; + left: -0.5em; + top: 0; + width: 0; + height: 0; + border-style: solid; + border-width: 0 0.5em 0.5em 0; + border-color: transparent rgba(var(--text-color), 0.1) transparent + transparent; + } +} + +.unconfirmed { + opacity: 0.7; + transform-origin: left; + animation: pop 0.3s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +@keyframes pop { + 0% { + transform: translate(-0.5rem, 1rem); + } + + 100% { + transform: translate(0, 0); + } +} + +.back-button { + padding: 0.5rem; + margin-left: -0.8rem; +} + +#chat_header { + padding: 0.5rem 1rem; + align-content: center; + grid-template-columns: minmax(0, 1fr); + z-index: 1; + + .flex { + height: 100%; + } + + .initial { + cursor: pointer; + height: 1.8rem; + width: 1.8rem; + flex-shrink: 0; + font-size: 1rem; + } + + .group-icon { + padding: 0.1rem; + } +} + +#chat_details_button { + position: relative; + background-color: rgba(var(--text-color), 0.06); + border-radius: 2rem; + justify-self: center; + padding: 0.3rem; + view-transition-name: chat-button; +} +::view-transition-old(chat-button), +::view-transition-new(chat-button) { + height: 100%; + width: 100%; +} + +#receiver_name { + &:has(span) { + display: grid; + place-content: center; + place-items: center; + } + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + > * { + grid-area: 1/1/2/2; + } +} +#scroll_to_bottom { + position: absolute; + display: flex; + right: 0; + bottom: 3rem; + border-radius: 4rem; + z-index: 1; + aspect-ratio: 1/1; + margin: 1.5rem; + transform: scale(0); + transition: transform 0.2s; + + button { + border-radius: 4rem; + } + + &.new-message::after { + position: absolute; + content: ""; + top: 0; + right: 0; + z-index: 2; + padding: 0.5rem; + border-radius: 50%; + background: #00e676; + } +} + +.no-transformations { + transform: none !important; +} + +#chat_footer { + .flex { + align-items: flex-end; + padding: 0.5rem 1rem 0.5rem 0.5rem; + } +} + +#emoji_toggle { + align-self: center; + padding: 0.6rem; + margin-right: 0.5rem; + width: 2.6rem; + height: 2.6rem; + border-radius: 2rem; + cursor: pointer; + + path { + fill: rgba(var(--text-color), 0.5); + } + + &.active path { + fill: var(--accent-color); + } +} + +#send_message_button { + align-self: center; + padding: 0.5rem 0.8rem; + margin-left: 0.5rem; + opacity: 0.5; + font-weight: 500; + + &:not(:disabled) { + opacity: 1; + color: var(--accent-color); + } +} + +#type_message { + margin: 0; + --background: rgba(var(--text-color), 0.1); +} + +#messages_container { + flex: 1; + padding: 0 1rem; +} + +#emoji_picker { + --background: rgba(var(--text-color), 0.06); + --border-size: 0; + --input-border-color: none; + --input-padding: 0.4rem 1rem; + --outline-color: var(--accent-color); + --input-font-color: rgba(var(--text-color), 1); + --input-placeholder-color: rgba(var(--text-color), 0.6); + --indicator-color: var(--accent-color); + --button-hover-background: rgba(var(--text-color), 0.2); + user-select: none; + width: 100%; + max-height: 40vh; +} + +.emoji { + font-size: 1.6rem; + cursor: pointer; + padding: 0.4rem; + border-radius: 0.6rem; + user-select: none; + text-align: center; +} +.icon--medium, +.icon--big { + margin-bottom: 1.5rem; + justify-self: center; +} +.icon--medium { + height: 4rem; + width: 4rem; +} +.icon--big { + height: 8rem; + width: 8rem; +} + +#messages_container, +#chats_list, +#inbox_mail_container, +#sent_mail_container, +#mail { + width: 100%; + flex-direction: column; + height: 100%; + overflow-y: auto; +} + +#chats_list { + gap: 0.2rem; + padding-bottom: 6rem; +} + +.mail-container { + height: 100%; + flex-direction: column; + overflow-y: auto; +} + +#inbox_mail_container, +#sent_mail_container { + gap: 0.2rem; + padding-bottom: 6rem; +} + +.has-bg-image { + .received::after { + border-color: transparent rgba(var(--foreground-color), 0.6) transparent + transparent; + } + + #emoji_picker { + --background: rgba(var(--foreground-color), 0.6); + } + + #emoji_toggle { + path { + fill: rgba(var(--text-color), 0.8); + } + } + + sm-textarea { + --background: rgba(var(--foreground-color), 0.6); + } +} + +#mail { + align-items: flex-start; + padding-bottom: 1.5rem; + + & > .flex { + padding: 0 1.5rem; + margin-top: 2rem; + + button:first-of-type { + margin-right: 0.5rem; + } + } +} + +.sidebar-item { + display: flex; + align-items: center; + padding: 1rem 0.8rem; + text-transform: capitalize; + font-weight: 500; + opacity: 0.9; + color: inherit; + margin: 0 0.2rem; + border-radius: 0.5rem; + &:not(:last-of-type) { + margin-bottom: 0.2rem; + } + .icon { + margin-right: 1em; + width: 1em; + } +} + +#settings { + .card { + display: flex; + flex-direction: column; + margin: 0 0.5rem; + width: calc(min(36rem, 100%) - 1rem); + } + #settings_title { + text-transform: capitalize; + } + + #settings_sidebar { + height: 100%; + } + + #settings_panel { + overflow-y: auto; + height: 100%; + } + .panel { + padding-bottom: 6rem; + & > * { + margin: 0 auto; + } + } + #sign_out::part(button) { + color: var(--error-color); + } + #bg_preview_container { + padding: 1rem 0; + gap: 0.5rem; + flex-wrap: wrap; + } + + .bg-preview { + position: relative; + display: grid; + place-items: center; + height: 8rem; + width: 8rem; + cursor: pointer; + border-radius: 1rem; + overflow: hidden; + background: rgba(var(--text-color), 0.1); + + &--selected::after { + content: ""; + position: absolute; + height: 100%; + width: 100%; + border-radius: inherit; + box-shadow: 0 0 0 0.5rem var(--accent-color) inset; + } + + input { + display: none; + } + } + + .bg-preview__image { + object-fit: cover; + height: 100%; + width: 100%; + } +} + +.option { + width: 100%; + justify-content: flex-start; + padding: 0.8rem 0; + color: var(--accent-color); + .icon { + fill: var(--accent-color); + margin-right: 0.5rem; + } + &--danger { + color: var(--danger-color); + + .icon { + fill: var(--danger-color); + } + } +} +#contact_options { + .option { + justify-content: center; + background-color: rgba(var(--text-color), 0.04); + } +} + +.multisig-option { + position: relative; + padding: 1rem; + font-size: 0.9rem; + border-bottom: thin solid rgba(var(--text-color), 0.3); + .wrap-around { + font-weight: 500; + } + &__label { + font-weight: 500; + font-size: 1rem; + } + &__balance { + color: rgba(var(--text-color), 0.8); + } + .icon-only { + padding: 0.3rem; + } + &.highlight { + animation: highlight 0.5s ease-in-out infinite alternate; + } +} +@keyframes highlight { + 0% { + background-color: rgba(var(--text-color), 0.1); + } + 100% { + background-color: transparent; + } +} + +.multisig-type-button { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 1rem; + justify-items: flex-start; + background-color: rgba(var(--text-color), 0.04); + padding: 1.5rem 1.2rem; + font-size: 1rem; + text-align: start; +} + +.remove-card-wrapper { + min-height: 2rem; +} +.receiver-card { + display: grid; + gap: 0.5rem; + padding: 0.5rem 0; + border: none; + &:not(:last-of-type) { + border-bottom: solid thin rgba(var(--text-color), 0.3); + } +} +#selected_fee_tip, +#error_section { + font-weight: 500; +} +.error { + color: var(--danger-color); + .icon { + fill: var(--danger-color); + } +} +#send_fee_wrapper { + display: grid; + & > * { + grid-area: 1/1; + } +} +#send_fee_loader { + background-color: rgba(var(--foreground-color), 1); + z-index: 1; + width: 100%; +} + +@media screen and (max-width: 640px) { + sm-popup { + --border-radius: 1rem 1rem 0 0; + } + + #landing { + grid-template-areas: "illustration" "."; + align-items: flex-start; + + .title-font { + font-size: 2rem; + font-weight: 500; + } + } + + #landing_illustration { + grid-area: illustration; + } + + .inner-page { + margin-bottom: 5rem; + } + + #main_navbar { + background-color: transparent; + backdrop-filter: blur(0.5rem); + background-color: rgba(var(--foreground-color), 0.9); + ul { + background-color: rgba(var(--text-color), 0.05); + border-radius: inherit; + box-shadow: 0 1rem 1.5rem rgba(0 0 0 / 0.16); + } + + &.hide-away { + bottom: 0; + left: 0; + right: 0; + } + + .nav-item { + height: 3.8rem; + width: 4.5rem; + margin: 0 auto; + } + } + + #chats_list, + #contact_container { + gap: 0.2rem; + } + #chat_view { + .message { + width: auto; + max-width: 90%; + } + } + + #chat_header { + grid-template-columns: auto minmax(0, 1fr); + + #chat_details_button { + max-width: calc(100% - 2rem); + margin-left: -1.7rem; + } + } + + #settings { + overflow-x: hidden; + + #settings_header { + position: sticky; + top: 0; + z-index: 1; + padding: 1rem 1.5rem; + margin-bottom: 0.5rem; + background: linear-gradient( + rgba(var(--background-color), 0.8), + rgba(var(--background-color), 0) + ); + backdrop-filter: blur(0.5rem); + align-items: flex-start; + } + } + + .hide-on-mobile { + display: none !important; + } +} + +@media screen and (min-width: 40rem) { + .hide-on-desktop { + display: none !important; + } + + .page { + padding-bottom: 0; + } + .card { + padding: 1.5rem; + button, + .button { + align-self: flex-start; + } + } + + .popup__header { + grid-column: 1/-1; + padding: 1rem 1.5rem 0 1.5rem; + } + + .logo-section { + padding: 2rem 3rem 0 3rem; + margin: 0.5rem 0; + } + + sm-popup { + --width: 25rem; + --min-width: 25rem; + --border-radius: 0.5rem; + } + #multisig_tx_popup { + --width: 28rem; + } + + #landing { + align-items: center; + gap: 4vw; + grid-template-columns: 1fr 1fr; + padding: 0 4vw; + } + + #main_page { + grid-template-columns: min-content 1fr; + grid-template-rows: 1fr; + grid-template-areas: "nav ."; + overflow: hidden; + box-shadow: 0 0.1rem 0.2rem rgba(0, 0, 0, 0.05), + 0 1rem 3rem rgba(0, 0, 0, 0.2); + } + + #main_navbar { + grid-area: nav; + border-top: none; + flex-direction: column; + border-right: solid thin rgba(var(--text-color), 0.1); + background-color: rgba(var(--foreground-color), 0.3); + ul { + flex-direction: column; + gap: 0.5rem; + + li:last-of-type { + margin-top: auto; + } + } + } + + .nav-item { + &__indicator { + width: 0.25rem; + height: 50%; + left: 0; + border-radius: 0 1rem 1rem 0; + bottom: auto; + } + } + #profile_popup { + --width: 32rem; + } + + #add_contact_popup { + --min-width: 24rem; + } + + #compose_mail_popup, + #reply_mail_popup { + --min-width: 36rem; + } + #chat_details_button { + background-color: transparent; + } + #pseudo_background { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(var(--text-color), 0.06); + border-radius: inherit; + } + + #emoji_picker { + max-height: 18rem; + } + + #chat_view { + .message { + width: auto; + align-self: flex-start; + max-width: 55ch; + } + } + + #chat_page, + #mail_page { + display: grid; + grid-template-columns: 19rem 1fr; + + & > :first-child { + border-right: solid thin rgba(var(--text-color), 0.1); + } + } + + #settings { + display: grid; + grid-template-columns: 14rem 1fr; + + & > :first-child { + border-right: solid thin rgba(var(--text-color), 0.1); + } + .active { + background: rgba(var(--text-color), 0.1); + } + .panel { + padding: 1.5rem; + } + } + + .contact.active, + .mail-card.active { + background: rgba(var(--text-color), 0.06); + } + + #contact_details_popup { + &.is-group { + --width: 52rem; + + #contact_details_section { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; + + & > :first-child { + padding-right: 1.5rem; + border-right: thin solid rgba(var(--text-color), 0.3); + } + } + } + } +} + +@media only screen and (max-width: 1280px) { + .hide-on-medium { + display: none !important; + } +} + +@media only screen and (min-width: 1080px) { + #chat_view { + #messages_container { + padding: 1rem 4rem; + padding-top: 0; + } + } + + #chat_page, + #mail_page { + grid-template-columns: 21rem 1fr; + } +} + +@media only screen and (min-width: 1280px) { + #landing { + gap: 4vw; + padding: 0 8vw; + + .title-font { + font-size: 3rem; + } + } + + #emoji_picker { + --num-columns: 16; + } + + .contact.active, + .mail-card.active { + background: rgba(var(--text-color), 0.06); + } +} + +@media (hover: hover) { + ::-webkit-scrollbar { + width: 0.5rem; + height: 0.5rem; + } + + ::-webkit-scrollbar-thumb { + background: rgba(var(--text-color), 0.3); + border-radius: 1rem; + + &:hover { + background: rgba(var(--text-color), 0.5); + } + } + + .interactive:hover { + background-color: rgba(var(--text-color), 0.06); + } + + .emoji:hover { + cursor: pointer; + background: rgba(var(--text-color), 0.06); + } + + .contact { + .menu { + opacity: 0; + transition: opacity 0.3s; + } + } + + .contact:hover { + .menu { + opacity: 1; + } + } +} + +@media (hover: none) { + .contact-preview, + .contact { + -webkit-tap-highlight-color: transparent; + } + + .contact .menu { + display: none; + } +} + +@supports (overflow: overlay) { + body { + overflow: overlay; + } +} + +.hidden { + display: none !important; +} diff --git a/messenger/docs/additional-resources.md b/messenger/docs/additional-resources.md new file mode 100644 index 0000000..838e004 --- /dev/null +++ b/messenger/docs/additional-resources.md @@ -0,0 +1,6 @@ +## Additional Resources + +### Please refer the following for explanation of non messenger files +- RanchiMall Standard Operations +- RanchiMall Standard UI +- RanchiMall Single User BTC Wallet \ No newline at end of file diff --git a/messenger/docs/changelog.md b/messenger/docs/changelog.md new file mode 100644 index 0000000..73fda88 --- /dev/null +++ b/messenger/docs/changelog.md @@ -0,0 +1,2 @@ +### Important Changes +- Added Bitcoin Transaction fee increase update capability for multisig if the transaction is not getting confirmed. \ No newline at end of file diff --git a/messenger/docs/compactIDB.md b/messenger/docs/compactIDB.md new file mode 100644 index 0000000..2cb38d7 --- /dev/null +++ b/messenger/docs/compactIDB.md @@ -0,0 +1,400 @@ +## upgradeDB Function + +The `upgradeDB` function is used to upgrade an IndexedDB database to a new version. It allows creating and deleting object stores and indexes during the upgrade process. + +### Function Parameters + +- **dbName**: The name of the IndexedDB database to be upgraded. +- **createList**: An object specifying the object stores to create and their options. Each key represents the object store name, and the corresponding value is an object with optional `options` for the object store and `indexes` for creating indexes. +- **deleteList**: An array containing the names of object stores to be deleted during the upgrade. + +### Function Logic + +1. **Getting Current Version:** Retrieves the current version of the IndexedDB database using the `getDBversion` function. + +2. **Opening a New Database:** Opens a new version of the IndexedDB database (version + 1). + +3. **Upgrade Logic:** Handles the upgrade process inside the `onupgradeneeded` event. It creates new object stores and indexes based on the provided `createList` and deletes specified object stores from `deleteList`. + +4. **Success and Error Handling:** Resolves with a success message when the upgrade is completed successfully. Rejects with an error message if there's an error in opening the IndexedDB. + +### Return Value + +- Returns a Promise that resolves with a success message when the database upgrade is completed successfully. + +### Example Usage + +- In this example, the upgradeDB function is used to upgrade the 'exampleDB' IndexedDB. It creates new object stores ('users' and 'posts') with specified options and indexes, and it deletes the 'oldStore' object store. + +```javascript +const dbName = 'exampleDB'; +const createList = { + 'users': { + options: { keyPath: 'id', autoIncrement: true }, + indexes: { 'username': 'username' } + }, + 'posts': { + options: { keyPath: 'postId', autoIncrement: true }, + indexes: { 'author': 'authorId' } + } +}; +const deleteList = ['oldStore']; + +upgradeDB(dbName, createList, deleteList) + .then(result => console.log(result)) + .catch(error => console.error(error)); +// Output: Database upgraded +``` + +## compactIDB.initDB Function + +The `compactIDB.initDB` function is used to initialize an IndexedDB database with specified object stores. It checks the existing object stores in the database and creates new ones and deletes unwanted ones based on the provided `objectStores` parameter. + +### Function Parameters + +- **dbName**: The name of the IndexedDB database to be initialized. +- **objectStores**: An object specifying the object stores to be created. Each key represents the object store name, and the corresponding value is an object with optional `options` for the object store. + +### Function Logic + +1. **Opening Existing Database:** Opens the existing IndexedDB database specified by `dbName`. + +2. **Object Store Handling:** Compares the existing object stores with the specified `objectStores`. It creates new object stores that are not present in the database and deletes object stores that are not specified in `objectStores`. + +3. **Upgrade Process:** Uses the `upgradeDB` function to handle the upgrade process, creating new object stores and deleting unwanted object stores. + +4. **Success and Error Handling:** Resolves with a success message when the database initialization is completed successfully. Rejects with an error message if there's an error in opening the IndexedDB. + +### Return Value + +- Returns a Promise that resolves with a success message when the database initialization is completed successfully. + +### Example Usage + +- In this example, the compactIDB.initDB function is used to initialize the 'exampleDB' IndexedDB. It creates new object stores ('users' and 'posts') with specified options and indexes. + +```javascript +const dbName = 'exampleDB'; +const objectStores = { + 'users': { + options: { keyPath: 'id', autoIncrement: true }, + indexes: { 'username': 'username' } + }, + 'posts': { + options: { keyPath: 'postId', autoIncrement: true }, + indexes: { 'author': 'authorId' } + } +}; + +compactIDB.initDB(dbName, objectStores) + .then(result => console.log(result)) + .catch(error => console.error(error)); +// Output: Initiated IndexedDB +``` + +## compactIDB.openDB Function + +The `compactIDB.openDB` function is used to open an IndexedDB database for performing database operations. + +### Function Parameters + +- **dbName** (Optional): The name of the IndexedDB database to open. If not provided, the default database specified during initialization is used. + +### Function Logic + +1. **Opening Database:** Attempts to open the specified IndexedDB database (`dbName`). + +2. **Error Handling:** If there's an error in opening the database, the function rejects the promise with an error message indicating the failure. + +3. **Upgradeneeded Event:** If an "upgradeneeded" event is triggered during the database opening process, the function closes the existing database, deletes the database using `deleteDB` function, and rejects the promise with a message indicating that the database was not found. + +4. **Success Event:** If the database is successfully opened, the function resolves with the opened database object for performing further database operations. + +### Return Value + +- Returns a Promise that resolves with the opened IndexedDB database object or rejects with an error message if there's an issue opening the database. + +### Example Usage + +```javascript +const dbName = 'myDatabase'; + +compactIDB.openDB(dbName) + .then(database => { + // Perform database operations using the 'database' object. + console.log(`Successfully opened database: ${dbName}`); + }) + .catch(error => { + console.error(`Error opening database: ${error}`); + }); +// Output: Successfully opened database: myDatabase + +// Opens the 'myDatabase' IndexedDB database and performs operations inside the 'then' block. +``` + +## compactIDB.deleteDB Function + +The `compactIDB.deleteDB` function is used to delete an existing IndexedDB database. + +### Function Parameters + +- **dbName** (Optional): The name of the IndexedDB database to be deleted. If not provided, the default database specified during initialization is used. + +### Function Logic + +1. **Deleting Database:** Initiates a request to delete the specified IndexedDB database (`dbName`). + +2. **Error Handling:** If there's an error during the deletion process, the function rejects the promise with an error message indicating the failure. + +3. **Success Event:** If the database is successfully deleted, the function resolves with a success message indicating that the database was deleted successfully. + +### Return Value + +- Returns a Promise that resolves with a success message if the database is deleted successfully or rejects with an error message if there's an issue deleting the database. + +### Example Usage + +```javascript +const dbName = 'myDatabase'; + +compactIDB.deleteDB(dbName) + .then(message => { + console.log(message); // Output: Database deleted successfully + }) + .catch(error => { + console.error(`Error deleting database: ${error}`); + }); +// Output: Database deleted successfully + +// Deletes the 'myDatabase' IndexedDB database and logs a success message if the deletion is successful. +``` + + +## compactIDB.writeData Function + +The `compactIDB.writeData` function is used to write data to a specified object store in the IndexedDB. It allows adding or updating data records in the specified object store. + +### Function Parameters + +- **obsName**: The name of the object store where the data will be written. +- **data**: The data to be written to the object store. +- **key** (Optional): The key to identify the data record. If provided, the function updates the existing record with the specified key. If not provided, a new record is added to the object store. +- **dbName** (Optional): The name of the IndexedDB database where the object store is located. If not provided, the default database specified during initialization is used. + +### Function Logic + +1. **Opening Database:** Opens the specified IndexedDB database (`dbName`). + +2. **Transaction and Object Store:** Starts a read-write transaction on the specified object store (`obsName`). + +3. **Writing Data:** Writes the provided `data` to the object store. If a `key` is provided, it updates the existing record; otherwise, it adds a new record. + +4. **Success and Error Handling:** Resolves with a success message when the data writing is successful. Rejects with an error message if there's an error during the write operation. + +### Return Value + +- Returns a Promise that resolves with a success message when the data writing is completed successfully. + +### Example Usage + +```javascript +const obsName = 'users'; +const data = { id: 1, name: 'John Doe', email: 'john@example.com' }; + +compactIDB.writeData(obsName, data, 1) + .then(result => console.log(result)) + .catch(error => console.error(error)); +// Output: Write data Successful + +// Updating an existing record with key 1 in the 'users' object store. +``` + +## compactIDB.addData Function + +The `compactIDB.addData` function is used to add new data records to a specified object store in the IndexedDB. It allows adding data records with unique keys, ensuring no duplicate records with the same key are added. + +### Function Parameters + +- **obsName**: The name of the object store where the data will be added. +- **data**: The data to be added to the object store. +- **key** (Optional): The key to identify the new data record. If provided and a record with the same key already exists, the function fails, ensuring the uniqueness of the key. If not provided, the function generates a unique key for the new record. +- **dbName** (Optional): The name of the IndexedDB database where the object store is located. If not provided, the default database specified during initialization is used. + +### Function Logic + +1. **Opening Database:** Opens the specified IndexedDB database (`dbName`). + +2. **Transaction and Object Store:** Starts a read-write transaction on the specified object store (`obsName`). + +3. **Adding Data:** Adds the provided `data` to the object store. If a `key` is provided and a record with the same key already exists, the add operation fails. If no `key` is provided, the function generates a unique key for the new record. + +4. **Success and Error Handling:** Resolves with a success message when the data addition is successful. Rejects with an error message if there's an error during the add operation, such as attempting to add a duplicate record with an existing key. + +### Return Value + +- Returns a Promise that resolves with a success message when the data addition is completed successfully. + +### Example Usage + +```javascript +const obsName = 'users'; +const data = { id: 2, name: 'Alice Smith', email: 'alice@example.com' }; + +compactIDB.addData(obsName, data, 2) + .then(result => console.log(result)) + .catch(error => console.error(error)); +// Output: Add data successful + +// Adding a new record with key 2 to the 'users' object store. +``` + +## compactIDB.removeData Function + +The `compactIDB.removeData` function is used to remove a data record from a specified object store in the IndexedDB based on its key. + +### Function Parameters + +- **obsName**: The name of the object store from which the data record will be removed. +- **key**: The key of the data record that needs to be removed from the object store. +- **dbName** (Optional): The name of the IndexedDB database where the object store is located. If not provided, the default database specified during initialization is used. + +### Function Logic + +1. **Opening Database:** Opens the specified IndexedDB database (`dbName`). + +2. **Transaction and Object Store:** Starts a read-write transaction on the specified object store (`obsName`). + +3. **Removing Data:** Removes the data record with the provided `key` from the object store. + +4. **Success and Error Handling:** Resolves with a success message, including the removed key, when the removal is successful. Rejects with an error message if there's an error during the delete operation, such as attempting to remove a non-existent record. + +### Return Value + +- Returns a Promise that resolves with a success message, including the removed key, when the removal is completed successfully. + +### Example Usage + +- In this example, the compactIDB.removeData function is used to remove the data record with key 2 from the 'users' object store in the IndexedDB. + +```javascript +const obsName = 'users'; +const key = 2; + +compactIDB.removeData(obsName, key) + .then(result => console.log(result)) + .catch(error => console.error(error)); +// Output: Removed Data 2 + +// Removes the data record with key 2 from the 'users' object store. +``` + +## compactIDB.clearData Function + +The `compactIDB.clearData` function is used to clear all data records from a specified object store in the IndexedDB. + +### Function Parameters + +- **obsName**: The name of the object store from which all data records will be cleared. +- **dbName** (Optional): The name of the IndexedDB database where the object store is located. If not provided, the default database specified during initialization is used. + +### Function Logic + +1. **Opening Database:** Opens the specified IndexedDB database (`dbName`). + +2. **Transaction and Object Store:** Starts a read-write transaction on the specified object store (`obsName`). + +3. **Clearing Data:** Removes all data records from the object store, effectively clearing it. + +4. **Success and Error Handling:** Resolves with a success message when the clear operation is successful. Rejects with an error message if there's an error during the clear operation. + +### Return Value + +- Returns a Promise that resolves with a success message when the clear operation is completed successfully. + +### Example Usage + +```javascript +const obsName = 'users'; + +compactIDB.clearData(obsName) + .then(result => console.log(result)) + .catch(error => console.error(error)); +// Output: Clear data Successful + +// Clears all data records from the 'users' object store. +``` + +## compactIDB.readData Function + +The `compactIDB.readData` function is used to read a specific data record from an object store in the IndexedDB. + +### Function Parameters + +- **obsName**: The name of the object store from which the data record will be read. +- **key**: The key of the specific data record to be retrieved from the object store. +- **dbName** (Optional): The name of the IndexedDB database where the object store is located. If not provided, the default database specified during initialization is used. + +### Function Logic + +1. **Opening Database:** Opens the specified IndexedDB database (`dbName`). + +2. **Transaction and Object Store:** Starts a read-only transaction on the specified object store (`obsName`). + +3. **Reading Data:** Retrieves the data record corresponding to the provided key from the object store. + +4. **Success and Error Handling:** Resolves with the retrieved data record when the read operation is successful. Rejects with an error message if there's an error during the read operation. + +### Return Value + +- Returns a Promise that resolves with the retrieved data record when the read operation is completed successfully. + +### Example Usage + +```javascript +const obsName = 'users'; +const userID = 123; + +compactIDB.readData(obsName, userID) + .then(data => console.log(data)) + .catch(error => console.error(error)); +// Output: { id: 123, name: 'John Doe', ... } + +// Retrieves the data record with the key 123 from the 'users' object store. +``` + +## compactIDB.readAllData Function + +The `compactIDB.readAllData` function is used to retrieve all data records from an object store in the IndexedDB. + +### Function Parameters + +- **obsName**: The name of the object store from which all data records will be retrieved. +- **dbName** (Optional): The name of the IndexedDB database where the object store is located. If not provided, the default database specified during initialization is used. + +### Function Logic + +1. **Opening Database:** Opens the specified IndexedDB database (`dbName`). + +2. **Transaction and Object Store:** Starts a read-only transaction on the specified object store (`obsName`). + +3. **Reading All Data:** Iterates over the object store using a cursor and collects all data records into a temporary result object. + +4. **Success and Error Handling:** Resolves with the temporary result object containing all data records when the read operation is successful. Rejects with an error message if there's an error during the read operation. + +### Return Value + +- Returns a Promise that resolves with an object containing all retrieved data records when the read operation is completed successfully. + +### Example Usage + +```javascript +const obsName = 'users'; + +compactIDB.readAllData(obsName) + .then(data => console.log(data)) + .catch(error => console.error(error)); +// Output: { 123: { id: 123, name: 'John Doe', ... }, 124: { id: 124, name: 'Jane Smith', ... }, ... } + +// Retrieves all data records from the 'users' object store and logs the result. +``` + diff --git a/messenger/docs/features.md b/messenger/docs/features.md new file mode 100644 index 0000000..6706c0a --- /dev/null +++ b/messenger/docs/features.md @@ -0,0 +1,7 @@ +## Features +- Ability to send messages to Bitcoin addresses +- Ability to co-ordinate Bitcoin Multisig Creation +- Ability to create messaging group of Bitcoin IDs +- Ability to add and remove Bitcoin ID as contact +- Ability to send direct messages, and mails +- Attachments are not supported yet diff --git a/messenger/docs/floBlockchainAPI.md b/messenger/docs/floBlockchainAPI.md new file mode 100644 index 0000000..2c19577 --- /dev/null +++ b/messenger/docs/floBlockchainAPI.md @@ -0,0 +1,1204 @@ +## `fetch_retry(apicall, rm_node)` + +This function makes a fetch API call to the specified endpoint (`apicall`) and retries the request if it fails. It removes a specific server node (`rm_node`) from the list of servers before making the request. It returns a promise that resolves with the response data if the request is successful and rejects with an error if the request fails. + +### Parameters: + +- **`apicall`** (string): The API endpoint to which the fetch request is made. +- **`rm_node`** (string): The server node to be removed from the list before making the request. + +### Return Value: + +A Promise that resolves with the response data if the request is successful, and rejects with an error if the request fails. + +### Example Usage: + +```javascript +const apiEndpoint = "example/api/endpoint"; +const nodeToRemove = "exampleServerNode"; + +fetch_retry(apiEndpoint, nodeToRemove) + .then(responseData => { + console.log("API Response Data:", responseData); + // Handle the response data here + }) + .catch(error => { + console.error("API Request Error:", error); + // Handle API request errors here and implement retry logic if needed + }); +``` + +## `fetch_api(apicall, ic = true)` + +This function makes a fetch API call to the specified endpoint (`apicall`) using the available server nodes. If the request fails, it automatically retries the request using a different server node from the list. It returns a promise that resolves with the response data if the request is successful and rejects with an error if the request fails. + +### Parameters: + +- **`apicall`** (string): The API endpoint to which the fetch request is made. +- **`ic`** (boolean, optional, default: `true`): If `true`, the function initializes the server list from the full server list before making the request. If set to `false`, the server list is not initialized, and the function uses the existing server list. + +### Return Value: + +A Promise that resolves with the response data if the request is successful, and rejects with an error if the request fails. + +### Example Usage: + +```javascript +const apiEndpoint = "example/api/endpoint"; + +fetch_api(apiEndpoint) + .then(responseData => { + console.log("API Response Data:", responseData); + // Handle the response data here + }) + .catch(error => { + console.error("API Request Error:", error); + // Handle API request errors here and implement retry logic if needed + }); +``` + +## `promisedAPI(apicall, query_params = undefined)` + +This function serves as a wrapper around the `fetch_api` function, providing a convenient way to make API calls to the FLO blockchain. It returns a promise that resolves with the response data if the request is successful and rejects with an error if the request fails. + +### Parameters: + +- **`apicall`** (string): The API endpoint to which the fetch request is made. +- **`query_params`** (object, optional, default: `undefined`): An object containing query parameters to be appended to the API endpoint. If provided, these parameters are included in the API request. + +### Return Value: + +A Promise that resolves with the response data if the request is successful, and rejects with an error if the request fails. + +### Example Usage: + +```javascript +const apiEndpoint = "example/api/endpoint"; +const queryParams = { + param1: "value1", + param2: "value2" +}; + +promisedAPI(apiEndpoint, queryParams) + .then(responseData => { + console.log("API Response Data:", responseData); + // Handle the response data here + }) + .catch(error => { + console.error("API Request Error:", error); + // Handle API request errors here and implement retry logic if needed + }); +``` + +## `getBalance(addr)` + +This function retrieves the balance of a specified FLO blockchain address. + +### Parameters: + +- **`addr`** (string): The FLO blockchain address for which the balance needs to be retrieved. + +### Return Value: + +A Promise that resolves with the balance of the specified address if the request is successful, and rejects with an error if the request fails. + +### Example Usage: + +```javascript +const floAddress = "FLO1234567890abcdef"; // Replace with the target FLO blockchain address + +getBalance(floAddress) + .then(balance => { + console.log("Address Balance:", balance); + // Handle the balance data here + }) + .catch(error => { + console.error("Balance Retrieval Error:", error); + // Handle errors in retrieving address balance here + }); +``` + +## `getScriptPubKey(address)` + +This function generates the ScriptPubKey for a specified FLO blockchain address. + +### Parameters: + +- **`address`** (string): The FLO blockchain address for which the ScriptPubKey needs to be generated. + +### Return Value: + +A hexadecimal representation of the ScriptPubKey associated with the specified address. + +### Example Usage: + +```javascript +const floAddress = "FLO1234567890abcdef"; // Replace with the target FLO blockchain address + +const scriptPubKey = getScriptPubKey(floAddress); +console.log("ScriptPubKey:", scriptPubKey); +// Use the generated ScriptPubKey in your application +``` + +## `getUTXOs(address)` + +This function retrieves the Unspent Transaction Outputs (UTXOs) associated with a specified FLO blockchain address. + +### Parameters: + +- **`address`** (string): The FLO blockchain address for which UTXOs need to be retrieved. + +### Return Value: + +A Promise that resolves to an array of UTXO objects. Each UTXO object contains the following properties: +- **`txid`** (string): Transaction ID of the UTXO. +- **`vout`** (number): Output index of the UTXO transaction. +- **`amount`** (number): Amount of FLO tokens in the UTXO. +- **`confirmations`** (number): Number of confirmations for the UTXO transaction. +- **`scriptPubKey`** (string): Hexadecimal representation of the ScriptPubKey associated with the address. + +### Example Usage: + +- In this example, the getUTXOs function is used to retrieve the UTXOs associated with the specified FLO blockchain address. The function returns an array of UTXO objects, each containing relevant details about the unspent transaction output. These UTXOs can be used in transactions or for other purposes within your application. + + +```javascript +const floAddress = "FLO1234567890abcdef"; // Replace with the target FLO blockchain address + +getUTXOs(floAddress) + .then(utxos => { + console.log("UTXOs:", utxos); + // Process the retrieved UTXOs in your application + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + +## `createTx(senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true)` + +This function constructs a raw transaction for sending FLO tokens from one address to another on the FLO blockchain. + +### Parameters: + +- **`senderAddr`** (string): The sender's FLO blockchain address. +- **`receiverAddr`** (string): The recipient's FLO blockchain address. +- **`sendAmt`** (number): The amount of FLO tokens to be sent in the transaction. +- **`floData`** (string, optional): Additional data to be embedded in the transaction. Must contain printable ASCII characters only. +- **`strict_utxo`** (boolean, optional): If set to `true`, only confirmed UTXOs will be used in the transaction. Defaults to `true`. + +### Return Value: + +A Promise that resolves to the raw transaction object, or rejects with an error message if there are issues with the input parameters or insufficient balance. + +### Example Usage: + +- In this example, the createTx function is used to construct a raw transaction for sending a specified amount of FLO tokens from the sender's address to the recipient's address. Additional data (if provided) is embedded in the transaction. The function performs validations and constructs the transaction using appropriate UTXOs. + +```javascript +const senderAddress = "FLOSenderAddress"; // Replace with the sender's FLO address +const receiverAddress = "FLOReceiverAddress"; // Replace with the recipient's FLO address +const amountToSend = 10; // Amount of FLO tokens to send +const additionalData = "Hello, FLO!"; // Optional additional data to be included in the transaction + +createTx(senderAddress, receiverAddress, amountToSend, additionalData) + .then(rawTransaction => { + console.log("Raw Transaction:", rawTransaction); + // Broadcast the raw transaction to the FLO network using appropriate methods + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + + +## `floBlockchainAPI.sendTx(senderAddr, receiverAddr, sendAmt, privKey, floData = '', strict_utxo = true)` + +This function constructs, signs, and broadcasts a FLO blockchain transaction from the sender's address to the recipient's address with a specified amount of FLO tokens. It also allows embedding optional additional data in the transaction. + +### Parameters: + +- **`senderAddr`** (string): The sender's FLO blockchain address. +- **`receiverAddr`** (string): The recipient's FLO blockchain address. +- **`sendAmt`** (number): The amount of FLO tokens to be sent in the transaction. +- **`privKey`** (string): The sender's private key for signing the transaction. +- **`floData`** (string, optional): Additional data to be embedded in the transaction. Must contain printable ASCII characters only. +- **`strict_utxo`** (boolean, optional): If set to `true`, only confirmed UTXOs will be used in the transaction. Defaults to `true`. + +### Return Value: + +A Promise that resolves to the transaction ID (txid) once the transaction is successfully broadcasted, or rejects with an error message if there are issues with the input parameters or the transaction broadcast fails. + +### Example Usage: + +- In this example, the floBlockchainAPI.sendTx function is used to create, sign, and broadcast a FLO transaction from the specified sender address to the recipient address. Additional data (if provided) is embedded in the transaction. The function performs validations, constructs the transaction, signs it with the provided private key, and then broadcasts it to the FLO network. + +```javascript +const senderAddress = "FLOSenderAddress"; // Replace with the sender's FLO address +const receiverAddress = "FLOReceiverAddress"; // Replace with the recipient's FLO address +const amountToSend = 10; // Amount of FLO tokens to send +const privatekey = "SenderPrivateKey"; // Sender's private key for signing the transaction +const additionalData = "Hello, FLO!"; // Optional additional data to be included in the transaction + +floBlockchainAPI.sendTx(senderAddress, receiverAddress, amountToSend, privatekey, additionalData) + .then(txid => { + console.log("Transaction ID:", txid); + // Handle the successful transaction broadcast, if needed + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + +## `floBlockchainAPI.writeData(senderAddr, data, privKey, receiverAddr = DEFAULT.receiverID, options = {})` + +This function writes data into the FLO blockchain by creating a transaction with the specified data and embedding it into the blockchain. It allows customization of transaction parameters and additional options. + +### Parameters: + +- **`senderAddr`** (string): The sender's FLO blockchain address. +- **`data`** (string or object): The data to be written into the blockchain. It can be a string or an object, which will be automatically converted to a JSON string. +- **`privKey`** (string): The sender's private key for signing the transaction. +- **`receiverAddr`** (string, optional): The recipient's FLO blockchain address. Defaults to `DEFAULT.receiverID`. +- **`options`** (object, optional): Additional options for the transaction. + - **`strict_utxo`** (boolean, optional): If set to `false`, unconfirmed UTXOs will be considered for the transaction. Defaults to `true`. + - **`sendAmt`** (number, optional): The amount of FLO tokens to be sent in the transaction. Defaults to `DEFAULT.sendAmt`. + +### Return Value: + +A Promise that resolves to the transaction ID (txid) once the transaction is successfully broadcasted, or rejects with an error message if there are issues with the input parameters or the transaction broadcast fails. + +### Example Usage: + +- In this example, the floBlockchainAPI.writeData function is used to create a transaction containing the specified data and writes it into the FLO blockchain. The sender's address, data, and private key are provided as parameters. Additional options such as recipient address, UTXO strictness, and send amount can be customized if needed. + +```javascript +const senderAddress = "FLOSenderAddress"; // Replace with the sender's FLO address +const dataToSend = "Hello, FLO Blockchain!"; // Data to be written into the blockchain +const privatekey = "SenderPrivateKey"; // Sender's private key for signing the transaction + +floBlockchainAPI.writeData(senderAddress, dataToSend, privatekey) + .then(txid => { + console.log("Transaction ID:", txid); + // Handle the successful transaction broadcast, if needed + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + +## `floBlockchainAPI.mergeUTXOs(floID, privKey, floData = '')` + +This function merges all UTXOs of a given FLO address into a single UTXO and creates a transaction with the specified data, if provided, embedding it into the blockchain. It consolidates multiple UTXOs into one for efficient transaction management. + +### Parameters: + +- **`floID`** (string): The FLO blockchain address whose UTXOs need to be merged. +- **`privKey`** (string): The private key corresponding to the `floID` address for signing the transaction. +- **`floData`** (string, optional): The data to be embedded into the blockchain. Only printable ASCII characters are allowed. Defaults to an empty string. + +### Return Value: + +A Promise that resolves to the transaction ID (txid) once the merge transaction is successfully broadcasted, or rejects with an error message if there are issues with the input parameters or the transaction broadcast fails. + +### Example Usage: + +- In this example, the floBlockchainAPI.mergeUTXOs function is used to merge all UTXOs of the specified FLO address into a single UTXO. The FLO address, corresponding private key, and optional data to be embedded into the blockchain are provided as parameters. The function consolidates the UTXOs and creates a transaction, embedding the data if provided, and broadcasts it into the FLO blockchain. + +```javascript +const floID = "FLOAddressToMergeUTXOs"; // Replace with the FLO address to merge UTXOs +const privatekey = "PrivateKey"; // Private key corresponding to the FLO address + +floBlockchainAPI.mergeUTXOs(floID, privatekey, "Merged UTXOs data") + .then(txid => { + console.log("Merge Transaction ID:", txid); + // Handle the successful merge transaction, if needed + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + +## `floBlockchainAPI.splitUTXOs(floID, privKey, count, floData = '')` + +This function splits sufficient UTXOs of a given FLO address into multiple UTXOs for parallel sending. It creates transactions to split the UTXOs and embeds the specified data into the blockchain if provided. + +### Parameters: + +- **`floID`** (string): The FLO blockchain address whose UTXOs need to be split. +- **`privKey`** (string): The private key corresponding to the `floID` address for signing the split transactions. +- **`count`** (number): The number of UTXOs to split into. +- **`floData`** (string, optional): The data to be embedded into the blockchain. Only printable ASCII characters are allowed. Defaults to an empty string. + +### Return Value: + +A Promise that resolves to the transaction ID (txid) once the split transactions are successfully broadcasted, or rejects with an error message if there are issues with the input parameters or the transaction broadcast fails. + +### Example Usage: + +- In this example, the floBlockchainAPI.splitUTXOs function is used to split the UTXOs of the specified FLO address into the specified number of UTXOs. The FLO address, corresponding private key, the number of UTXOs to split into, and optional data to be embedded into the blockchain are provided as parameters. The function splits the UTXOs, creates transactions, embeds the data if provided, and broadcasts them into the FLO blockchain. + +```javascript +const floID = "FLOAddressToSplitUTXOs"; // Replace with the FLO address to split UTXOs +const privatekey = "PrivateKey"; // Private key corresponding to the FLO address +const splitCount = 5; // Number of UTXOs to split into + +floBlockchainAPI.splitUTXOs(floID, privatekey, splitCount, "Split UTXOs data") + .then(txid => { + console.log("Split Transaction ID:", txid); + // Handle the successful split transactions, if needed + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + +## `floBlockchainAPI.writeDataMultiple(senderPrivKeys, data, receivers = [DEFAULT.receiverID], options = {})` + +This function allows multiple senders to split and send FLO data to multiple receivers. It takes an array of sender private keys, a single piece of data (string or JSON object), an array of receiver addresses, and additional options if needed. + +### Parameters: + +- **`senderPrivKeys`** (array of strings): An array of private keys corresponding to the sender addresses. +- **`data`** (string or object): The data to be sent and embedded into the blockchain. It can be a string or a JSON object. +- **`receivers`** (array of strings, optional): An array of FLO addresses representing the receivers. Defaults to `[DEFAULT.receiverID]`. +- **`options`** (object, optional): Additional options for the transaction. It can include: + - **`sendAmt`** (number): The amount of FLO to be sent from each sender to each receiver. Defaults to `DEFAULT.sendAmt`. + - **`preserveRatio`** (boolean): If `true`, the specified `sendAmt` will be divided equally among receivers. If `false`, each sender will send the specified `sendAmt` to each receiver. Defaults to `true`. + +### Return Value: + +A Promise that resolves to the transaction ID (txid) once the transactions are successfully broadcasted, or rejects with an error message if there are issues with the input parameters or the transaction broadcast fails. + +### Example Usage: + +- In this example, the floBlockchainAPI.writeDataMultiple function is used to send the specified data from multiple senders to multiple receivers. It allows customization of the send amount and division of the amount among receivers based on the preserveRatio option. The function broadcasts the transactions into the FLO blockchain. + +```javascript +const senderPrivKeys = ["PrivateKey1", "PrivateKey2", "PrivateKey3"]; // Array of sender private keys +const receivers = ["ReceiverAddress1", "ReceiverAddress2"]; // Array of receiver addresses +const data = { message: "Hello, World!" }; // Data to be sent (can be a string or object) +const options = { + sendAmt: 0.1, // Amount of FLO to be sent from each sender to each receiver + preserveRatio: false // Divide the specified sendAmt equally among receivers +}; + +floBlockchainAPI.writeDataMultiple(senderPrivKeys, data, receivers, options) + .then(txid => { + console.log("Transaction ID:", txid); + // Handle the successful broadcast of transactions, if needed + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + +## `floBlockchainAPI.sendTxMultiple(senderPrivKeys, receivers, floData = '')` + +This function allows multiple senders to send FLO tokens to multiple receivers. It takes an array of sender private keys (or an object with private keys and corresponding send amounts), an object containing receiver addresses and corresponding receive amounts, and an optional FLO data string to embed in the transactions. + +### Parameters: + +- **`senderPrivKeys`** (array or object): An array of sender private keys or an object where keys are sender addresses and values are send amounts. +- **`receivers`** (object): An object where keys are receiver addresses and values are receive amounts. +- **`floData`** (string, optional): The FLO data to be embedded in the transactions. Only printable ASCII characters are allowed. + +### Return Value: + +A Promise that resolves to the transaction ID (txid) once the transactions are successfully broadcasted, or rejects with an error message if there are issues with the input parameters or the transaction broadcast fails. + +### Example Usage: + +#### Example 1: Using an Array of Sender Private Keys + +```javascript +const senderPrivKeys = ["PrivateKey1", "PrivateKey2", "PrivateKey3"]; // Array of sender private keys +const receivers = { + "ReceiverAddress1": 0.1, + "ReceiverAddress2": 0.2 +}; // Object with receiver addresses and corresponding receive amounts +const floData = "Hello, FLO!"; // Optional FLO data + +floBlockchainAPI.sendTxMultiple(senderPrivKeys, receivers, floData) + .then(txid => { + console.log("Transaction ID:", txid); + // Handle the successful broadcast of transactions, if needed + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + +#### Example 2: Using an Object of Sender Private Keys and Send Amounts + +```javascript +const senderPrivKeys = { + "PrivateKey1": 0.1, + "PrivateKey2": 0.2, + "PrivateKey3": 0.3 +}; // Object with sender addresses as keys and corresponding send amounts as values +const receivers = { + "ReceiverAddress1": 0.1, + "ReceiverAddress2": 0.2 +}; // Object with receiver addresses and corresponding receive amounts +const floData = "Hello, FLO!"; // Optional FLO data + +floBlockchainAPI.sendTxMultiple(senderPrivKeys, receivers, floData) + .then(txid => { + console.log("Transaction ID:", txid); + // Handle the successful broadcast of transactions, if needed + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + +## `createMultisigTx(redeemScript, receivers, amounts, floData = '', strict_utxo = true)` + +This function creates a multisig transaction, allowing multiple receivers to receive specified amounts of FLO tokens. It takes a redeem script, an array of receiver addresses, an array of corresponding receive amounts, optional FLO data, and a strict UTXO flag as input. + +### Parameters: + +- **`redeemScript`** (string): The redeem script associated with the multisig address. +- **`receivers`** (array): An array of receiver addresses where FLO tokens will be sent. +- **`amounts`** (array): An array of corresponding receive amounts for each receiver address. +- **`floData`** (string, optional): The FLO data to be embedded in the transactions. Only printable ASCII characters are allowed. +- **`strict_utxo`** (boolean, optional): A flag indicating whether to use only confirmed UTXOs for the transaction (default is `true`). + +### Return Value: + +A Promise that resolves to the constructed transaction object once the transaction data is successfully created, or rejects with an error message if there are issues with the input parameters or the UTXO validation fails. + +### Example Usage: + +- In this example, the createMultisigTx function is used to construct a multisig transaction with specified receiver addresses, corresponding amounts, optional FLO data, and a redeem script. The resulting transaction object can be further processed or signed before broadcasting. + +```javascript +const redeemScript = "redeemScriptHere"; // Replace with the actual redeem script +const receivers = ["ReceiverAddress1", "ReceiverAddress2"]; +const amounts = [0.1, 0.2]; // Amounts corresponding to the receivers +const floData = "Hello, FLO!"; // Optional FLO data + +createMultisigTx(redeemScript, receivers, amounts, floData) + .then(transaction => { + console.log("Constructed Transaction Object:", transaction); + // Handle the constructed transaction object, if needed + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + +## `sendMultisigTx(redeemScript, privateKeys, receivers, amounts, floData = '', strict_utxo = true)` + +This function is part of the `floBlockchainAPI` and facilitates the creation and sending of multisig transactions on the FLO blockchain. It takes a redeem script, an array of private keys corresponding to the multisig address, an array of receiver addresses, an array of corresponding receive amounts, optional FLO data, and a strict UTXO flag as input. + +### Parameters: + +- **`redeemScript`** (string): The redeem script associated with the multisig address. +- **`privateKeys`** (array): An array of private keys corresponding to the multisig address. The number of private keys must meet the requirements specified in the redeem script. +- **`receivers`** (array): An array of receiver addresses where FLO tokens will be sent. +- **`amounts`** (array): An array of corresponding receive amounts for each receiver address. +- **`floData`** (string, optional): The FLO data to be embedded in the transactions. Only printable ASCII characters are allowed. +- **`strict_utxo`** (boolean, optional): A flag indicating whether to use only confirmed UTXOs for the transaction (default is `true`). + +### Return Value: + +A Promise that resolves to the transaction ID (txid) once the multisig transaction is successfully broadcasted, or rejects with an error message if there are issues with the input parameters, private keys, or the UTXO validation fails. + +### Example Usage: + +- In this example, the sendMultisigTx function is used to create and send a multisig transaction with specified private keys, receiver addresses, corresponding amounts, optional FLO data, and a redeem script. The resulting transaction ID (txid) can be used for further reference or verification. + +```javascript +const redeemScript = "redeemScriptHere"; // Replace with the actual redeem script +const privateKeys = ["PrivateKey1", "PrivateKey2", "PrivateKey3"]; // Private keys corresponding to the multisig address +const receivers = ["ReceiverAddress1", "ReceiverAddress2"]; +const amounts = [0.1, 0.2]; // Amounts corresponding to the receivers +const floData = "Hello, FLO!"; // Optional FLO data + +floBlockchainAPI.sendMultisigTx(redeemScript, privateKeys, receivers, amounts, floData) + .then(txid => { + console.log("Multisig Transaction Sent! TXID:", txid); + // Handle the transaction ID (txid), if needed + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + +## `writeMultisigData(redeemScript, data, privatekeys, receiverAddr = DEFAULT.receiverID, options = {})` + +This function is part of the `floBlockchainAPI` and allows you to write data to the FLO blockchain using a multisig transaction. It takes a redeem script, data to be written, an array of private keys corresponding to the multisig address, an optional receiver address (defaulting to `DEFAULT.receiverID`), and additional options such as strict UTXO and send amount. + +### Parameters: + +- **`redeemScript`** (string): The redeem script associated with the multisig address. +- **`data`** (string): The data to be written to the blockchain. +- **`privatekeys`** (array): An array of private keys corresponding to the multisig address. +- **`receiverAddr`** (string, optional): The receiver address where FLO tokens will be sent. Defaults to `DEFAULT.receiverID`. +- **`options`** (object, optional): Additional options for the transaction, such as `strict_utxo` (boolean) and `sendAmt` (number). + +### Return Value: + +A Promise that resolves to the transaction ID (txid) once the multisig transaction with the specified data is successfully broadcasted, or rejects with an error message if there are issues with the input parameters or the transaction cannot be created or broadcasted. + +### Example Usage: + +- In this example, the sendMultisigTx function is used to create and send a multisig transaction with specified private keys, receiver addresses, corresponding amounts, optional FLO data, and a redeem script. The resulting transaction ID (txid) can be used for further reference or verification. + +```javascript +const redeemScript = "redeemScriptHere"; // Replace with the actual redeem script +const data = "Your data to be written to the blockchain"; +const privatekeys = ["PrivateKey1", "PrivateKey2", "PrivateKey3"]; // Private keys corresponding to the multisig address +const receiverAddr = "ReceiverAddress"; // Optional receiver address (default is DEFAULT.receiverID) +const options = { + strict_utxo: true, // Optional: Use only confirmed UTXOs (default is true) + sendAmt: 0.1 // Optional: Amount of FLO tokens to be sent (default is DEFAULT.sendAmt) +}; + +floBlockchainAPI.writeMultisigData(redeemScript, data, privatekeys, receiverAddr, options) + .then(txid => { + console.log("Multisig Data Written to Blockchain! TXID:", txid); + // Handle the transaction ID (txid), if needed + }) + .catch(error => { + console.error("Error:", error); + // Handle errors if any + }); +``` + +## `deserializeTx(tx)` + +This function allows you to deserialize a transaction from its hexadecimal representation or from a transaction object. It validates the input and returns a transaction object that can be further processed. + +### Parameters: + +- **`tx`** (string | array | object): The input transaction data. It can be either a hexadecimal string representing the transaction, an array containing the transaction data, or a transaction object. + +### Return Value: + +Returns a deserialized transaction object. + +### Throws: + +- Throws an error with the message "Invalid transaction hex" if the input transaction data is a string (hexadecimal representation) and cannot be deserialized. +- Throws an error with the message "Invalid transaction object" if the input transaction data is not a valid hexadecimal string, array, or transaction object. + +### Example Usage: + +```javascript +const serializedTxHex = "0100000001..."; // Replace with the actual transaction hex +const serializedTxArray = [0, 1, 2, ...]; // Replace with the actual transaction array +const transactionObject = { /* Replace with the actual transaction object */ }; + +try { + const deserializedTxFromHex = deserializeTx(serializedTxHex); + console.log("Deserialized Transaction from Hex:", deserializedTxFromHex); + + const deserializedTxFromArray = deserializeTx(serializedTxArray); + console.log("Deserialized Transaction from Array:", deserializedTxFromArray); + + const deserializedTxFromObject = deserializeTx(transactionObject); + console.log("Deserialized Transaction from Object:", deserializedTxFromObject); +} catch (error) { + console.error("Error:", error); + // Handle errors if any +} +``` + +## `floBlockchainAPI.signTx(tx, privateKey, sighashtype = 1)` + +This function allows you to sign a transaction with the provided private key and signature hash type. It takes a transaction object (or its hexadecimal representation), a private key, and an optional signature hash type. It returns the signed transaction in its hexadecimal form. + +### Parameters: + +- **`tx`** (string | array | object): The transaction to be signed. It can be either a hexadecimal string representing the transaction, an array containing the transaction data, or a transaction object. +- **`privateKey`** (string): The private key used for signing the transaction. +- **`sighashtype`** (number, optional, default: 1): The signature hash type. It specifies the way the transaction data is hashed. The default value is 1, indicating SIGHASH_ALL. + +### Return Value: + +Returns the signed transaction in its hexadecimal form. + +### Throws: + +- Throws an error with the message "Invalid Private key" if the provided private key is not valid. + +### Example Usage: + +- In this example, the signTx function is used to sign an unsigned transaction represented as a hexadecimal string. You can modify the unsignedTxHex and privateKey variables with actual transaction data and private key to test the function. + +```javascript +const unsignedTxHex = "0100000001..."; // Replace with the actual unsigned transaction hex +const privateKey = "privateKey"; // Replace with the actual private key + +try { + const signedTxHex = floBlockchainAPI.signTx(unsignedTxHex, privateKey); + console.log("Signed Transaction Hex:", signedTxHex); +} catch (error) { + console.error("Error:", error); + // Handle errors if any +} +``` + +## `floBlockchainAPI.checkSigned(tx, bool = true)` + +This function allows you to check if a transaction is signed correctly. It takes a transaction object (or its hexadecimal representation) and an optional boolean parameter. If the boolean parameter is set to `true` (default), it returns a boolean indicating whether the transaction is fully signed. If set to `false`, it returns an array containing information about the signing status for each input. + +### Parameters: + +- **`tx`** (string | array | object): The transaction to be checked. It can be either a hexadecimal string representing the transaction, an array containing the transaction data, or a transaction object. +- **`bool`** (boolean, optional, default: `true`): A boolean parameter indicating whether to return a boolean result (`true` for fully signed, `false` otherwise) or an array containing signing status information for each input. + +### Return Value: + +- If `bool` is `true` (default), returns `true` if the transaction is fully signed, and `false` otherwise. +- If `bool` is `false`, returns an array containing signing status information for each input. For inputs that require multisig, it includes an object with the properties `{ s: , r: , t: }`. For regular inputs, it includes `true` if signed and `false` otherwise. + +### Throws: + +- Throws an error with the message "signaturesRequired is more than publicKeys" if the number of required signatures is greater than the total number of public keys in a multisig input. + +### Example Usage: + +- In this example, the checkSigned function is used to check if a signed transaction represented as a hexadecimal string is fully signed. The isFullySigned variable contains a boolean indicating whether the transaction is fully signed, and the signingStatus variable contains an array with signing status information for each input. You can modify the signedTxHex variable with an actual signed transaction hex to test the function. + +```javascript +const signedTxHex = "0100000001..."; // Replace with the actual signed transaction hex + +try { + const isFullySigned = floBlockchainAPI.checkSigned(signedTxHex); + console.log("Is Fully Signed:", isFullySigned); + + const signingStatus = floBlockchainAPI.checkSigned(signedTxHex, false); + console.log("Signing Status for Each Input:", signingStatus); +} catch (error) { + console.error("Error:", error); + // Handle errors if any +} +``` + +## `floBlockchainAPI.checkIfSameTx(tx1, tx2)` + +This function allows you to compare two transactions to check if they are the same. It takes two transaction objects (or their hexadecimal representations) and returns a boolean indicating whether the transactions are identical in terms of inputs, outputs, and floData. + +### Parameters: + +- **`tx1`** (string | array | object): The first transaction to be compared. It can be either a hexadecimal string representing the transaction, an array containing the transaction data, or a transaction object. +- **`tx2`** (string | array | object): The second transaction to be compared. It can be either a hexadecimal string representing the transaction, an array containing the transaction data, or a transaction object. + +### Return Value: + +- Returns `true` if the two transactions are identical in terms of inputs, outputs, and floData. Returns `false` otherwise. + +### Example Usage: + +- In this example, the checkIfSameTx function is used to compare two transactions represented as hexadecimal strings. The areSameTransactions variable contains a boolean indicating whether the two transactions are identical in terms of inputs, outputs, and floData. + + + +```javascript +const tx1Hex = "0100000001..."; // Replace with the actual hexadecimal representation of the first transaction +const tx2Hex = "0100000001..."; // Replace with the actual hexadecimal representation of the second transaction + +const areSameTransactions = floBlockchainAPI.checkIfSameTx(tx1Hex, tx2Hex); +console.log("Are the Transactions the Same?", areSameTransactions); +``` + +## `floBlockchainAPI.transactionID(tx)` + +This function calculates the transaction ID (TxID) for a given transaction. It takes a transaction object (or its hexadecimal representation) and returns the corresponding transaction ID. + +### Parameters: + +- **`tx`** (string | array | object): The transaction for which the TxID needs to be calculated. It can be either a hexadecimal string representing the transaction, an array containing the transaction data, or a transaction object. + +### Return Value: + +- Returns the transaction ID (TxID) as a hexadecimal string. + +### Example Usage: + +- In this example, the transactionID function is used to calculate the transaction ID (TxID) for a given transaction represented as a hexadecimal string. The txid variable contains the resulting transaction ID as a hexadecimal string. + +```javascript +const txHex = "0100000001..."; // Replace with the actual hexadecimal representation of the transaction + +const txid = floBlockchainAPI.transactionID(txHex); +console.log("Transaction ID:", txid); +``` + + +## `getTxOutput(txid, index)` + +This function retrieves a specific transaction output (vout) from a given transaction by its transaction ID (TxID) and index. + +### Parameters: + +- **`txid`** (string): The transaction ID (TxID) of the transaction from which to retrieve the output. +- **`index`** (number): The index of the output in the transaction's vout array. + +### Return Value: + +- Returns a Promise that resolves with the specific transaction output (vout) object. + +### Example Usage: + +- In this example, the getTxOutput function is used to retrieve the transaction output at index 0 from the transaction with the specified txid. The function returns a Promise, and the resolved output object contains details about the specific transaction output. + +```javascript +const txid = "1234567890abcdef"; // Replace with the actual transaction ID +const outputIndex = 0; // Replace with the desired output index + +getTxOutput(txid, outputIndex) + .then(output => { + console.log("Transaction Output:", output); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + +# `getOutputAddress(outscript)` + +The `getOutputAddress` function generates a FLO (or Bitcoin) address from a given output script. + +## Parameters: + +- **`outscript`** (Array): An array representing the output script. + +## Return Value: + +- Returns a FLO (or Bitcoin) address as a string. + +## Example Usage: + +- In this function, the input outscript is expected to be an array representing the output script of a transaction output. Depending on the script type (legacy or multisig), it processes the script bytes and version to create the address. The function then returns the generated FLO (or Bitcoin) address as a string. + +```javascript +const outscript = [118, 169, 20, ...]; // Replace with the actual output script + +const address = getOutputAddress(outscript); +console.log("Generated Address:", address); +``` + +# `floBlockchainAPI.parseTransaction(tx)` + +The `parseTransaction` function parses a FLO transaction, extracting inputs, outputs, total amounts, fees, and FLO data. + +## Parameters: + +- **`tx`** (String or Array): The serialized transaction in hexadecimal format or as an array of transaction bytes. + +## Return Value: + +- Returns a Promise that resolves to an object containing parsed transaction details: + - **`inputs`** (Array): An array of objects representing transaction inputs with properties `address` (input address) and `value` (input value). + - **`outputs`** (Array): An array of objects representing transaction outputs with properties `address` (output address) and `value` (output value). + - **`total_input`** (Number): Total value of inputs in FLO. + - **`total_output`** (Number): Total value of outputs in FLO. + - **`fee`** (Number): Transaction fee in FLO. + - **`floData`** (String): FLO data included in the transaction. + +## Example Usage: + +- In this function, you provide the tx parameter, which can be either a hexadecimal string or an array of transaction bytes. The function then resolves with an object containing detailed information about the parsed transaction, including inputs, outputs, total amounts, fees, and FLO data. + +```javascript +const txHex = '0123456789abcdef...'; // Replace with the actual transaction hex string + +floBlockchainAPI.parseTransaction(txHex) + .then(parsedTx => { + console.log("Parsed Transaction Details:"); + console.log("Inputs:", parsedTx.inputs); + console.log("Outputs:", parsedTx.outputs); + console.log("Total Input:", parsedTx.total_input, "FLO"); + console.log("Total Output:", parsedTx.total_output, "FLO"); + console.log("Transaction Fee:", parsedTx.fee, "FLO"); + console.log("FLO Data:", parsedTx.floData); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + +# `floBlockchainAPI.broadcastTx(signedTxHash)` + +The `broadcastTx` function broadcasts a signed FLO transaction to the blockchain using the API. + +## Parameters: + +- **`signedTxHash`** (String): The signed FLO transaction hash in hexadecimal format. + +## Return Value: + +- Returns a Promise that resolves to the broadcasted transaction ID if successful. + +## Example Usage: + +- In this function, you provide the signedTxHash parameter, which is the hexadecimal representation of the signed FLO transaction. The function then broadcasts the transaction to the FLO blockchain using the API endpoint. If successful, it resolves with the transaction ID. + +```javascript +const signedTxHash = '0123456789abcdef...'; // Replace with the actual signed transaction hash + +floBlockchainAPI.broadcastTx(signedTxHash) + .then(txid => { + console.log("Transaction successfully broadcasted! Transaction ID:", txid); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + +# `floBlockchainAPI.getTx(txid)` + +The `getTx` function retrieves information about a specific FLO transaction using its transaction ID (txid). + +## Parameters: + +- **`txid`** (String): The transaction ID (txid) of the FLO transaction to retrieve information about. + +## Return Value: + +- Returns a Promise that resolves to the transaction information if the transaction with the provided txid exists. + +## Example Usage: + +```javascript +const txid = '0123456789abcdef...'; // Replace with the actual transaction ID + +floBlockchainAPI.getTx(txid) + .then(transactionInfo => { + console.log("Transaction Information:", transactionInfo); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + +# `floBlockchainAPI.waitForConfirmation(txid, max_retry = -1, retry_timeout = 20)` + +The `waitForConfirmation` function waits for a FLO transaction to be confirmed on the blockchain. It checks the confirmation status of the transaction at regular intervals until it gets confirmed or until the maximum retry limit is reached. + +## Parameters: + +- **`txid`** (String): The transaction ID (txid) of the FLO transaction to wait for confirmation. +- **`max_retry`** (Optional, Number): The maximum number of times to retry checking for confirmation. Set to `-1` for infinite retries (default: `-1`). +- **`retry_timeout`** (Optional, Number): The time interval (in seconds) between each confirmation check attempt (default: `20` seconds). + +## Return Value: + +- Returns a Promise that resolves to the confirmed transaction object when the transaction is confirmed. +- Rejects the Promise with an error message if the transaction is not found or if the maximum retry limit is reached. + +## Example Usage: + +- This function is useful for scenarios where developers need to wait for a transaction to be confirmed before proceeding with subsequent actions. It provides a way to handle waiting for transactions asynchronously, allowing for more efficient and responsive application behavior. + +```javascript +const txid = '0123456789abcdef...'; // Replace with the actual transaction ID + +floBlockchainAPI.waitForConfirmation(txid, 10, 30) + .then(confirmedTx => { + console.log("Transaction Confirmed:", confirmedTx); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + +# `floBlockchainAPI.readTxs(addr, options = {})` + +The `readTxs` function retrieves a list of FLO transactions associated with a specific address. + +## Parameters: + +- **`addr`** (String): The FLO address for which transactions are to be retrieved. +- **`options`** (Object, Optional): Additional options for customizing the transaction retrieval. + - **`page`** (Number, Optional): The page number for paginated results (default: `1`). + - **`pageSize`** (Number, Optional): The number of transactions per page (default: `10`). + - **`confirmed`** (Boolean, Optional): If set to `true`, retrieves only confirmed transactions (default: `false`). + +## Return Value: + +- Returns a Promise that resolves to an object containing transaction details associated with the given address. + - **`txs`** (Array): An array of transaction objects containing details such as transaction ID, inputs, outputs, etc. + - **`totalItems`** (Number): Total number of transactions associated with the address. + - **`totalPages`** (Number): Total number of pages based on the given `pageSize`. + +## Example Usage: + +```javascript +const address = 'FLO_ADDRESS'; // Replace with the actual FLO address + +floBlockchainAPI.readTxs(address, { page: 1, pageSize: 10, confirmed: true }) + .then(transactionDetails => { + console.log("Transactions for Address:", transactionDetails.txs); + console.log("Total Transactions:", transactionDetails.totalItems); + console.log("Total Pages:", transactionDetails.totalPages); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + +# `readAllTxs_oldSupport(addr, options, ignoreOld = 0, cacheTotal = 0)` + +The `readAllTxs_oldSupport` function retrieves all FLO transactions associated with a specific address with backward compatibility for `floBlockchainAPI` versions prior to `v2.5.6`. + +## Parameters: + +- **`addr`** (String): The FLO address for which transactions are to be retrieved. +- **`options`** (Object): Additional options for customizing the transaction retrieval. + - **`page`** (Number, Optional): The page number for paginated results (default: `1`). + - **`pageSize`** (Number, Optional): The number of transactions per page (default: `10`). + - **`confirmed`** (Boolean, Optional): If set to `true`, retrieves only confirmed transactions (default: `false`). +- **`ignoreOld`** (Number, Optional): Number of old transactions to ignore from the beginning of the result set (default: `0`). +- **`cacheTotal`** (Number, Optional): Total number of transactions already cached from previous calls (default: `0`). + +## Return Value: + +- Returns a Promise that resolves to an array of transaction objects containing details such as transaction ID, inputs, outputs, etc. + +## Example Usage: + +-This function provides backward compatibility for versions of floBlockchainAPI prior to v2.5.6. It retrieves all FLO transactions associated with a specific address while allowing developers to ignore a specified number of old transactions from the beginning of the result set. + +```javascript +const address = 'FLO_ADDRESS'; // Replace with the actual FLO address + +const options = { + page: 1, + pageSize: 10, + confirmed: true +}; + +const ignoreOld = 5; // Ignore 5 old transactions from the beginning of the result set + +readAllTxs_oldSupport(address, options, ignoreOld) + .then(transactions => { + console.log("All Transactions (Ignoring 5 Old Transactions):", transactions); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + +# `readAllTxs_new(addr, options, lastItem)` + +The `readAllTxs_new` function retrieves FLO transactions associated with a specific address starting from the most recent transactions up to a specified `lastItem`. + +## Parameters: + +- **`addr`** (String): The FLO address for which transactions are to be retrieved. +- **`options`** (Object): Additional options for customizing the transaction retrieval. + - **`page`** (Number, Optional): The page number for paginated results (default: `1`). + - **`pageSize`** (Number, Optional): The number of transactions per page (default: `10`). + - **`confirmed`** (Boolean, Optional): If set to `true`, retrieves only confirmed transactions (default: `false`). +- **`lastItem`** (String, Optional): The transaction ID of the last known transaction. The retrieval will start from this transaction onward. + +## Return Value: + +- Returns a Promise that resolves to an array of transaction objects containing details such as transaction ID, inputs, outputs, etc. + +## Example Usage: + +- This function retrieves FLO transactions associated with a specific address starting from the most recent transactions up to a specified lastItem. Developers can provide the ID of the last known transaction, and the function will fetch transactions from that point onward. + +```javascript +const address = 'FLO_ADDRESS'; // Replace with the actual FLO address + +const options = { + page: 1, + pageSize: 10, + confirmed: true +}; + +const lastItemTxID = 'LAST_TXID'; // Replace with the actual transaction ID of the last known transaction + +readAllTxs_new(address, options, lastItemTxID) + .then(transactions => { + console.log("Transactions Starting from Last Known Item:", transactions); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + +# `floBlockchainAPI.readAllTxs(addr, options)` + +The `readAllTxs` function retrieves all FLO transactions associated with a specific address, with the newest transactions appearing first. + +## Parameters: + +- **`addr`** (String): The FLO address for which transactions are to be retrieved. +- **`options`** (Object, Optional): Additional options for customizing the transaction retrieval. + - **`ignoreOld`** (Number, Optional): Number of old transactions to ignore from the beginning of the transaction history (backward compatibility with floBlockchainAPI versions prior to v2.5.6). + - **`after`** (String, Optional): The transaction ID of the last known transaction. The retrieval will start from this transaction onward. + - **`page`** (Number, Optional): The page number for paginated results (default: `1`). + - **`pageSize`** (Number, Optional): The number of transactions per page (default: `10`). + - **`confirmed`** (Boolean, Optional): If set to `true`, retrieves only confirmed transactions (default: `false`). + +## Return Value: + +- Returns a Promise that resolves to an object containing: + - **`lastItem`** (String): The transaction ID of the last known transaction. + - **`items`** (Array): An array of transaction objects containing details such as transaction ID, inputs, outputs, etc. + +## Example Usage: + +-This function retrieves all FLO transactions associated with a specific address, allowing developers to specify various options such as ignoring old transactions, starting from a specific transaction ID, and pagination. + +```javascript +const address = 'FLO_ADDRESS'; // Replace with the actual FLO address + +const options = { + page: 1, + pageSize: 10, + confirmed: true +}; + +// For backward compatibility (floBlockchainAPI < v2.5.6) +// const options = { +// ignoreOld: 5 +// }; + +// For starting from a specific transaction ID +// const options = { +// after: 'LAST_TXID' +// }; + +floBlockchainAPI.readAllTxs(address, options) + .then(result => { + console.log("Last Known Transaction ID:", result.lastItem); + console.log("Transactions:", result.items); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + + +# `floBlockchainAPI.readData(addr, options)` + +The `readData` function retrieves FLO transactions associated with a specific address, with optional filtering options. + +## Parameters: + +- **`addr`** (String): The FLO address for which transactions are to be retrieved. +- **`options`** (Object, Optional): Additional options for customizing the transaction retrieval. + - **`confirmed`** (Boolean, Optional): If set to `true`, retrieves only confirmed transactions (default: `true`). + - **`after`** (String, Optional): The transaction ID of the last known transaction. The retrieval will start from this transaction onward. + - **`ignoreOld`** (Number, Optional): Number of old transactions to ignore from the beginning of the transaction history. + - **`sentOnly`** (Boolean, Optional): If set to `true`, retrieves only transactions sent from the specified address. + - **`receivedOnly`** (Boolean, Optional): If set to `true`, retrieves only transactions received by the specified address. + - **`senders`** (Array or String, Optional): Array of sender addresses to filter transactions. + - **`receivers`** (Array or String, Optional): Array of receiver addresses to filter transactions. + - **`pattern`** (String, Optional): JSON key pattern to filter transactions containing specific keys. + - **`filter`** (Function, Optional): Custom function to further filter transactions based on the FLO data. + - **`tx`** (Boolean, Optional): If set to `true`, returns detailed transaction objects (default: `false`). + +## Return Value: + +- Returns a Promise that resolves to an object containing: + - **`lastItem`** (String): The transaction ID of the last known transaction. + - **`items`** (Array, Optional): An array of transaction objects or FLO data strings based on the `tx` option. + +## Example Usage: + +```javascript +const address = 'FLO_ADDRESS'; // Replace with the actual FLO address + +const options = { + confirmed: true, + after: 'LAST_TXID', + sentOnly: true, + senders: ['SENDER_ADDRESS'], + receivers: ['RECEIVER_ADDRESS'], + pattern: 'KEY_PATTERN', + filter: (floData) => { + // Custom filter logic, return true to include the transaction + return floData.includes('FILTER_TEXT'); + }, + tx: true +}; + +floBlockchainAPI.readData(address, options) + .then(result => { + console.log("Last Known Transaction ID:", result.lastItem); + console.log("Transactions:", result.items); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + +# `floBlockchainAPI.getLatestData(addr, caseFn, options)` + +The `getLatestData` function retrieves the latest confirmed FLO transaction data associated with a specific address based on a provided validation function (`caseFn`). + +## Parameters: + +- **`addr`** (String): The FLO address for which transactions are to be retrieved. +- **`caseFn`** (Function): A validation function that takes FLO data as input and returns a boolean value (`true` for valid, `false` for invalid). +- **`options`** (Object, Optional): Additional options for customizing the transaction retrieval. + - **`confirmed`** (Boolean, Optional): If set to `true`, retrieves only confirmed transactions (default: `true`). + - **`page`** (Number, Optional): The page number of transactions to retrieve (default: `1`). + - **`after`** (String, Optional): The transaction ID of the last known transaction. The retrieval will start from this transaction onward. + - **`sentOnly`** (Boolean, Optional): If set to `true`, retrieves only transactions sent from the specified address. + - **`receivedOnly`** (Boolean, Optional): If set to `true`, retrieves only transactions received by the specified address. + - **`senders`** (Array or String, Optional): Array of sender addresses to filter transactions. + - **`receivers`** (Array or String, Optional): Array of receiver addresses to filter transactions. + - **`tx`** (Boolean, Optional): If set to `true`, returns detailed transaction object (default: `false`). + +## Return Value: + +- Returns a Promise that resolves to an object containing: + - **`lastItem`** (String): The transaction ID of the last known transaction that matches the validation function. + - **`item`** (Object, Optional): Detailed transaction object if `tx` option is set to `true`. + - **`data`** (String, Optional): FLO data that matches the validation function. + +## Example Usage: + +```javascript +const address = 'FLO_ADDRESS'; // Replace with the actual FLO address + +const validationFunction = (floData) => { + // Custom validation logic, return true for valid data, false otherwise + return floData.includes('VALIDATION_TEXT'); +}; + +const options = { + confirmed: true, + page: 1, + after: 'LAST_TXID', + sentOnly: true, + senders: ['SENDER_ADDRESS'], + receivers: ['RECEIVER_ADDRESS'], + tx: true +}; + +floBlockchainAPI.getLatestData(address, validationFunction, options) + .then(result => { + console.log("Last Valid Transaction ID:", result.lastItem); + console.log("Transaction Data:", result.data); + // If tx option is set to true + // console.log("Transaction Object:", result.item); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + diff --git a/messenger/docs/floCloud.md b/messenger/docs/floCloud.md new file mode 100644 index 0000000..9839a7d --- /dev/null +++ b/messenger/docs/floCloud.md @@ -0,0 +1,968 @@ +## ws_connect Function + +The `ws_connect` function is used to establish a WebSocket connection to a specified supernode (`snID`). It performs checks to ensure that the supernode is in the list of available supernodes and is active before attempting to establish a connection. + +### Parameters + +- `snID` (string): The ID of the supernode to which the WebSocket connection will be established. + +### Return Value + +- Returns a Promise that resolves with the WebSocket connection object if the connection is successfully established, and rejects with an error message if the specified supernode is not in the list of available supernodes or is inactive. + +### Example Usage + +- In this example, the ws_connect function establishes a WebSocket connection to the specified supernode (snID). It checks if the supernode is available and active before attempting to establish the connection. Once the connection is established, the function resolves with the WebSocket connection object, allowing communication with the server in real-time. + +```javascript +const snID = "exampleSnID"; + +ws_connect(snID) + .then(wsConnection => { + console.log("WebSocket connection established:", wsConnection); + // You can now use wsConnection to send and receive messages. + }) + .catch(error => { + console.error("Error occurred during WebSocket connection:", error); + }); +``` + +## ws_activeConnect Function + +The `ws_activeConnect` function is used to establish an active WebSocket connection to a supernode, handling failover by selecting the next or previous active supernode from the routing table (`kBucket`). + +### Parameters + +- `snID` (string): The ID of the supernode to which the WebSocket connection will be established. If the provided supernode is inactive, the function selects the next or previous active supernode based on the `reverse` parameter. +- `reverse` (boolean, optional): A flag indicating whether to select the previous active supernode (`true`) or the next active supernode (`false`). Default is `false`. + +### Return Value + +- Returns a Promise that resolves with the active WebSocket connection object if the connection is successfully established, and rejects with an error message if all supernodes are inactive or if all attempts to establish an active connection fail. + +### Example Usage + +- In this example, the ws_activeConnect function establishes an active WebSocket connection to the specified supernode (snID). If the specified supernode is inactive, it selects the next or previous active supernode based on the reverse parameter. The function handles failover by attempting to connect to the active supernodes in the routing table until a successful connection is established, allowing real-time communication with the server. + +```javascript +const snID = "exampleSnID"; + +ws_activeConnect(snID) + .then(wsConnection => { + console.log("Active WebSocket connection established:", wsConnection); + // You can now use wsConnection to send and receive messages in real-time. + }) + .catch(error => { + console.error("Error occurred during active WebSocket connection:", error); + }); + +ws_activeConnect(snID, true) // To establish a connection to the previous active supernode + .then(wsConnection => { + console.log("Active WebSocket connection established:", wsConnection); + // You can now use wsConnection to send and receive messages in real-time. + }) + .catch(error => { + console.error("Error occurred during active WebSocket connection:", error); + }); +``` + + + + +## fetch_API Function + +The `fetch_API` function is used to send an HTTP request to the specified supernode (`snID`). It allows making GET and POST requests and handles response validation. + +### Parameters + +- `snID` (string): The ID of the supernode to which the request will be sent. +- `data` (string or object): The data to be sent with the request. If it is a string, it is appended to the URL as query parameters for GET requests. If it is an object with `method` property set to `"POST"`, it is sent as the body for a POST request. + +### Return Value + +- Returns a Promise that resolves with the response object if the request is successful (HTTP status code 200 or 400 or 500), and rejects with an error message if the request fails. + +### Example Usage + +- In this example, the fetch_API function sends an HTTP request to the specified supernode (snID). It handles both GET and POST requests and provides flexibility in sending data with the request. The function validates the response and resolves or rejects the Promise based on the response status code. + +```javascript +const snID = "exampleSnID"; +const queryParams = "param1=value1¶m2=value2"; + +fetch_API(snID, queryParams) + .then(response => { + console.log("GET request successful. Response:", response); + }) + .catch(error => { + console.error("Error occurred during the GET request:", error); + }); + +const postData = { + method: "POST", + body: JSON.stringify({ key: "value" }) +}; + +fetch_API(snID, postData) + .then(response => { + console.log("POST request successful. Response:", response); + }) + .catch(error => { + console.error("Error occurred during the POST request:", error); + }); +``` + +## fetch_ActiveAPI Function + +The `fetch_ActiveAPI` function is used to send an HTTP request to an active supernode, handling failover by selecting the next or previous active supernode from the routing table (`kBucket`). + +### Parameters + +- `snID` (string): The ID of the supernode to which the request will be sent. If the provided supernode is inactive, the function selects the next or previous active supernode based on the `reverse` parameter. +- `data` (string or object): The data to be sent with the request. +- `reverse` (boolean, optional): A flag indicating whether to select the previous active supernode (`true`) or the next active supernode (`false`). Default is `false`. + +### Return Value + +- Returns a Promise that resolves with the response object if the request is successful, and rejects with an error message if all supernodes are inactive or if all attempts to fetch from active supernodes fail. + +### Example Usage + +- In this example, the fetch_ActiveAPI function sends an HTTP request to an active supernode (snID). If the specified supernode is inactive, it selects the next or previous active supernode based on the reverse parameter. The function handles failover by attempting to fetch from the active supernodes in the routing table until a successful response is received. + +```javascript +const snID = "exampleSnID"; +const queryParams = "param1=value1¶m2=value2"; + +fetch_ActiveAPI(snID, queryParams) + .then(response => { + console.log("Request successful. Response:", response); + }) + .catch(error => { + console.error("Error occurred during the request:", error); + }); + +const postData = { + method: "POST", + body: JSON.stringify({ key: "value" }) +}; + +fetch_ActiveAPI(snID, postData, true) + .then(response => { + console.log("Request successful. Response:", response); + }) + .catch(error => { + console.error("Error occurred during the request:", error); + }); +``` + +## singleRequest Function + +The `singleRequest` function is used to send a single HTTP request to the specified flo server (`floID`). It allows customization of the request method and data payload. + +### Parameters + +- `floID` (string): The ID of the flo server to which the request will be sent. +- `data_obj` (object): The data object to be sent with the request. It will be serialized to JSON for POST requests and URL parameters for other request methods. +- `method` (string, optional): The HTTP request method. Default is `"POST"`. Can be `"POST"` or any other valid HTTP method like `"GET"`, `"PUT"`, `"DELETE"`, etc. + +### Return Value + +- Returns a Promise that resolves with the JSON response body if the request is successful (HTTP status code 200), and rejects with an error message if the request fails. + +### Example Usage +- In this example, the singleRequest function sends a single HTTP request to the cloud server. The request method and data payload are customizable + +```javascript +const floID = "exampleFloID"; +const dataObj = { + key: "value", + anotherKey: "anotherValue" +}; + +singleRequest(floID, dataObj, "POST") + .then(response => { + console.log("POST request successful. Response:", response); + }) + .catch(error => { + console.error("Error occurred during the request:", error); + }); +``` + + +## liveRequest Function + +The `liveRequest` function is used to make a live request to FLO cloud node using WebSockets. It takes the following parameters: + +### Parameters + +- `floID` (string): The ID of the flo server. +- `request` (object): An object containing filter criteria for the live request. It can have the following properties: + - `status` (boolean, optional): If true, includes all data in the response. If false, filters the response based on other criteria. + - `trackList` (array of strings, optional): An array of keys to include in the response when `status` is false. + - `atVectorClock` (string, optional): Filters data with vector clock equal to the specified value. + - `lowerVectorClock` (string, optional): Filters data with vector clock greater than or equal to the specified value. + - `upperVectorClock` (string, optional): Filters data with vector clock less than or equal to the specified value. + - `afterTime` (number, optional): Filters data with log_time greater than the specified value. + - `application` (string, optional): Filters data with the specified application. + - `receiverID` (string, optional): Filters data with the specified receiver ID or proxy ID. + - `comment` (string, optional): Filters data with the specified comment. + - `type` (string, optional): Filters data with the specified type. + - `senderID` (array of strings, optional): Filters data with sender IDs included in the specified array. + +- `callback` (function): A callback function to handle the response data and errors. + +### Return Value + +- Returns a Promise that resolves with a unique `randID` (string) representing the live request. + +### Example Usage + +- In this example, the liveRequest function connects to the specified flo server (floID), sends a live request with the provided filter criteria (request), and handles the response using the callback function. + +```javascript +const floID = "exampleFloID"; +const request = { + status: true, + application: "exampleApp", + callback: function(data, error) { + if (error) { + console.error("Error occurred:", error); + } else { + console.log("Filtered data received:", data); + } + } +}; + +liveRequest(floID, request, callback) + .then(randID => { + console.log("Live request sent with ID:", randID); + }) + .catch(error => { + console.error("Error sending live request:", error); + }); +``` + + +## proxyID Function + +The `proxyID` function is used to convert a given address into a proxy ID. It supports various address formats including legacy encoding, bech32 encoding, and public key hex format. + +### Parameters + +- `address` (string): The input address to be converted into a proxy ID. + +### Return Value + +- Returns a string representing the proxy ID derived from the input address. + +### Address Formats Supported + +- **Legacy Encoding (Base58)** + - Addresses with lengths 33 or 34 characters are supported. +- **Bech32 Encoding** + - Addresses with lengths 42 or 62 characters are supported. +- **Public Key Hex** + - Addresses with length 66 characters are supported. + +### Example Usage + +- In this example, the proxyID function converts a legacy Bitcoin address into a proxy ID. The function automatically detects the input address format and converts it into the corresponding proxy ID format. + +```javascript +const address = "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2"; + +const proxyID = util.proxyID(address); +console.log("Proxy ID:", proxyID); +``` + + + +## updateObject Function + +The `updateObject` function is used to update an object in the `appObjects` data store based on the incoming dataset. It performs actions like resetting or updating the object based on the dataset's content. + +### Parameters + +- `objectName` (string): The name of the object to be updated. +- `dataSet` (object): The dataset containing the updates for the specified object. + +### Functionality + +- The function processes the dataset for the specified object and performs the following actions: + - If the comment in the dataset is "RESET," the function resets the object with the data provided in the `message.reset` property. + - If the comment in the dataset is "UPDATE," the function updates the object by merging the changes provided in the `message.diff` property using the `diff.merge` function. + +### Example Usage + +In this example, the updateObject function processes the dataSet and updates the specified object (exampleObject) based on the provided reset and diff actions. The function maintains the last version control (lastVC) and stores the updated object in the appObjects data store. + + +```javascript +const objectName = "exampleObject"; +const dataSet = { + "123": { type: "exampleObject", comment: "RESET", message: { reset: { key: "value" } } }, + "456": { type: "exampleObject", comment: "UPDATE", message: { diff: { updatedKey: "updatedValue" } } } +}; + +updateObject(objectName, dataSet); +``` + +### Note +- The diff.merge function and other related functions used in the implementation are assumed to be available in the scope where this function is used. + + +## storeGeneral Function + +The `storeGeneral` function is used to store general data entries in the `generalData` data store. It updates the data store with the provided dataset for a specific foreign key (`fk`). + +### Parameters + +- `fk` (string): The foreign key indicating the category or type of the general data. +- `dataSet` (object): The dataset containing the general data entries to be stored. + +### Functionality + +- The function updates the `generalData` data store for the specified foreign key with the entries from the provided dataset. +- It checks the `log_time` property of each entry in the dataset and updates the `lastVC` (last version control) property for the specified foreign key with the maximum log time value among the entries. + +### Example Usage + +- In this example, the storeGeneral function updates the generalData data store for the specified foreign key (exampleForeignKey) with the provided dataset. It also updates the lastVC property with the maximum log time value from the entries in the dataset. + +```javascript +const fk = "exampleForeignKey"; +const dataSet = { + "123": { log_time: 1633363200000, data: "Entry 1" }, + "124": { log_time: 1633363800000, data: "Entry 2" }, + // ... more data entries +}; + +storeGeneral(fk, dataSet); +``` + +### Note +Ensure that the necessary data structures and storage mechanisms, such as generalData and lastVC, are defined and available in the scope where this function is used. + +## objectifier Function + +The `objectifier` function is used to transform data from an array format into an object format. It takes an array of data objects as input and returns an object where the keys are taken from the `vectorClock` property of the input objects and the values are the input objects themselves with an additional property `message` that is decoded using the `decodeMessage` function. + +### Parameters + +- `data` (array or object): The input data to be transformed. If `data` is not an array, it will be converted into a single-element array before processing. + +### Return Value + +- Returns an object where keys are taken from the `vectorClock` property of the input objects, and the values are objects with the following properties: + - `vectorClock` (string): The key used in the input data. + - `message` (string): The decoded message obtained by applying the `decodeMessage` function to the `message` property of the input data. + +### Example Usage + +```javascript +const inputData = [ + { vectorClock: "12345", message: "Encoded Message 1" }, + { vectorClock: "67890", message: "Encoded Message 2" } +]; + +const transformedData = objectifier(inputData); +console.log(transformedData); + + +/* +Output +{ + "12345": { vectorClock: "12345", message: "Decoded Message 1" }, + "67890": { vectorClock: "67890", message: "Decoded Message 2" } +} +*/ +``` + + +## setStatus Function + +The `setStatus` function is used to set the online status for a user specified by their `user_id` on the floCloud platform. It takes an options object as input, allowing customization of the request parameters. + +### Parameters + +- `options` (object, optional): An object containing the following properties: + - `callback` (function, optional): A callback function to handle the response data and errors. If not provided, the default callback function is used. + - `application` (string, optional): The application name associated with the status update. If not provided, the default application name is used. + - `refID` (string, optional): The reference ID used for the live request. If not provided, the default admin ID is used. + +### Return Value + +- Returns a Promise that resolves with the result of the live request upon success. + +### Example Usage + +- In this example, the setStatus function sets the online status for the user specified by user.id on the floCloud platform. The function allows customization of the request parameters through the options object and provides a callback function to handle the response data and errors. + +```javascript +const options = { + callback: function(data, error) { + if (error) { + console.error("Error occurred:", error); + } else { + console.log("Status update successful:", data); + } + }, + application: "MyApp", + refID: "admin123" +}; + +floCloudAPI.setStatus(options) + .then(result => { + console.log("Status set successfully:", result); + }) + .catch(error => { + console.error("Error setting status:", error); + }); +``` + +## requestStatus Function + +The `requestStatus` function is used to request the status of one or more `floID`s specified in the `trackList`. It sends a live request to obtain the status of the specified `floID`s and returns the response. + +### Parameters + +- `trackList` (string or array of strings): The `floID`(s) for which the status will be requested. Can be a single `floID` or an array of multiple `floID`s. +- `options` (object, optional): An object containing the following properties: + - `callback` (function, optional): A callback function to handle the response data and errors. If not provided, the default callback function is used. + - `application` (string, optional): The application name associated with the status request. If not provided, the default application name is used. + - `refID` (string, optional): The reference ID used for the live request. If not provided, the default admin ID is used. + +### Return Value + +- Returns a Promise that resolves with the response data containing the status of the specified `floID`(s) upon success. + +### Example Usage + +- In this example, the requestStatus function sends a live request to obtain the status of the floID(s) specified in the trackList. The function allows customization of the request parameters through the options object and provides a callback function to handle the response data and errors. + +```javascript +const trackList = ["floID1", "floID2", "floID3"]; +const options = { + callback: function(data, error) { + if (error) { + console.error("Error occurred:", error); + } else { + console.log("Status data received:", data); + } + }, + application: "MyApp", + refID: "admin123" +}; + +floCloudAPI.requestStatus(trackList, options) + .then(statusData => { + console.log("Status request successful:", statusData); + }) + .catch(error => { + console.error("Error sending status request:", error); + }); +``` + +## sendApplicationData Function + +The `sendApplicationData` function is used to send application-specific data messages to a receiver identified by their `receiverID`. It allows customization of the message content, type, receiver ID, application name, and additional comments. + +### Parameters + +- `message` (string): The application-specific message to be sent. +- `type` (string): The type of the application data. +- `options` (object, optional): An object containing the following properties: + - `receiverID` (string, optional): The ID of the receiver for the application data message. If not provided, the default admin ID is used. + - `application` (string, optional): The application name associated with the data message. If not provided, the default application name is used. + - `comment` (string, optional): Additional comments or notes associated with the data message. + +### Return Value + +- Returns a Promise that resolves with the response data upon successful delivery of the application data message. + +### Example Usage + +- In this example, the sendApplicationData function sends an application-specific data message with the specified content, type, receiver ID, application name, and additional comments. The function allows customization of the message parameters through the options object and resolves with the response data upon successful delivery of the message. + +```javascript +const message = "Hello, this is an application data message."; +const type = "text"; +const options = { + receiverID: "receiverUserID", + application: "MyApp", + comment: "Optional comment for the message." +}; + +floCloudAPI.sendApplicationData(message, type, options) + .then(response => { + console.log("Application data message sent successfully:", response); + }) + .catch(error => { + console.error("Error occurred while sending application data message:", error); + }); +``` + +## requestApplicationData Function + +The `requestApplicationData` function is a versatile method used to request data from the supernode cloud. It allows customization of the request parameters including the type of data, receiver and sender IDs, application name, vector clocks, timestamp, and request method. + +### Parameters + +- `type` (string): The type of data to be requested. +- `options` (object, optional): An object containing the following properties: + - `receiverID` (string, optional): The ID of the receiver for the data request. If not provided, the default admin ID is used. + - `senderID` (string, optional): The ID of the sender for the data request. If not provided, it's set to `undefined`. + - `application` (string, optional): The application name associated with the data request. If not provided, the default application name is used. + - `comment` (string, optional): Additional comments or notes for the data request. If not provided, it's set to `undefined`. + - `lowerVectorClock` (string, optional): The lower boundary for vector clock filtering. If not provided, it's set to `undefined`. + - `upperVectorClock` (string, optional): The upper boundary for vector clock filtering. If not provided, it's set to `undefined`. + - `atVectorClock` (string, optional): The specific vector clock at which the data is requested. If not provided, it's set to `undefined`. + - `afterTime` (number, optional): Request data after the specified timestamp. If not provided, it's set to `undefined`. + - `mostRecent` (boolean, optional): If set to `true`, requests the most recent data. If not provided, it's set to `undefined`. + - `method` (string, optional): The HTTP request method. Default is `"GET"`. Can be `"GET"` or `"POST"`. + +### Return Value + +- Returns a Promise that resolves with the response data upon successful retrieval of the requested data. + +### Example Usage + +- In this example, the requestApplicationData function sends a customizable data request to the supernode cloud. It allows flexibility in specifying request parameters, including vector clocks, timestamps, and request method. The function resolves with the response data upon successful retrieval of the requested data. + +```javascript +const type = "exampleData"; +const options = { + receiverID: "receiverUserID", + senderID: "senderUserID", + application: "MyApp", + comment: "Optional comment for the request.", + lowerVectorClock: "123", + upperVectorClock: "456", + atVectorClock: "789", + afterTime: 1633363200000, + mostRecent: true, + method: "POST", + callback: function(data, error) { + if (error) { + console.error("Error occurred:", error); + } else { + console.log("Data received:", data); + } + } +}; + +floCloudAPI.requestApplicationData(type, options) + .then(response => { + console.log("Data request successful:", response); + }) + .catch(error => { + console.error("Error sending data request:", error); + }); +``` + +## editApplicationData Function + +The `editApplicationData` function is used by the sender to edit the comment of specific data in the supernode cloud identified by its `vectorClock`. It retrieves the data, verifies the sender's identity, edits the comment, and updates the data in the supernode cloud. + +### Parameters + +- `vectorClock` (string): The vector clock of the data to be edited. +- `comment_edit` (string): The edited comment to be applied to the data. +- `options` (object, optional): An object containing the following properties: + - `receiverID` (string, optional): The ID of the receiver for the edited data. If not provided, the default admin ID is used. + +### Return Value + +- Returns a Promise that resolves with the response data upon successful editing of the data comment. + +### Example Usage + +- In this example, the editApplicationData function allows the sender to edit the comment of specific data identified by its vectorClock. The function verifies the sender's identity and ensures that only the sender can edit the comment. Upon successful editing, the function resolves with the response data. + +```javascript +const vectorClock = "exampleVectorClock123"; +const editedComment = "This is the edited comment for the data."; +const options = { + receiverID: "receiverUserID" +}; + +floCloudAPI.editApplicationData(vectorClock, editedComment, options) + .then(response => { + console.log("Data comment edited successfully:", response); + }) + .catch(error => { + console.error("Error editing data comment:", error); + }); +``` + +## tagApplicationData Function + +The `tagApplicationData` function is used by subAdmins to tag specific data in the supernode cloud identified by its `vectorClock`. It allows subAdmins to add tags to the data for organizational purposes. + +### Parameters + +- `vectorClock` (string): The vector clock of the data to be tagged. +- `tag` (string): The tag to be applied to the data. +- `options` (object, optional): An object containing the following properties: + - `receiverID` (string, optional): The ID of the receiver for the tagged data. If not provided, the default admin ID is used. + +### Return Value + +- Returns a Promise that resolves with the response data upon successful tagging of the data. + +### Example Usage + +- In this example, the tagApplicationData function allows subAdmins to tag specific data identified by its vectorClock. SubAdmins can add tags to the data for organizational purposes. The function ensures that only subAdmins have access to tagging data, and upon successful tagging, it resolves with the response data. + +```javascript +const vectorClock = "exampleVectorClock123"; +const tag = "important"; + +floCloudAPI.tagApplicationData(vectorClock, tag) + .then(response => { + console.log("Data tagged successfully:", response); + }) + .catch(error => { + console.error("Error tagging data:", error); + }); +``` + +## noteApplicationData Function + +The `noteApplicationData` function allows users (receivers) and subAdmins (if the receiver is the `adminID`) to add notes to specific data in the supernode cloud identified by its `vectorClock`. + +### Parameters + +- `vectorClock` (string): The vector clock of the data to be noted. +- `note` (string): The note to be added to the data. +- `options` (object, optional): An object containing the following properties: + - `receiverID` (string, optional): The ID of the receiver for the noted data. If not provided, the default admin ID is used. + +### Return Value + +- Returns a Promise that resolves with the response data upon successful noting of the data. + +### Example Usage + +- In this example, the noteApplicationData function allows users and subAdmins to add notes to specific data identified by its vectorClock in the supernode cloud. The function ensures that only the receiver and subAdmins (if the receiver is the adminID) can add notes to the data. Upon successful noting, it resolves with the response data. + +```javascript +const vectorClock = "exampleVectorClock123"; +const note = "This is a note for the data."; + +floCloudAPI.noteApplicationData(vectorClock, note) + .then(response => { + console.log("Data noted successfully:", response); + }) + .catch(error => { + console.error("Error adding note to data:", error); + }); +``` + +## sendGeneralData Function + +The `sendGeneralData` function is used to send general data messages to the supernode cloud. It allows customization of the data content, type, encryption, and other options before sending. + +### Parameters + +- `message` (object or string): The general data to be sent. It can be an object or a string. +- `type` (string): The type of the general data. +- `options` (object, optional): An object containing the following properties: + - `encrypt` (boolean or string, optional): If `true`, the `message` will be encrypted using the default encryption key. If a string is provided, it will be used as the encryption key. If not provided or `false`, no encryption will be applied. + +### Return Value + +- Returns a Promise that resolves with the response data upon successful sending of the general data message. + +### Example Usage + +- In this example, the sendGeneralData function sends a general data message with the specified content and type to the supernode cloud. The function allows customization of the encryption option through the options object. Upon successful sending, it resolves with the response data. + +```javascript +const message = { + key: "value" +}; +const type = "exampleType"; +const options = { + encrypt: true // Encrypt the message using the default encryption key +}; + +floCloudAPI.sendGeneralData(message, type, options) + .then(response => { + console.log("General data sent successfully:", response); + }) + .catch(error => { + console.error("Error sending general data:", error); + }); +``` + +## requestGeneralData Function + +The `requestGeneralData` function is used to request general data of a specific type from the supernode cloud. It allows customization of the request parameters including the data type, filtering options, and callback function for data storage and handling. + +### Parameters + +- `type` (string): The type of general data to be requested. +- `options` (object, optional): An object containing the following properties: + - `receiverID` (string, optional): The ID of the receiver for the data request. If not provided, the default admin ID is used. + - `senderID` (string, optional): The ID of the sender for the data request. If not provided, it's set to `undefined`. + - `application` (string, optional): The application name associated with the data request. If not provided, the default application name is used. + - `comment` (string, optional): Additional comments or notes for the data request. If not provided, it's set to `undefined`. + - `lowerVectorClock` (string, optional): The lower boundary for vector clock filtering. If not provided, it's set to `undefined`. + - `upperVectorClock` (string, optional): The upper boundary for vector clock filtering. If not provided, it's set to `undefined`. + - `atVectorClock` (string, optional): The specific vector clock at which the data is requested. If not provided, it's set to `undefined`. + - `afterTime` (number, optional): Request data after the specified timestamp. If not provided, it's set to the last stored vector clock for the specified data type. + - `mostRecent` (boolean, optional): If set to `true`, requests the most recent data. If not provided, it's set to `undefined`. + - `callback` (function, optional): A callback function to handle the response data and errors. If provided, the function stores the data and then calls the callback. If not provided, the data is directly resolved. + +### Return Value + +- Returns a Promise that resolves with the response data upon successful retrieval of the requested general data. + +### Example Usage + +- In this example, the requestGeneralData function sends a customizable data request to the supernode cloud. It allows flexibility in specifying request parameters, including vector clocks, timestamps, and callback function for data storage and handling. The function resolves with the response data upon successful retrieval of the requested general data. + +```javascript +const type = "exampleType"; +const options = { + receiverID: "receiverUserID", + senderID: "senderUserID", + application: "MyApp", + comment: "Optional comment for the request.", + lowerVectorClock: "123", + upperVectorClock: "456", + atVectorClock: "789", + afterTime: 1633363200000, + mostRecent: true, + callback: function(data, error) { + if (error) { + console.error("Error occurred:", error); + } else { + console.log("Data received:", data); + } + } +}; + +floCloudAPI.requestGeneralData(type, options) + .then(response => { + console.log("General data request successful:", response); + }) + .catch(error => { + console.error("Error sending general data request:", error); + }); +``` + +## requestObjectData Function + +The `requestObjectData` function is used to request data of a specific object type from the supernode cloud. It allows customization of the request parameters including the object name, filtering options, and callback function for data storage and handling. + +### Parameters + +- `objectName` (string): The name of the object data to be requested. +- `options` (object, optional): An object containing the following properties: + - `lowerVectorClock` (string, optional): The lower boundary for vector clock filtering. If not provided, it's set to the last stored vector clock for the specified object data type plus 1. + - `senderID` (string or array, optional): The sender ID(s) for filtering the data. If not provided, it's set to `null`. If provided, only data from the specified sender(s) will be requested. + - `mostRecent` (boolean, optional): If set to `true`, requests the most recent data. If not provided, it's set to `true`. + - `comment` (string, optional): The comment to be applied to the request. If provided, it's set to `'RESET'`. + - `callback` (function, optional): A callback function to handle the response data and errors. If provided, the function stores the data and then calls the callback. If not provided, the data is directly resolved. + +### Return Value + +- Returns a Promise that resolves with the response data upon successful retrieval of the requested object data. + +### Example Usage + +- In this example, the requestObjectData function sends a customizable data request for a specific object type to the supernode cloud. It allows flexibility in specifying request parameters, including vector clocks, sender IDs, and callback function for data storage and handling. The function resolves with the response data upon successful retrieval of the requested object data. + +```javascript +const objectName = "exampleObject"; +const options = { + lowerVectorClock: "123", + senderID: "senderUserID", + mostRecent: true, + callback: function(data, error) { + if (error) { + console.error("Error occurred:", error); + } else { + console.log("Data received:", data); + } + } +}; + +floCloudAPI.requestObjectData(objectName, options) + .then(response => { + console.log("Object data request successful:", response); + }) + .catch(error => { + console.error("Error sending object data request:", error); + }); +``` + +## closeRequest Function + +The `closeRequest` function is used to close an active request connection to the supernode cloud identified by its `requestID`. + +### Parameters + +- `requestID` (string): The unique identifier of the request connection to be closed. + +### Return Value + +- Returns a Promise that resolves with a success message upon successful closure of the request connection. + +### Example Usage + +- In this example, the closeRequest function closes an active request connection to the supernode cloud based on the provided requestID. Upon successful closure, it resolves with a success message. If the request connection is not found, it rejects with an error message. + +```javascript +const requestID = "exampleRequestID"; + +floCloudAPI.closeRequest(requestID) + .then(response => { + console.log("Request connection closed successfully:", response); + }) + .catch(error => { + console.error("Error closing request connection:", error); + }); +``` + +## resetObjectData Function + +The `resetObjectData` function is used to reset or initialize an object and send it to the supernode cloud. It sends a reset message containing the initial state of the specified object to the cloud for synchronization. + +### Parameters + +- `objectName` (string): The name of the object to be reset and sent to the cloud. +- `options` (object, optional): An object containing the following properties: + - `comment` (string, optional): The comment to be applied to the reset operation. If provided, it's set to `'RESET'`. + +### Return Value + +- Returns a Promise that resolves with the response data upon successful reset and synchronization of the object data. + +### Example Usage + +- In this example, the resetObjectData function resets the specified object to its initial state and sends the reset message to the supernode cloud for synchronization. The function allows customization through the options object, including adding a comment to the reset operation. Upon successful reset and synchronization, it resolves with the response data. + +```javascript +const objectName = "exampleObject"; +const options = { + comment: "Resetting object to initial state." +}; + +floCloudAPI.resetObjectData(objectName, options) + .then(response => { + console.log("Object data reset and synchronized successfully:", response); + }) + .catch(error => { + console.error("Error resetting object data:", error); + }); +``` + +## updateObjectData Function + +The `updateObjectData` function is used to update the differential changes of an object and send them to the supernode cloud. It computes the difference between the last committed state and the current state of the specified object and sends the update message to the cloud for synchronization. + +### Parameters + +- `objectName` (string): The name of the object whose differential changes need to be sent to the cloud. +- `options` (object, optional): An object containing the following properties: + - `comment` (string, optional): The comment to be applied to the update operation. If provided, it's set to `'UPDATE'`. + +### Return Value + +- Returns a Promise that resolves with the response data upon successful update and synchronization of the object's differential changes. + +### Example Usage + +- In this example, the updateObjectData function computes the differential changes of the specified object and sends the update message to the supernode cloud for synchronization. The function allows customization through the options object, including adding a comment to the update operation. Upon successful update and synchronization, it resolves with the response data. + +```javascript +const objectName = "exampleObject"; +const options = { + comment: "Updating object with differential changes." +}; + +floCloudAPI.updateObjectData(objectName, options) + .then(response => { + console.log("Object data updated and synchronized successfully:", response); + }) + .catch(error => { + console.error("Error updating object data:", error); + }); +``` + +## uploadFile Function + +The `uploadFile` function is used to upload a file to the supernode cloud. It accepts a file blob or instance of File/Blob, processes the file content, and sends it to the cloud for storage. + +### Parameters + +- `fileBlob` (File/Blob): The File or Blob instance representing the file to be uploaded. +- `type` (string): The type of data to be associated with the uploaded file. +- `options` (object, optional): An object containing the following properties: + - `encrypt` (boolean or string, optional): If `true`, encrypts the file data using the default encryption key. If a string is provided, it uses the specified encryption key. If not provided, the file data is not encrypted. + +### Return Value + +- Returns a Promise that resolves with an object containing vectorClock, receiverID, type, and application upon successful upload of the file. + +### Example Usage +- In this example, the uploadFile function uploads a file to the supernode cloud. It accepts a file blob or instance of File/Blob, processes the file content, and sends it to the cloud for storage. The function allows customization through the options object, including encryption of the file data. Upon successful upload, it resolves with an object containing vectorClock, receiverID, type, and application. + +```javascript +const fileBlob = new Blob(["File content"], { type: "text/plain" }); +const type = "fileData"; +const options = { + encrypt: true +}; + +floCloudAPI.uploadFile(fileBlob, type, options) + .then(response => { + console.log("File uploaded successfully:", response); + }) + .catch(error => { + console.error("Error uploading file:", error); + }); +``` + +## downloadFile Function + +The `downloadFile` function is used to download a file from the supernode cloud based on its vectorClock. It retrieves the file data, decrypts it if necessary, and reconstructs the file for download. + +### Parameters + +- `vectorClock` (string): The vectorClock of the file to be downloaded. +- `options` (object, optional): An object containing the following properties: + - `type` (string, optional): The type of the data to be downloaded. If not provided, it uses the default type. + - `decrypt` (boolean or string, optional): If `true`, decrypts the file data using the default decryption key. If a string is provided, it uses the specified decryption key. If not provided, and the file data is encrypted, it rejects the download request. + +### Return Value + +- Returns a Promise that resolves with an object containing the downloaded File instance upon successful download. + +### Example Usage + +- In this example, the downloadFile function downloads a file from the supernode cloud based on its vectorClock. It allows customization through the options object, including specifying the data type and providing a decryption key if the file data is encrypted. Upon successful download, it resolves with an object containing the downloaded File instance. + +```javascript +const vectorClock = "exampleVectorClock"; +const options = { + type: "fileData", + decrypt: true +}; + +floCloudAPI.downloadFile(vectorClock, options) + .then(response => { + console.log("File downloaded successfully:", response); + // Use response.file to access the downloaded file instance + }) + .catch(error => { + console.error("Error downloading file:", error); + }); +``` + + + diff --git a/messenger/docs/floCrypto.md b/messenger/docs/floCrypto.md new file mode 100644 index 0000000..9617d94 --- /dev/null +++ b/messenger/docs/floCrypto.md @@ -0,0 +1,685 @@ +# encryptData Function Documentation + +The `encryptData` function takes two parameters: `data` (string) and `receiverPublicKeyHex` (string). It encrypts the given data using AES encryption algorithm with a shared key derived from the sender's private key and receiver's public key. The function returns an object with two properties: `secret` (the encrypted data) and `senderPublicKeyString` (the sender's public key string). + +## Parameters +- **data** (String): The data to be encrypted. +- **receiverPublicKeyHex** (String): The hexadecimal representation of the receiver's public key. + +## Return Value +An object containing the following properties: +- **secret** (String): The encrypted data. +- **senderPublicKeyString** (String): The sender's public key string. + +## Example Usage +```javascript +const data = "Hello, World!"; +const receiverPublicKeyHex = "receiver_public_key_hex_here"; + +const encryptedData = floCrypto.encryptData(data, receiverPublicKeyHex); +console.log("Encrypted Data:", encryptedData.secret); +console.log("Sender's Public Key:", encryptedData.senderPublicKeyString); +``` + +## Dependencies + +`getSenderPublicKeyString`: A function to retrieve the sender's public key string. +`deriveSharedKeySender`: A function to derive a shared key using sender's private key and receiver's public key. +`Crypto.AES.encrypt`: AES encryption function used to encrypt the data. + +## Notes +- Ensure that the necessary dependencies (getSenderPublicKeyString, deriveSharedKeySender, Crypto.AES.encrypt) are available and properly implemented before using this function. +- Note: Replace receiver_public_key_hex_here with the actual hexadecimal representation of the receiver's public key when using the function. + +# decryptData Function Documentation + +The `decryptData` function is used to decrypt encrypted data using the receiver's private key. + +## Parameters +- **data** (Object): An object containing the encrypted data and sender's public key string. + - **secret** (String): The encrypted data. + - **senderPublicKeyString** (String): The sender's public key string. +- **privateKeyHex** (String): The hexadecimal representation of the receiver's private key. + +## Return Value +The decrypted message as a string. + +## Throws +- **Error**: If `privateKeyHex` is not a valid string or if the private key cannot be determined from the provided hexadecimal representation. + +## Example Usage +```javascript +const encryptedData = { + secret: "encrypted_data_here", + senderPublicKeyString: "sender_public_key_string_here" +}; +const privateKeyHex = "receiver_private_key_hex_here"; + +const decryptedMessage = floCrypto.decryptData(encryptedData, privateKeyHex); +console.log("Decrypted Message:", decryptedMessage); +``` + +## Dependencies + +- **wifToDecimal**: A function to convert the receiver's private key from WIF format to decimal format. +- **deriveSharedKeyReceiver**: A function to derive a shared key using sender's public key string and receiver's private key. +- **Crypto.AES.decrypt**: AES decryption function used to decrypt the data. + +## Notes + +- Ensure that the necessary dependencies (`wifToDecimal`, `deriveSharedKeyReceiver`, `Crypto.AES.decrypt`) are available and properly implemented before using this function. +- **Note**: Replace `"encrypted_data_here"` with the actual encrypted data, `"sender_public_key_string_here"` with the sender's public key string, and `"receiver_private_key_hex_here"` with the receiver's private key in hexadecimal format when using the function. + +# signData Function Documentation + +The `signData` function is used to sign data using the sender's private key. + +## Parameters +- **data** (String): The data to be signed. +- **privateKeyHex** (String): The hexadecimal representation of the sender's private key. + +## Return Value +A hexadecimal string representing the signature of the input data. + +## Dependencies +- **Bitcoin.ECKey**: A class for handling elliptic curve cryptography operations. +- **Crypto.SHA256**: SHA-256 hash function used to create the message hash. +- **Bitcoin.ECDSA.sign**: Function for generating the ECDSA signature. +- **Crypto.util.bytesToHex**: Utility function to convert bytes to hexadecimal string. + +## Example Usage +```javascript +const data = "Hello, World!"; +const privateKeyHex = "sender_private_key_hex_here"; + +const signature = floCrypto.signData(data, privateKeyHex); +console.log("Signature:", signature); +``` + +## Notes + +- Ensure that the necessary dependencies (`Bitcoin.ECKey`, `Crypto.SHA256`, `Bitcoin.ECDSA.sign`, `Crypto.util.bytesToHex`) are available and properly implemented before using this function. +- **Note**: Replace `"sender_private_key_hex_here"` with the actual sender's private key in hexadecimal format when using the function. + +# verifySign Function Documentation + +The `verifySign` function is used to verify the signature of data using the sender's public key. + +## Parameters +- **data** (String): The original data that was signed. +- **signatureHex** (String): The hexadecimal representation of the signature to be verified. +- **publicKeyHex** (String): The hexadecimal representation of the sender's public key. + +## Return Value +A boolean value indicating whether the signature is valid (`true`) or not (`false`). + +## Dependencies +- **Crypto.SHA256**: SHA-256 hash function used to create the message hash. +- **Crypto.util.hexToBytes**: Utility function to convert hexadecimal string to bytes. +- **ecparams.getCurve().decodePointHex**: Function to decode the sender's public key from hexadecimal format. +- **Bitcoin.ECDSA.verify**: Function for verifying the ECDSA signature. + +## Example Usage +```javascript +const data = "Hello, World!"; +const signatureHex = "signature_hex_here"; +const publicKeyHex = "sender_public_key_hex_here"; + +const isSignatureValid = floCrypto.verifySign(data, signatureHex, publicKeyHex); +console.log("Is Signature Valid?", isSignatureValid); +``` +## Notes + +- Ensure that the necessary dependencies (`Crypto.SHA256`, `Crypto.util.hexToBytes`, `ecparams.getCurve().decodePointHex`, `Bitcoin.ECDSA.verify`) are available and properly implemented before using this function. +- **Note**: Replace `"signature_hex_here"` with the actual signature in hexadecimal format, and `"sender_public_key_hex_here"` with the sender's public key in hexadecimal format when using the function + +# generateNewID Function Documentation + +The `generateNewID` function is used to generate a new flo ID, along with its corresponding private key and public key. + +## Return Value +An object containing the following properties: +- **floID** (String): The newly generated flo ID. +- **pubKey** (String): The hexadecimal representation of the corresponding public key. +- **privKey** (String): The Wallet Import Format (WIF) representation of the corresponding private key. + +## Dependencies +- **Bitcoin.ECKey**: A class for generating elliptic curve key pairs. + +## Example Usage +```javascript +const newIDInfo = floCrypto.generateNewID(); +console.log("Flo ID:", newIDInfo.floID); +console.log("Public Key:", newIDInfo.pubKey); +console.log("Private Key (WIF):", newIDInfo.privKey); +``` + +## Notes + +- Ensure that the necessary dependency (`Bitcoin.ECKey`) is available and properly implemented before using this function. + +# getPubKeyHex Function Documentation + +The `getPubKeyHex` function is used to obtain the public key from a given private key. + +## Parameters +- **privateKeyHex** (String): The hexadecimal representation of the private key. + +## Return Value +- (String): The hexadecimal representation of the corresponding public key. + +## Dependencies +- **Bitcoin.ECKey**: A class for handling elliptic curve cryptography operations. + +## Example Usage +```javascript +const privateKeyHex = "private_key_hex_here"; +const publicKeyHex = floCrypto.getPubKeyHex(privateKeyHex); +console.log("Public Key:", publicKeyHex); +``` + +## Notes +- Ensure that the necessary dependency (`Bitcoin.ECKey`) is available and properly implemented before using this function. +- If `privateKeyHex` is not provided or invalid, the function will return `null`. + +# getFloID Function Documentation + +The `getFloID` function is used to obtain the flo-ID from a given public key or private key. + +## Parameters +- **keyHex** (String): The hexadecimal representation of the public key or private key. + +## Return Value +- (String): The corresponding flo-ID. + +## Dependencies +- **Bitcoin.ECKey**: A class for handling elliptic curve cryptography operations. + +## Example Usage +```javascript +const keyHex = "public_key_or_private_key_hex_here"; +const floID = floCrypto.getFloID(keyHex); +console.log("Flo-ID:", floID); +``` + +## Notes +- Ensure that the necessary dependency (`Bitcoin.ECKey`) is available and properly implemented before using this function. +- If `keyHex` is not provided or invalid, the function will return `null`. + +# getAddress Function Documentation + +The `getAddress` function is used to obtain the cryptocurrency address (BTC or FLO) corresponding to a given private key. + +## Parameters +- **privateKeyHex** (String): The hexadecimal representation of the private key. +- **strict** (Boolean, optional): A flag to enforce strict address generation. Default is `false`. + +## Return Value +- (String): The cryptocurrency address corresponding to the provided private key. + +## Dependencies +- **Bitcoin.ECKey**: A class for handling elliptic curve cryptography operations. +- **bitjs.Base58.decode**: Function to decode Base58 encoded strings. +- **coinjs.bech32Address**: Function to generate BTC bech32 address. +- **bitjs.pubkey2address**: Function to generate FLO address from public key. + +## Example Usage +```javascript +const privateKeyHex = "private_key_hex_here"; +const address = floCrypto.getAddress(privateKeyHex, true); +console.log("Cryptocurrency Address:", address); +``` + +## Notes +- Ensure that the necessary dependencies (`Bitcoin.ECKey`, `bitjs.Base58.decode`, `coinjs.bech32Address`, `bitjs.pubkey2address`) are available and properly implemented before using this function. +- If `privateKeyHex` is not provided or invalid, the function will return `null`. +- The `strict` flag, if set to `true`, enforces strict address generation based on the provided private key version. If set to `false`, the function will default to generating a FLO address. + +# verifyPrivKey Function Documentation + +The `verifyPrivKey` function is used to verify whether a given private key corresponds to a provided public key or flo-ID. + +## Parameters +- **privateKeyHex** (String): The hexadecimal representation of the private key. +- **pubKey_floID** (String): The public key or flo-ID to be verified. +- **isfloID** (Boolean, optional): A flag indicating whether the provided key is a flo-ID. Default is `true`. + +## Return Value +- (Boolean): `true` if the private key corresponds to the provided public key or flo-ID, `false` otherwise. Returns `null` if an error occurs during verification. + +## Dependencies +- **Bitcoin.ECKey**: A class for handling elliptic curve cryptography operations. + +## Example Usage +```javascript +const privateKeyHex = "private_key_hex_here"; +const pubKey_floID = "public_key_or_floID_here"; +const isfloID = true; + +const isPrivateKeyValid = floCrypto.verifyPrivKey(privateKeyHex, pubKey_floID, isfloID); +console.log("Is Private Key Valid?", isPrivateKeyValid); +``` + +## Notes +- Ensure that the necessary dependency (`Bitcoin.ECKey`) is available and properly implemented before using this function. +- If `privateKeyHex` or `pubKey_floID` is not provided or invalid, the function will return `false`. +- If `isfloID` is set to `true`, the function will verify against a flo-ID. If set to `false`, it will verify against a public key. + +# getMultisigAddress Function Documentation + +The `getMultisigAddress` function is used to generate a multisignature address based on a list of public keys and the number of required signatures. + +## Parameters +- **publicKeyList** (Array of Strings): An array containing hexadecimal representations of public keys. +- **requiredSignatures** (Integer): The number of required signatures for the multisignature address. + +## Return Value +- (String): The generated multisignature address, or `null` if an error occurs during the generation. + +## Dependencies +- **bitjs.pubkeys2multisig**: Function to generate a multisignature address from an array of public keys. + +## Example Usage +```javascript +const publicKeyList = ["pubKey1_hex", "pubKey2_hex", "pubKey3_hex"]; +const requiredSignatures = 2; + +const multisigAddress = floCrypto.getMultisigAddress(publicKeyList, requiredSignatures); +console.log("Multisig Address:", multisigAddress); +``` + +## Notes +- Ensure that the necessary dependency (`bitjs.pubkeys2multisig`) is available and properly implemented before using this function. +- `publicKeyList` should be an array of hexadecimal strings representing the public keys. +- `requiredSignatures` should be an integer indicating the number of required signatures for the multisignature address. +- If `publicKeyList` is empty, not an array, or contains invalid keys, or if `requiredSignatures` is not a valid integer, the function will return `null`. + +# decodeRedeemScript Function Documentation + +The `decodeRedeemScript` function is used to decode a multisignature redeem script. + +## Parameters +- **redeemScript** (String): The hexadecimal representation of the redeem script. + +## Return Value +- (Object): An object containing the decoded information of the multisignature redeem script, or `null` if an error occurs during decoding. + +## Dependencies +- **bitjs.transaction().decodeRedeemScript**: Function to decode a multisignature redeem script. + +## Example Usage +```javascript +const redeemScript = "redeem_script_hex_here"; + +const decodedRedeemScript = floCrypto.decodeRedeemScript(redeemScript); +console.log("Decoded Redeem Script:", decodedRedeemScript); +``` + +## Notes +- Ensure that the necessary dependency (`bitjs.transaction().decodeRedeemScript`) is available and properly implemented before using this function. +- If the `redeemScript` is not a valid hexadecimal string or if an error occurs during decoding, the function will return `null`. + +# validateFloID Function Documentation + +The `validateFloID` function is used to check whether a given flo-ID is valid or not. + +## Parameters +- **floID** (String): The flo-ID to be validated. +- **regularOnly** (Boolean, optional): A flag indicating whether to allow only regular flo-IDs. Default is `false`. + +## Return Value +- (Boolean): `true` if the flo-ID is valid, `false` otherwise. + +## Dependencies +- **Bitcoin.Address**: A class for handling Bitcoin addresses. + +## Example Usage +```javascript +const floID = "flo_ID_here"; +const isValidFloID = floCrypto.validateFloID(floID, true); +console.log("Is Flo-ID Valid?", isValidFloID); +``` + +## Notes +- Ensure that the necessary dependency (`Bitcoin.Address`) is available and properly implemented before using this function. +- If `floID` is not a valid flo-ID (not a valid Bitcoin address), the function will return `false`. +- If `regularOnly` is set to `true`, the function will only consider standard flo-IDs as valid; otherwise, it will consider all valid flo-IDs as valid. + +# validateAddr Function Documentation + +The `validateAddr` function is used to check whether a given address (from any blockchain) is valid or not. + +## Parameters +- **address** (String): The address to be validated. +- **std** (Boolean or Integer or Array, optional): A flag or array indicating whether to allow specific address versions (standard addresses). Default is `true`. +- **bech** (Boolean or Integer or Array, optional): A flag or array indicating whether to allow specific bech32 address versions. Default is `true`. + +## Return Value +- (Boolean): `true` if the address is valid, `false` otherwise. + +## Dependencies +- **decodeAddress**: A function to decode the provided address. + +## Example Usage +```javascript +const address = "address_here"; +const isValidAddress = floCrypto.validateAddr(address, true, true); +console.log("Is Address Valid?", isValidAddress); +``` + +## Notes +- Ensure that the necessary dependency (`decodeAddress`) is available and properly implemented before using this function. +- If `address` is not a valid address, the function will return `false`. +- The `std` parameter allows specifying the allowed standard address versions. If set to `true`, all standard versions are allowed. If set to an integer or an array of integers, only the specified version(s) are allowed. +- The `bech` parameter allows specifying the allowed bech32 address versions. If set to `true`, all bech32 versions are allowed. If set to an integer or an array of integers, only the specified version(s) are allowed. + +# verifyPubKey Function Documentation + +The `verifyPubKey` function is used to verify whether a given public key or redeem script corresponds to the provided address (from any blockchain). + +## Parameters +- **pubKeyHex** (String): The hexadecimal representation of the public key or redeem script. +- **address** (String): The address to be verified. + +## Return Value +- (Boolean): `true` if the public key or redeem script corresponds to the provided address, `false` otherwise. + +## Dependencies +- **decodeAddress**: A function to decode the provided address. +- **Crypto.util.bytesToHex**: Utility function to convert bytes to hexadecimal string. +- **Crypto.SHA256**: SHA-256 hash function. +- **ripemd160**: RIPEMD-160 hash function. + +## Example Usage +```javascript +const pubKeyHex = "public_key_or_redeem_script_hex_here"; +const address = "address_here"; + +const isPubKeyValid = floCrypto.verifyPubKey(pubKeyHex, address); +console.log("Is Public Key Valid?", isPubKeyValid); +``` +## Notes +- Ensure that the necessary dependencies (`decodeAddress`, `Crypto.util.bytesToHex`, `Crypto.SHA256`, `Crypto.util.hexToBytes`, `ripemd160`) are available and properly implemented before using this function. +- If the public key or redeem script does not correspond to the provided address, the function will return `false`. + +# toFloID Function Documentation + +The `toFloID` function is used to convert a given address (from any blockchain) to its equivalent floID. + +## Parameters +- **address** (String): The address to be converted. +- **options** (Object, optional): An object containing options for version checks. Default is `null`. + - **std** (Array of Integers, optional): An array of allowed standard address versions. + - **bech** (Array of Integers, optional): An array of allowed bech32 address versions. + +## Return Value +- (String): The equivalent floID corresponding to the provided address. Returns `undefined` if the address is not valid or does not match the specified options. + +## Dependencies +- **decodeAddress**: A function to decode the provided address. +- **Crypto.SHA256**: SHA-256 hash function. +- **bitjs.pub**: The version byte for FLO addresses. +- **bitjs.Base58.encode**: Base58 encoding function. + +## Example Usage +```javascript +const address = "address_here"; +const options = { + std: [version1, version2], + bech: [version3, version4] +}; + +const floID = floCrypto.toFloID(address, options); +console.log("Equivalent FloID:", floID); +``` + +## Notes +- Ensure that the necessary dependencies (`decodeAddress`, `Crypto.SHA256`, `bitjs.pub`, `bitjs.Base58.encode`) are available and properly implemented before using this function. +- If the `address` is not valid or does not match the specified options, the function will return `undefined`. + +# toMultisigFloID Function Documentation + +The `toMultisigFloID` function is used to convert a given multisig address (from any blockchain) to its equivalent multisig floID. + +## Parameters +- **address** (String): The multisig address to be converted. +- **options** (Object, optional): An object containing options for version checks. Default is `null`. + - **std** (Array of Integers, optional): An array of allowed standard address versions. + - **bech** (Array of Integers, optional): An array of allowed bech32 address versions. + +## Return Value +- (String): The equivalent multisig floID corresponding to the provided multisig address. Returns `undefined` if the address is not valid or does not match the specified options. + +## Dependencies +- **decodeAddress**: A function to decode the provided multisig address. +- **Crypto.SHA256**: SHA-256 hash function. +- **ripemd160**: A cryptographic hash function. +- **bitjs.multisig**: The version byte for multisig FLO addresses. +- **bitjs.Base58.encode**: Base58 encoding function. + +## Example Usage +```javascript +const multisigAddress = "multisig_address_here"; +const options = { + std: [version1, version2], + bech: [version3, version4] +}; + +const multisigFloID = floCrypto.toMultisigFloID(multisigAddress, options); +console.log("Equivalent Multisig FloID:", multisigFloID); +``` + +## Notes +- Ensure that the necessary dependencies (`decodeAddress`, `Crypto.SHA256`, `ripemd160`, `bitjs.multisig`, `bitjs.Base58.encode`) are available and properly implemented before using this function. +- If the `address` is not valid or does not match the specified options, the function will return `undefined`. + +# isSameAddr Function Documentation + +The `isSameAddr` function is used to check whether two given addresses (from any blockchain) correspond to the same keys. + +## Parameters +- **addr1** (String): The first address to be compared. +- **addr2** (String): The second address to be compared. + +## Return Value +- (Boolean): `true` if the addresses correspond to the same keys, `false` otherwise. Returns `undefined` if either `addr1` or `addr2` is not provided or invalid. + +## Dependencies +- **decodeAddress**: A function to decode the provided addresses. +- **Crypto.util.bytesToHex**: Utility function to convert bytes to hexadecimal string. +- **ripemd160**: A cryptographic hash function. + +## Example Usage +```javascript +const addr1 = "address1_here"; +const addr2 = "address2_here"; + +const sameKeys = floCrypto.isSameAddr(addr1, addr2); +console.log("Do Addresses Correspond to Same Keys?", sameKeys); +``` + +## Notes +- Ensure that the necessary dependencies (`decodeAddress`, `Crypto.util.bytesToHex`, `ripemd160`) are available and properly implemented before using this function. +- If either `addr1` or `addr2` is not provided or invalid, the function will return `undefined`. + +# decodeAddr Function Documentation + +The `decodeAddr` function is used to decode the provided address and determine its version, hexadecimal representation, and raw bytes. + +## Parameters +- **address** (String): The address to be decoded. + +## Return Value +- (Object or null): An object containing decoded information, including version, hexadecimal representation, and raw bytes. Returns `null` if the provided address is not valid. + +## Dependencies +- **bitjs.Base58.decode**: Base58 decoding function for legacy addresses. +- **Crypto.SHA256**: SHA-256 hash function. +- **Crypto.util.bytesToHex**: Utility function to convert bytes to hexadecimal string. +- **coinjs.bech32_decode**: Bech32 decoding function for bech32 addresses. +- **coinjs.bech32_convert**: Bech32 conversion function. + +## Example Usage +```javascript +const address = "address_here"; +const decodedInfo = floCrypto.decodeAddr(address); +console.log("Decoded Address Information:", decodedInfo); +``` + +## Notes +- Ensure that the necessary dependencies (`bitjs.Base58.decode`, `Crypto.SHA256`, `Crypto.util.bytesToHex`, `coinjs.bech32_decode`, `coinjs.bech32_convert`) are available and properly implemented before using this function. +- The function will return `null` if the provided address is not valid. + +# createShamirsSecretShares Function Documentation + +The `createShamirsSecretShares` function uses Shamir's Secret Sharing algorithm to split a given string into shares based on the specified total shares and threshold limit. + +## Parameters +- **str** (String): The input string to be split. +- **total_shares** (Integer): The total number of shares to be generated. +- **threshold_limit** (Integer): The minimum number of shares required to reconstruct the original string. + +## Return Value +- (Array or Boolean): An array containing the shares generated using Shamir's Secret Sharing algorithm. Returns `false` if the input string is empty or if there is an error during the splitting process. + +## Dependencies +- **shamirSecretShare.str2hex**: Function to convert a string to hexadecimal representation. +- **shamirSecretShare.share**: Function to generate shares using Shamir's Secret Sharing algorithm. + +## Example Usage +```javascript +const inputString = "input_string_here"; +const totalShares = 5; +const thresholdLimit = 3; + +const shares = floCrypto.createShamirsSecretShares(inputString, totalShares, thresholdLimit); +console.log("Generated Shares:", shares); +``` + +## Notes +- Ensure that the necessary dependencies (`shamirSecretShare.str2hex`, `shamirSecretShare.share`) are available and properly implemented before using this function. +- The function will return `false` if the input string is empty or if there is an error during the splitting process. + +# retrieveShamirSecret Function Documentation + +The `retrieveShamirSecret` function is used to retrieve the original secret by combining the provided Shamir's Secret Sharing shares. + +## Parameters +- **sharesArray** (Array): An array containing Shamir's Secret Sharing shares to be combined. + +## Return Value +- (String or Boolean): The retrieved original secret. Returns `false` if the input shares array is empty or if there is an error during the retrieval process. + +## Dependencies +- **shamirSecretShare.combine**: Function to combine shares using Shamir's Secret Sharing algorithm. +- **shamirSecretShare.hex2str**: Function to convert hexadecimal representation to a string. + +## Example Usage +```javascript +const sharesArray = ["share1", "share2", "share3"]; +const retrievedSecret = floCrypto.retrieveShamirSecret(sharesArray); +console.log("Retrieved Secret:", retrievedSecret); +``` +## Notes +- Ensure that the necessary dependencies (`shamirSecretShare.combine`, `shamirSecretShare.hex2str`) are available and properly implemented before using this function. +- The function will return `false` if the input shares array is empty or if there is an error during the retrieval process. + +# verifyShamirsSecret Function Documentation + +The `verifyShamirsSecret` function is used to verify the provided Shamir's Secret Sharing shares against a given string. + +## Parameters +- **sharesArray** (Array): An array containing Shamir's Secret Sharing shares to be verified. +- **str** (String): The original string to be verified against the retrieved secret. + +## Return Value +- (Boolean or null): `true` if the shares verify the provided string, `false` otherwise. Returns `null` if the input string is not provided. + +## Dependencies +- **retrieveShamirSecret**: Function to retrieve the original secret by combining shares using Shamir's Secret Sharing algorithm. + +## Example Usage +```javascript +const sharesArray = ["share1", "share2", "share3"]; +const originalString = "original_string_here"; + +const verificationResult = floCrypto.verifyShamirsSecret(sharesArray, originalString); +console.log("Shares Verification Result:", verificationResult); +``` +## Notes +- Ensure that the necessary dependency (`retrieveShamirSecret`) is available and properly implemented before using this function. +- The function will return `null` if the input string is not provided. + +# validateASCII Function Documentation + +The `validateASCII` function is used to validate a string to ensure it contains only ASCII characters within the printable range (32 to 127) by default. + +## Parameters +- **string** (String): The input string to be validated. +- **bool** (Boolean, optional): If `true`, the function returns `true` if the entire string contains only printable ASCII characters; if `false`, it returns an object containing information about invalid characters and their positions. Default is `true`. + +## Return Value +- (Boolean or Object or null): Returns `true` if the input string contains only printable ASCII characters. Returns an object containing information about invalid characters and their positions if `bool` is set to `false`. Returns `null` if the input is not a string. + +## Example Usage +```javascript +const inputString = "ASCII_string_here"; + +// Validate ASCII characters +const isValid = floCrypto.validateASCII(inputString); +console.log("Is Valid ASCII?", isValid); + +// Validate ASCII characters and get invalid characters with their positions +const invalidChars = floCrypto.validateASCII(inputString, false); +console.log("Invalid Characters:", invalidChars); +``` + +## Notes +- If `bool` is set to `true`, the function will return `true` if the input string contains only printable ASCII characters; otherwise, it will return `false`. +- If `bool` is set to `false`, the function will return an object containing information about invalid characters and their positions. If there are no invalid characters, it will return an empty object. +- The function will return `null` if the input is not a string. + +# convertToASCII Function Documentation + +The `convertToASCII` function is used to convert a string to ASCII characters using specified conversion modes. + +## Parameters +- **string** (String): The input string to be converted. +- **mode** (String, optional): The conversion mode. Available options are 'hard-unicode', 'soft-unicode', 'hard-remove', and 'soft-remove'. Default is 'soft-remove'. + +## Return Value +- (String or null): The converted string. Returns `null` if the input is not a string. + +## Example Usage +```javascript +const inputString = "input_string_here"; + +// Convert to ASCII characters using soft-remove mode +const convertedString = floCrypto.convertToASCII(inputString); +console.log("Converted String:", convertedString); +``` +## Notes +- Available conversion modes: + - 'hard-unicode': Converts characters to Unicode escape sequences (e.g., `\uXXXX`). + - 'soft-unicode': Converts characters to Unicode escape sequences if available in `ascii_alternatives` list; otherwise, uses regular Unicode escape sequences. + - 'hard-remove': Removes non-ASCII characters from the string. + - 'soft-remove': Replaces non-ASCII characters with their alternatives from `ascii_alternatives` list if available; otherwise, removes them. +- Ensure that the necessary dependencies (`validateASCII`) and the `ascii_alternatives` list are available and properly implemented before using this function. +- The function will return `null` if the input is not a string. + +# revertUnicode Function Documentation + +The `revertUnicode` function is used to revert Unicode escape sequences (e.g., `\uXXXX`) back to their corresponding characters in a given string. + +## Parameters +- **string** (String): The input string containing Unicode escape sequences to be reverted. + +## Return Value +- (String): The string with reverted Unicode escape sequences. + +## Example Usage +```javascript +const unicodeString = "\\u0048\\u0065\\u006c\\u006c\\u006f"; +const revertedString = floCrypto.revertUnicode(unicodeString); +console.log("Reverted String:", revertedString); +``` diff --git a/messenger/docs/floDapps.md b/messenger/docs/floDapps.md new file mode 100644 index 0000000..b1445de --- /dev/null +++ b/messenger/docs/floDapps.md @@ -0,0 +1,344 @@ +## readSupernodeListFromAPI Function + +The `readSupernodeListFromAPI` function is a startup function used to fetch and update the list of supernodes from the Flo blockchain API. It reads supernode data from the blockchain, processes the data, and updates the list of supernodes stored in the local database. + +### Function Logic + +1. Checks if cloud functionality is enabled for the application. If not, it resolves with "No cloud for this app". +2. Reads the last transaction count or transaction ID from the local database for the cloud storage. +3. Fetches supernode data from the Flo blockchain using the specified query options. +4. Compares the fetched data with the local list of supernodes, updating or removing nodes as necessary. +5. Writes the updated supernode list and the last transaction information to the local database. +6. Initializes the `floCloudAPI` with the updated supernode list. + +### Return Value + +- Returns a Promise that resolves with a success message upon successful loading and initialization of the supernode list. + +### Example Usage + +- This function is typically called during the startup process of the application to ensure that the local list of supernodes is up-to-date. + +- In this example, the readSupernodeListFromAPI function updates the local list of supernodes based on the data fetched from the Flo blockchain. It resolves with a success message after successfully loading and initializing the supernode list. + +```javascript +startUpFunctions.push(function readSupernodeListFromAPI() { + // Function logic as described above + // ... + return new Promise((resolve, reject) => { + // Resolving or rejecting based on success or error + // ... + }); +}); +``` + +## readAppConfigFromAPI Function + +The `readAppConfigFromAPI` function is a startup function used to fetch and update the application configuration from the Flo blockchain API. It reads application configuration data from the blockchain, processes the data, and updates the local database with sub-admins, trusted IDs, and settings. + +### Function Logic + +1. Checks if application configuration functionality is enabled for the application. If not, it resolves with "No configs for this app". +2. Reads the last transaction count or transaction ID for the specified application and admin ID from the local database. +3. Fetches application configuration data from the Flo blockchain using the specified query options. +4. Processes the fetched data, updating sub-admins, trusted IDs, and settings in the local database. +5. Resolves with a success message after successfully reading the app configuration from the blockchain. + +### Return Value + +- Returns a Promise that resolves with a success message upon successful reading and updating of the application configuration. + +### Example Usage + +- This function is typically called during the startup process of the application to ensure that the local application configuration is synchronized with the data on the blockchain. + +- In this example, the readAppConfigFromAPI function updates the local application configuration based on the data fetched from the Flo blockchain. It resolves with a success message after successfully reading and updating the application configuration. + +```javascript +startUpFunctions.push(function readAppConfigFromAPI() { + // Function logic as described above + // ... + return new Promise((resolve, reject) => { + // Resolving or rejecting based on success or error + // ... + }); +}); +``` + +## loadDataFromAppIDB Function + +The `loadDataFromAppIDB` function is a startup function used to load data from the IndexedDB (IDB) storage of the application. It reads specific data sets (`appObjects`, `generalData`, and `lastVC`) from the local IndexedDB and stores them in the respective global variables of the `floGlobals` object. + +### Function Logic + +1. Checks if cloud functionality is enabled for the application. If not, it resolves with "No cloud for this app". +2. Defines an array `loadData` containing the names of the data sets to be loaded from IndexedDB (`appObjects`, `generalData`, `lastVC`). +3. Iterates through the `loadData` array and creates an array of promises to read all data from each data set in IndexedDB. +4. Uses `Promise.all` to wait for all data reading operations to complete. +5. Assigns the retrieved data to the respective properties of the `floGlobals` object. +6. Resolves with a success message after successfully loading data from IndexedDB. + +### Return Value + +- Returns a Promise that resolves with a success message upon successful loading of data from IndexedDB. + +### Example Usage + +- This function is typically called during the startup process of the application to load essential data sets from the local IndexedDB storage. +- In this example, the loadDataFromAppIDB function loads specific data sets from the IndexedDB storage and assigns them to global variables within the floGlobals object. It resolves with a success message after successfully loading the data. + +```javascript +startUpFunctions.push(function loadDataFromAppIDB() { + // Function logic as described above + // ... + return new Promise((resolve, reject) => { + // Resolving or rejecting based on success or error + // ... + }); +}); +``` + +## callStartUpFunction Function + +The `callStartUpFunction` function is a utility function used for executing startup functions sequentially. It takes an index `i` as an argument, which represents the index of the startup function to be executed. This function returns a Promise that resolves when the specified startup function is successfully executed or rejects if there is an error during execution. + +### Function Parameters + +- **i**: Index of the startup function to be executed. + +### Function Logic + +1. Takes an index `i` as an argument representing the index of the startup function to be executed. +2. Executes the startup function at the specified index. +3. If the startup function execution is successful, it increments the `callStartUpFunction.completed` counter and logs the result as a successful startup function execution. +4. If the startup function encounters an error, it increments the `callStartUpFunction.failed` counter and logs the error message as a failed startup function execution. +5. Returns a Promise that resolves with `true` when the startup function is successfully executed and rejects with `false` if there is an error during execution. + +### Return Value + +- Returns a Promise that resolves with `true` if the startup function is executed successfully and rejects with `false` if there is an error during execution. + +### Example Usage + +- The `callStartUpFunction` function is typically used in a loop to sequentially execute multiple startup functions. +- In this example, the callStartUpFunction function is used to execute startup functions sequentially and handle success and error cases for each function execution. + +```javascript +const callStartUpFunction = i => new Promise((resolve, reject) => { + startUpFunctions[i]().then(result => { + callStartUpFunction.completed += 1; + startUpLog(true, `${result}\nCompleted ${callStartUpFunction.completed}/${callStartUpFunction.total} Startup functions`) + resolve(true) + }).catch(error => { + callStartUpFunction.failed += 1; + startUpLog(false, `${error}\nFailed ${callStartUpFunction.failed}/${callStartUpFunction.total} Startup functions`) + reject(false) + }) +}); +``` + +## midStartUp Function + +The `midStartUp` function is a utility function used for executing a middle-stage startup function. It checks if the `_midFunction` variable contains a function reference. If `_midFunction` is a function, it executes `_midFunction()`, and the function resolves with a success message. If `_midFunction` is not a function, the function resolves with a message indicating that there is no middle-stage startup function. + +### Function Logic + +1. Checks if `_midFunction` is a function. +2. If `_midFunction` is a function, it executes `_midFunction()` and resolves with a success message. +3. If `_midFunction` is not a function, it resolves with a message indicating that there is no middle-stage startup function. + +### Return Value + +- Returns a Promise that resolves with a success message if the middle-stage startup function is executed successfully. +- Returns a Promise that resolves with a message indicating that there is no middle-stage startup function if `_midFunction` is not a function. + +### Example Usage + +- The `midStartUp` function is typically used to execute a specific function during the middle stage of the application startup process. + +```javascript +const midStartUp = () => new Promise((res, rej) => { + if (_midFunction instanceof Function) { + _midFunction() + .then(r => res("Mid startup function completed")) + .catch(e => rej("Mid startup function failed")) + } else + res("No mid startup function") +}); +``` + +## floDapps.launchStartUp Function + +The `floDapps.launchStartUp` function is a central startup function responsible for initializing and launching various components of the application. It ensures the setup of the IndexedDB, executes a series of startup functions, and handles the initialization of user-related databases and credentials. + +### Function Logic + +1. **IndexedDB Initialization:** The function initializes the IndexedDB for the application. + +2. **Startup Functions Execution:** Executes a list of startup functions (`startUpFunctions`) using the `callStartUpFunction` function. It logs the progress of the startup functions, indicating completion or failure. + +3. **Middle-Stage Startup:** Executes the `midStartUp` function, if available. This function typically handles middle-stage startup tasks. + +4. **User Database and Credentials:** Calls `getCredentials` to retrieve user credentials, initializes the user database using `initUserDB`, and loads user data using `loadUserDB`. + +5. **Promise Handling:** The function uses promises to manage the asynchronous tasks and resolves with a success message if all tasks are completed successfully. If any task fails, it rejects with an error message. + +### Return Value + +- Returns a Promise that resolves with a success message when the entire startup process is finished successfully. +- Returns a Promise that rejects with an error message if any part of the startup process fails. + +### Example Usage + +```javascript +floDapps.launchStartUp() + .then(result => { + console.log(result); // Output: 'App Startup finished successful' + // Further application logic after successful startup + }) + .catch(error => { + console.error(error); // Output: 'App Startup failed' + // Handle error and provide user feedback + }); +``` + +## floDapps.manageAppConfig Function + +The `floDapps.manageAppConfig` function allows the administrator to manage the application configuration. It enables adding or removing sub-administrators and updating application-specific settings. + +### Function Parameters + +- **adminPrivKey**: Private key of the administrator for authentication. +- **addList**: An array of FloIDs to be added as sub-administrators. (Optional) +- **rmList**: An array of FloIDs to be removed from sub-administrators. (Optional) +- **settings**: An object containing application-specific settings. (Optional) + +### Function Logic + +1. **Parameter Validation:** Validates the input parameters. If no changes are provided (addList, rmList, or settings), the function rejects with a message indicating no configuration change. + +2. **FloData Preparation:** Prepares the `floData` object containing the application configuration changes, including added sub-administrators, removed sub-administrators, and updated settings. + +3. **Admin Privilege Check:** Checks if the provided `adminPrivKey` matches the default admin ID (`DEFAULT.adminID`). If not, it rejects with an "Access Denied" error. + +4. **Blockchain Data Writing:** Uses `floBlockchainAPI.writeData` to write the updated `floData` to the Flo blockchain. Resolves with a success message and the transaction result if successful. Rejects with an error message if the write operation fails. + +### Return Value + +- Returns a Promise that resolves with an array containing the success message ("Updated App Configuration") and the transaction result from the blockchain write operation. +- Returns a Promise that rejects with an error message if any of the following conditions are met: + - No configuration change is provided. + - The provided `adminPrivKey` does not match the default admin ID (`DEFAULT.adminID`). + +### Example Usage + +```javascript +const adminPrivKey = 'your_admin_private_key'; +const addList = ['sub_admin_floID_1', 'sub_admin_floID_2']; +const rmList = ['sub_admin_floID_3']; +const settings = { + key: 'value', + // additional application-specific settings +}; + +floDapps.manageAppConfig(adminPrivKey, addList, rmList, settings) + .then(result => { + console.log(result[0]); // Output: 'Updated App Configuration' + console.log(result[1]); // Output: Transaction result from blockchain write operation + // Further application logic after successful configuration update + }) + .catch(error => { + console.error(error); // Output: Error message if configuration update fails + // Handle error and provide user feedback + }); +``` + +## floDapps.manageAppTrustedIDs Function + +The `floDapps.manageAppTrustedIDs` function allows the administrator to manage trusted IDs for the application. It enables adding or removing trusted IDs. + +### Function Parameters + +- **adminPrivKey**: Private key of the administrator for authentication. +- **addList**: An array of FloIDs to be added as trusted IDs. (Optional) +- **rmList**: An array of FloIDs to be removed from trusted IDs. (Optional) + +### Function Logic + +1. **Parameter Validation:** Validates the input parameters. If no changes are provided (addList or rmList), the function rejects with a message indicating no change in the list. + +2. **FloData Preparation:** Prepares the `floData` object containing the trusted ID changes, including added trusted IDs and removed trusted IDs. + +3. **Admin Privilege Check:** Checks if the provided `adminPrivKey` matches the default admin ID (`DEFAULT.adminID`). If not, it rejects with an "Access Denied" error. + +4. **Blockchain Data Writing:** Uses `floBlockchainAPI.writeData` to write the updated `floData` to the Flo blockchain. Resolves with a success message and the transaction result if successful. Rejects with an error message if the write operation fails. + +### Return Value + +- Returns a Promise that resolves with an array containing the success message ("Updated App Configuration") and the transaction result from the blockchain write operation. +- Returns a Promise that rejects with an error message if no change is provided in the list or if the provided `adminPrivKey` does not match the default admin ID (`DEFAULT.adminID`). + +### Example Usage + +```javascript +const adminPrivKey = 'your_admin_private_key'; +const addList = ['trusted_floID_1', 'trusted_floID_2']; +const rmList = ['trusted_floID_3']; + +floDapps.manageAppTrustedIDs(adminPrivKey, addList, rmList) + .then(result => { + console.log(result[0]); // Output: 'Updated App Configuration' + console.log(result[1]); // Output: Transaction result from blockchain write operation + // Further application logic after successful trusted IDs update + }) + .catch(error => { + console.error(error); // Output: Error message if trusted IDs update fails + // Handle error and provide user feedback + }); +``` + +## floDapps.getNextGeneralData Function + +The `floDapps.getNextGeneralData` function allows retrieving the next set of general data based on the specified type and vector clock. It filters the data based on the provided type, vector clock, and optional comment. + +### Function Parameters + +- **type**: The type of general data to retrieve. +- **vectorClock**: The vector clock to start filtering the data from. If not provided, it defaults to the stored vector clock or '0'. +- **options**: An optional object containing additional filtering options. + - **comment**: Optional. If provided, filters the data based on the specified comment. + - **decrypt**: Optional. Decryption key for decrypting encrypted data. If `true`, it uses the user's private key for decryption. + +### Function Logic + +1. **Filtering by Type and Vector Clock:** Retrieves general data of the specified type and greater vector clocks than the provided vector clock. + +2. **Filtering by Comment (Optional):** If the `comment` option is provided, further filters the data based on the specified comment. + +3. **Data Decryption (Optional):** If the `decrypt` option is provided, decrypts the data using the specified decryption key. If `true`, it uses the user's private key for decryption. + +4. **Updating Stored Vector Clock:** Updates the stored vector clock for the specified type with the latest vector clock from the retrieved data. + +5. **Return Filtered Data:** Returns the filtered general data as an object. + +### Return Value + +- Returns an object containing the filtered general data based on the specified type, vector clock, comment, and decryption key. + +### Example Usage + +- In this example, floDapps.getNextGeneralData is used to retrieve and filter the next set of general data for the specified type, vector clock, comment, and decryption key. + +```javascript +const type = 'exampleType'; +const vectorClock = '12345'; +const options = { + comment: 'exampleComment', + decrypt: true +}; + +const filteredData = floDapps.getNextGeneralData(type, vectorClock, options); +console.log(filteredData); +// Output: Filtered general data based on the specified type, vector clock, comment, and decryption key +``` + diff --git a/messenger/docs/floTokenAPI.md b/messenger/docs/floTokenAPI.md new file mode 100644 index 0000000..379ace8 --- /dev/null +++ b/messenger/docs/floTokenAPI.md @@ -0,0 +1,307 @@ +## `fetch_api(apicall)` + +This utility function performs a fetch request to the specified API endpoint using the provided `apicall`. It returns a promise that resolves with the parsed JSON response if the request is successful, and rejects with the response object or an error if the request fails. + +### Parameters: + +- **`apicall`** (string): The API endpoint to be called. + +### Return Value: + +A Promise that resolves with the parsed JSON response if the request is successful, and rejects with the response object or an error if the request fails. + +### Example Usage: + +```javascript +const apiEndpoint = "exampleEndpoint"; +fetch_api(apiEndpoint) + .then(data => { + console.log("API Response:", data); + // Handle the API response data here + }) + .catch(error => { + console.error("API Error:", error); + // Handle API errors here + }); +``` + +## `getBalance(floID, token = DEFAULT.currency)` + +This function retrieves the balance of the specified FLO address (`floID`) for the given `token`. It returns a promise that resolves with the balance value if the request is successful, and rejects with an error if the request fails. + +### Parameters: + +- **`floID`** (string): The FLO address for which the balance needs to be retrieved. +- **`token`** (string, optional): The token for which the balance is retrieved. Defaults to `DEFAULT.currency`. + +### Return Value: + +A Promise that resolves with the balance value if the request is successful, and rejects with an error if the request fails. + +### Example Usage: + +```javascript +const floID = "exampleFLOAddress"; +const token = "exampleToken"; + +getBalance(floID, token) + .then(balance => { + console.log("Balance:", balance); + // Handle the balance data here + }) + .catch(error => { + console.error("Balance Retrieval Error:", error); + // Handle balance retrieval errors here + }); +``` + +## `getTx(txID)` + +This function retrieves transaction details for the specified transaction ID (`txID`). It returns a promise that resolves with the transaction details if the request is successful and rejects with an error if the request fails. + +### Parameters: + +- **`txID`** (string): The unique identifier of the transaction for which details are retrieved. + +### Return Value: + +A Promise that resolves with the transaction details if the request is successful, and rejects with an error if the request fails. + +### Example Usage: + +```javascript +const txID = "exampleTxID"; + +getTx(txID) + .then(transactionDetails => { + console.log("Transaction Details:", transactionDetails); + // Handle the transaction details here + }) + .catch(error => { + console.error("Transaction Retrieval Error:", error); + // Handle transaction retrieval errors here + }); +``` + + + + +## `tokenAPI.sendToken(privKey, amount, receiverID, message = "", token = DEFAULT.currency, options = {})` + +This function allows sending a specified amount of tokens from the sender to a receiver. It takes the following parameters: + +- **`privKey`** (string): Private key of the sender. +- **`amount`** (number): Amount of tokens to be sent. +- **`receiverID`** (string): FLO ID of the receiver. +- **`message`** (string, optional): Additional message to attach to the transaction (default is an empty string). +- **`token`** (string, optional): Token type to be sent (default is `DEFAULT.currency`). +- **`options`** (object, optional): Additional options for the transaction (default is an empty object). + +### Parameters: + +- **`privKey`** (string): Private key of the sender. +- **`amount`** (number): Amount of tokens to be sent. +- **`receiverID`** (string): FLO ID of the receiver. +- **`message`** (string, optional): Additional message to attach to the transaction (default is an empty string). +- **`token`** (string, optional): Token type to be sent (default is `DEFAULT.currency`). +- **`options`** (object, optional): Additional options for the transaction (default is an empty object). + +### Return Value: + +A Promise that resolves to the transaction ID (txid) if the transaction is successful. If there are any errors, the Promise is rejected with an error message. + +### Example Usage: + +```javascript +const privKey = "senderPrivateKey"; +const amount = 100; +const receiverID = "receiverFLOID"; +const message = "Payment for services"; +const token = "exampleToken"; +const options = { fee: 0.1, someOption: "value" }; + +tokenAPI.sendToken(privKey, amount, receiverID, message, token, options) + .then(txid => { + console.log("Transaction ID:", txid); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + +## `sendTokens_raw(privKey, receiverID, token, amount, utxo, vout, scriptPubKey)` + +This function is a low-level utility used to send tokens from a specific Unspent Transaction Output (UTXO) to a receiver. It takes the following parameters: + +- **`privKey`** (string): Private key of the sender. +- **`receiverID`** (string): FLO ID of the receiver. +- **`token`** (string): Token type to be sent. +- **`amount`** (number): Amount of tokens to be sent. +- **`utxo`** (string): UTXO (Unspent Transaction Output) hash. +- **`vout`** (number): Vout (index) of the UTXO in the transaction. +- **`scriptPubKey`** (string): Script Public Key associated with the UTXO. + +### Parameters: + +- **`privKey`** (string): Private key of the sender. +- **`receiverID`** (string): FLO ID of the receiver. +- **`token`** (string): Token type to be sent. +- **`amount`** (number): Amount of tokens to be sent. +- **`utxo`** (string): UTXO (Unspent Transaction Output) hash. +- **`vout`** (number): Vout (index) of the UTXO in the transaction. +- **`scriptPubKey`** (string): Script Public Key associated with the UTXO. + +### Return Value: + +A Promise that resolves to an array containing the receiver's FLO ID and the transaction ID (txid) if the transaction is successful. If there are any errors, the Promise is rejected with an array containing the receiver's FLO ID and the error message. + +### Example Usage: + +```javascript +const privKey = "senderPrivateKey"; +const receiverID = "receiverFLOID"; +const token = "exampleToken"; +const amount = 100; +const utxo = "utxoHash"; +const vout = 0; +const scriptPubKey = "scriptPublicKey"; + +sendTokens_raw(privKey, receiverID, token, amount, utxo, vout, scriptPubKey) + .then(([receiver, txid]) => { + console.log(`Tokens sent to ${receiver}. Transaction ID: ${txid}`); + }) + .catch(([receiver, error]) => { + console.error(`Failed to send tokens to ${receiver}. Error: ${error}`); + }); +``` + +## `tokenAPI.bulkTransferTokens(sender, privKey, token, receivers)` + +This function facilitates bulk token transfers from a sender to multiple receivers. It takes the following parameters: + +- **`sender`** (string): FLO ID of the sender. +- **`privKey`** (string): Private key corresponding to the sender's FLO ID. +- **`token`** (string): Token type to be transferred. +- **`receivers`** (object): An object containing receiver addresses as keys and the corresponding token amounts as values. + +### Parameters: + +- **`sender`** (string): FLO ID of the sender. +- **`privKey`** (string): Private key corresponding to the sender's FLO ID. +- **`token`** (string): Token type to be transferred. +- **`receivers`** (object): An object representing receivers and their corresponding token amounts. The format of the object should be `{ receiver1: amount1, receiver2: amount2, ... }`. + +### Return Value: + +A Promise that resolves to an object containing successful transactions and any failed transactions. The resolved object has the following format: + +```javascript +{ + success: { + receiver1: txid1, + receiver2: txid2, + // ... other successful transactions + }, + failed: { + receiverX: "error message", + // ... other failed transactions + } +} +// If the function encounters any invalid inputs or errors during the process, it rejects the Promise with an error message. +``` + +### Example Usage +```javascript +const sender = "senderFLOID"; +const privKey = "senderPrivateKey"; +const token = "exampleToken"; +const receivers = { + "receiver1FLOID": 100, + "receiver2FLOID": 200, + // ... other receivers and amounts +}; + +tokenAPI.bulkTransferTokens(sender, privKey, token, receivers) + .then(result => { + console.log("Successful Transactions:", result.success); + console.log("Failed Transactions:", result.failed); + }) + .catch(error => { + console.error("Error:", error); + }); +``` + + +## `tokenAPI.getAllTxs(floID, token = DEFAULT.currency)` + +This function retrieves all transactions associated with a specific FLO ID and token. It takes the following parameters: + +- **`floID`** (string): FLO ID for which transactions need to be retrieved. +- **`token`** (string, optional): Token type for which transactions need to be retrieved. Defaults to the DEFAULT currency. + +### Parameters: + +- **`floID`** (string): FLO ID for which transactions need to be retrieved. +- **`token`** (string, optional): Token type for which transactions need to be retrieved. Defaults to the DEFAULT currency. + +### Return Value: + +A Promise that resolves to the result of the API call, containing the transactions associated with the specified FLO ID and token. + +### Example Usage: + +```javascript +const floID = "exampleFLOID"; +const token = "exampleToken"; + +tokenAPI.getAllTxs(floID, token) + .then(transactions => { + console.log("All transactions for FLO ID:", floID); + console.log(transactions); + }) + .catch(error => { + console.error("Error fetching transactions:", error); + }); +``` + +## `util.parseTxData(txData)` + +This utility function parses transaction data and extracts relevant information such as sender, receiver, parsed FLO data, and timestamp. + +### Parameters: + +- **`txData`** (object): Transaction data object to be parsed. + +### Return Value: + +An object containing the parsed transaction data with the following properties: + +- **`sender`** (string): Address of the sender. +- **`receiver`** (string): Address of the receiver. +- **`parsedFloData`** (object): Parsed FLO data extracted from the transaction. +- **`time`** (number): Timestamp of the transaction in Unix epoch format. + +### Example Usage: + +- In this example, the util.parseTxData function is used to parse the provided txData object. It extracts sender, receiver, parsed FLO data, and the transaction timestamp. The function returns an object containing the parsed data for further processing or display. + +```javascript +const txData = { + parsedFloData: { + key1: "value1", + key2: "value2" + // ...other parsed FLO data fields + }, + transactionDetails: { + vin: [{ addr: "senderAddress" }], + vout: [{ scriptPubKey: { addresses: ["receiverAddress"] } }], + time: 1633442400 // Unix timestamp + // ...other transaction details + } +}; + +const parsedData = util.parseTxData(txData); +console.log(parsedData); +``` + + diff --git a/messenger/docs/functions.md b/messenger/docs/functions.md new file mode 100644 index 0000000..918a70f --- /dev/null +++ b/messenger/docs/functions.md @@ -0,0 +1,1261 @@ +### This is a desciption of what we think are the functions that need some explaining in messenger.js and index.html It does not include explanations for Standard Operations functions for which you need to refer documentation of Standard Operations. It also does not include functions in messenger.js and index.html whose code is self evident. + +## `CheckDB()` + +### Description: +This function initializes a database connection based on the configuration provided in `config.json`. It then retrieves data structure information from `data_structure.json` and calculates CRC checksums for each column specified in the data structure. The function then queries each database table specified in `Database.DB.listTable()` and calculates the total number of records and CRC checksums for each table. The results are logged using `console.table`. Any errors during the process are logged using `console.error`. + +### Parameters: +None + +### Return Type: +- **Promise\**: A Promise that resolves to `true` if the database check is successful, and rejects with an error if there is any issue during the process. + +### Usage: +```javascript +CheckDB() + .then(_ => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); +``` + +## `sendRaw(message, recipient, type, encrypt)` + +### Description: +This function is at the heart of messenger application. This function sends raw application data messages to a recipient using Supernode Cloud. It allows for optional encryption of the message using the recipient's public key, if available. + +### Parameters: +- **message (string)**: The raw message data to be sent. +- **recipient (string)**: The FLO address of the message recipient. +- **type (string)**: The type of the application data message being sent. +- **encrypt (boolean|null, optional)**: If `true`, the message will be encrypted using the recipient's public key. If `null` or `false`, the message will be sent unencrypted. Defaults to `null`. +- **comment (string, optional)**: An optional comment or additional information to be included with the message. Defaults to `undefined`. + +### Return Type: +- **Promise\**: A Promise that resolves to the result object from the `floCloudAPI.sendApplicationData()` method upon successful message transmission. Rejects with an error message if there is an issue during the process. + +### Usage: +```javascript +sendRaw("Hello, recipient!", "recipient_address", "message_type", true, "Optional comment") + .then(result => console.log("Message sent successfully:", result)) + .catch(error => console.error("Error sending message:", error)); + +``` +## `initUserDB()` + +### Description: +This function initializes the user database for a messenger application. It creates an initial object structure with empty data fields for messages, mails, marked items, chats, groups, encryption keys, blocked contacts, pipeline, friend requests sent and received, responses sent and received, additional data (`flodata`), appendix, user settings, and multisig labels. The function requires specific data, including `floGlobals.application`, `floCrypto`, and `user.id`, to construct the `user_db` variable, which is used as the unique identifier for the user's database. It then initializes the IndexedDB database using the `compactIDB.initDB()` method and sets the default database. Upon successful initialization, it resolves with a success message. + +### Prerequisites: +- **floGlobals.application**: The application-specific identifier required for database construction must be available. +- **user.id**: The unique identifier for the user must be available, used in constructing the `user_db` variable. + +### Return Type: +- **Promise\**: A Promise that resolves with a success message when the user database initialization is completed successfully. Rejects with an error message if there is any issue during the process. + +### Usage: +```javascript +initUserDB() + .then(message => console.log(message)) + .catch(error => console.error(error)); + +``` + +## `messenger.sendMessage(message, receiver)` + +Sends a message to the specified recipient via the `sendRaw` function, encrypts the message, and records the message data in the cloud. + +### Parameters +- `message` (String): The message to be sent. +- `receiver` (String): The recipient's unique identifier (floID). + +### Returns +A Promise that resolves to an object containing the message data and its vector clock. + +### Example Usage + +```javascript +messenger.sendMessage("Hello, World!", "recipientFloID") + .then(response => { + console.log("Message sent successfully:", response); + }) + .catch(error => { + console.error("Error sending message:", error); + }); + +``` + +## `listRequests(obs, options = null)` + +Retrieves a list of requests from the specified `obs` (objects) based on the provided filtering options. + +### Parameters +- `obs` (Object): The objects from which to retrieve requests. +- `options` (Object, optional): An object containing filtering options. + - `type` (String): Filters requests by type. + - `floID` (String): Filters requests by floID. + - `completed` (Boolean): Filters requests based on their completion status. + +### Returns +A Promise that resolves to an object containing filtered requests based on the specified options. + +### Example Usage + +```javascript +listRequests(myObjects, { type: "sampleType", completed: true }) + .then(filteredRequests => { + console.log("Filtered requests:", filteredRequests); + }) + .catch(error => { + console.error("Error filtering requests:", error); + }); + + messenger.list_request_sent = (options = null) => listRequests('request_sent', options); + messenger.list_request_received = (options = null) => listRequests('request_received', options); + messenger.list_response_sent = (options = null) => listRequests('response_sent', options); + messenger.list_response_received = (options = null) => listRequests('response_received', options); + + +``` + +## `processData.direct()` + +Processes incoming data for individual user mails and messages and updates the local database accordingly. + +### Description +This function processes incoming data, decrypts messages if necessary, and updates the local database (`compactIDB`). It handles different types of incoming messages and actions, including regular messages, requests, responses, mails, group creation, key revocation, and pipeline creation. + +### Parameters +- `unparsed` (Object): The incoming data to be processed. +- `newInbox` (Object): An object representing the updated inbox with categorized messages, requests, responses, mails, and more. + +### Types of Messages Processed +- **MESSAGE**: Processed as a regular message. Stores the message data, encrypts the message, and updates the chats and messages databases. +- **REQUEST**: Processed as a request. Records the request data and updates the request database. +- **RESPONSE**: Processed as a response. Parses the response data, updates the response database, and marks the corresponding request as completed. +- **MAIL**: Processed as a mail. Parses mail data, encrypts the content, and updates the mails database. +- **CREATE_GROUP**: Processed for group creation. Verifies the group information, encrypts sensitive data, and updates the groups database. Requests the group inbox and adds the new group to the inbox. +- **REVOKE_KEY**: Processed for key revocation. Verifies sender credentials, revokes the group key, and updates the groups database. Adds the group to the key revocation list. +- **CREATE_PIPELINE**: Processed for pipeline creation. Encrypts sensitive data, updates the pipeline database, requests the pipeline inbox, and adds the pipeline to the inbox. + +### Throws +- **"blocked-user"**: If the sender is blocked and the message type is not "REVOKE_KEY". + +### Note +Ensure that the necessary functions (`floDapps.storePubKey`, `encrypt`, `addMark`, `requestGroupInbox`, `requestPipelineInbox`, etc.) and database (`compactIDB`) operations are correctly implemented for this function to work as expected. + +## `requestDirectInbox()` + +Requests and processes direct inbox data from the server, updating the local database and user interface accordingly. + +### Description +This function establishes a connection to the server to fetch direct inbox data for the user. It processes the received data using the `processData.direct()` function and updates the local database (`compactIDB`) and user interface (UI) based on the categorized messages, requests, responses, mails, new group notifications, key revocations, and pipelines. + +### Parameters +- None + +### Returns +A Promise that resolves to a success message when the direct inbox data is successfully fetched and processed. + +### Function Behavior +1. **Closes Existing Connection**: If there's an existing request connection (`directConnID`), it is closed to prepare for a new request. +2. **Fetches Data**: Requests application data from the server using `floCloudAPI.requestApplicationData` with the user ID and lower vector clock. +3. **Processes Data**: Processes the received data using the `processData.direct()` function, categorizing messages, requests, responses, mails, new group notifications, key revocations, and pipelines. +4. **Updates Local Database**: Updates the local database (`compactIDB`) with the latest received vector clock and stores it in the `appendix` table. +5. **Updates User Interface**: Passes the categorized data to the UI component for rendering. + +### Throws +- **Error**: If there's an issue with the server request or data processing. + +### Note +Ensure that the necessary functions (`processData.direct`, `floCloudAPI.requestApplicationData`, `compactIDB.writeData`, etc.) and components (UI) are correctly implemented and accessible for this function to work as expected. + +## `getChatOrder(separate = false)` + +Gets the order of chats, groups, and pipelines for the messenger application. + +### Description +This function retrieves the order of chats, groups, and pipelines based on their last received messages' vector clocks. It can return separate arrays for direct chats, groups, and pipelines, or a combined array containing all of them, sorted in descending order of their last received messages. + +### Parameters +- `separate` (Boolean, optional): If `true`, returns separate arrays for direct chats, groups, and pipelines. If `false` (default), returns a combined array. + +### Returns +An array or an object with separate arrays based on the `separate` parameter: +- **When `separate` is `true`**: + - `result.direct` (Array): An array of direct chat IDs, sorted by their last received messages' vector clocks. + - `result.group` (Array): An array of group IDs, sorted by their last received messages' vector clocks. + - `result.pipeline` (Array): An array of pipeline IDs, sorted by their last received messages' vector clocks. +- **When `separate` is `false`**: + - An array containing IDs of direct chats, groups, and pipelines combined, sorted by their last received messages' vector clocks. + +### Example Usage + +```javascript +const chatOrder = getChatOrder(true); +console.log("Separate Chat Order:", chatOrder.direct, chatOrder.group, chatOrder.pipeline); + +const combinedOrder = getChatOrder(); +console.log("Combined Chat Order:", combinedOrder); +``` +### Throws +None + +### Note +Ensure that the necessary data structures (_loaded.chats, _loaded.groups, _loaded.pipeline, _loaded.appendix) are correctly populated for this function to provide accurate results. +```javascript +const chatOrder = getChatOrder(true); +console.log("Separate Chat Order:", chatOrder.direct, chatOrder.group, chatOrder.pipeline); + +const combinedOrder = getChatOrder(); +console.log("Combined Chat Order:", combinedOrder); +``` + +## `loadDataFromIDB(defaultList = true)` + +Loads data from IndexedDB (IDB) and decrypts it if necessary, returning the processed data. + +### Description +This function reads specified data from IndexedDB (`compactIDB`) and decrypts the content using the AES key if present. It handles different data categories, such as messages, mails, groups, keys, pipeline, blocked users, and appendix data. + +### Parameters +- `defaultList` (Boolean, optional): If `true`, loads default data categories including mails, marked messages, groups, pipeline, chats, blocked users, and appendix. If `false`, loads additional categories including individual messages, mails, marked messages, chats, groups, group keys, pipeline, blocked users, and appendix. Default is `true`. + +### Returns +A Promise that resolves to an object containing the loaded and processed data. + +### Function Behavior +1. **Determine Data Categories**: Based on `defaultList` parameter, selects the appropriate data categories to load from IndexedDB. +2. **Decrypt Data (if AES Key exists)**: If an AES key is found in the data, decrypts messages, mails, group keys, pipeline, and other sensitive content using the AES key. +3. **Handle Missing AES Key**: If the AES key is not found and there are existing mails, generates a new AES key, encrypts it with the user's public key, and stores it in the appendix. +4. **Resolve or Reject Promise**: Resolves the promise with the processed data if successful. Rejects the promise with an error message if any issues occur during data loading or decryption. + +### Throws +- **"Corrupted AES Key"**: If the AES key is corrupted or cannot decrypt the data. +- **"AES Key not Found"**: If the AES key is not found, and there are existing mails in the data. + +### Note +Ensure that the necessary functions (`floCrypto.randString`, `floCrypto.encryptData`, `decrypt`, etc.) and the IndexedDB (`compactIDB`) operations are correctly implemented for this function to work as expected. + +## `messenger.backupData()` + +Creates a backup of user data including contacts, public keys, and encrypted application data. + +### Description +This function generates a backup of user-specific data from IndexedDB (`compactIDB`) and additional global data (contacts and public keys) stored in `floGlobals`. The backup data is encrypted and signed before being returned as a downloadable Blob object. + +### Returns +A Promise that resolves to a Blob containing the encrypted and signed backup data in JSON format. + +### Function Behavior +1. **Load Data**: Calls `loadDataFromIDB(false)` to load user-specific data from IndexedDB, excluding default categories. +2. **Prepare Backup Object**: Removes the AES key from the data, adds global contacts and public keys, and encodes the data to Base64. +3. **Encrypt Data**: Encrypts the prepared data using the user's encryption function (`floDapps.user.encipher`). +4. **Sign Data**: Signs the encrypted data using the user's signing function (`floDapps.user.sign`). +5. **Create Blob**: Creates a Blob object containing the signed JSON data. +6. **Resolve Promise**: Resolves the promise with the generated Blob. + +### Throws +- Any error encountered during data loading, encryption, or signing process. + +### Note +Ensure that the necessary functions (`loadDataFromIDB`, `floDapps.user.encipher`, `floDapps.user.sign`, etc.) and IndexedDB (`compactIDB`) operations are correctly implemented for this function to generate a valid backup. + +## `messenger.parseBackup(blob)` + +Parses and verifies the integrity of a backup file, decrypting and returning the original data. + +### Description +This function takes a Blob or File object representing a backup file and performs the following steps: verifies the signature, validates the user ID and public key, decrypts the data, and parses the decoded data. If successful, the original user data is returned. + +### Parameters +- `blob` (Blob or File): The backup file as a Blob or File object. + +### Returns +A Promise that resolves to the parsed and decrypted user data from the backup. + +### Function Behavior +1. **Check Blob Type**: Verifies if the input is a valid Blob or File object. +2. **Read Blob Content**: Reads the content of the Blob using a FileReader. +3. **Verify Signature**: Verifies the signature of the Blob data using the public key included in the Blob. +4. **Validate User ID and Public Key**: Ensures the user ID and public key in the Blob match the current user's credentials. +5. **Decrypt Data**: Decrypts the encrypted data using the user's decryption function (`floDapps.user.decipher`). +6. **Decode Data**: Decodes the decrypted data from Base64 and parses it into a JSON object. +7. **Resolve Promise**: Resolves the promise with the parsed user data if all steps are successful. + +### Throws +- **"Corrupted Backup file: Signature verification failed"**: If the signature verification fails. +- **"Invalid Backup file: Incorrect floID"**: If the user ID or public key in the backup file does not match the current user's credentials. +- **"Corrupted Backup file: Decryption failed"**: If the decryption process fails. +- **"Corrupted Backup file: Parse failed"**: If parsing the decrypted data fails. + +### Note +Ensure that the necessary functions (`floCrypto.verifySign`, `floDapps.user.decipher`, etc.) are correctly implemented for this function to successfully verify, decrypt, and parse the backup file. + +## `messenger.restoreData(arg)` + +Restores user data from a backup, encrypts the messages, and updates IndexedDB (`compactIDB`) with the restored data. + +### Description +This function restores user data from a backup (Blob or File object), decrypts it, encrypts the messages, and updates the appropriate IndexedDB categories (`contacts`, `pubKeys`, `messages`, `mails`, `chats`, `groups`, `gkeys`, `pipeline`, `blocked`, and `appendix`) with the restored and processed data. It selectively updates data based on the latest received vector clocks. + +### Parameters +- `arg` (Blob, File, or Object): The backup data as a Blob, File, or parsed Object. + +### Returns +A Promise that resolves to a success message if the restoration is successful. + +### Function Behavior +1. **Determine Input Type**: Checks if the input is a Blob or File object. If it's an Object, skips the parsing step. +2. **Parse Backup Data**: Parses the backup data using `parseBackup` function, decrypting and verifying the data. +3. **Encrypt Messages and Sensitive Data**: Encrypts the messages, mail contents, group keys, and pipeline keys in the restored data. +4. **Selective Update**: Compares the vector clocks of restored data with the current state of `compactIDB` and selectively updates data in IndexedDB. +5. **Update Contacts and Public Keys**: Updates contacts and public keys stored in `floGlobals` if they are not present in the restored data. +6. **Write Data to IndexedDB**: Writes the updated data to the appropriate categories in IndexedDB. +7. **Resolve Promise**: Resolves the promise with a success message if the restoration and data update are successful. + +### Throws +- **"Restore Failed: Unable to write to IDB"**: If there is an error while updating data in IndexedDB. +- Any error encountered during data processing, encryption, or verification. + +### Note +Ensure that the necessary functions (`parseBackup`, `floDapps.storeContact`, `floDapps.storePubKey`, `compactIDB.writeData`, etc.) and IndexedDB (`compactIDB`) operations are correctly implemented for this function to successfully restore and update the data. + +## `messenger.clearUserData()` + +Clears all user-related data, including IndexedDB databases and credentials, ensuring a clean slate. + +### Description +This function clears all user-specific data stored in IndexedDB, removes the last transaction details, and clears user credentials, providing a fresh start for the user. It performs a series of operations to ensure the removal of user data and related artifacts. + +### Returns +A Promise that resolves to a success message if the user data is cleared successfully. + +### Function Behavior +1. **Generate User FloID**: Computes the FLO blockchain identifier (FloID) for the current user. +2. **Create Promises Array**: Forms an array of promises for clearing user data, including IndexedDB databases and credentials. + - **CompactIDB Delete**: Deletes the specific IndexedDB database associated with the user (`${floGlobals.application}_${user_floID}`). + - **CompactIDB Remove Last Tx**: Removes the last transaction details associated with the user and application. + - **FloDapps Clear Credentials**: Clears user credentials and any associated authentication tokens. +3. **Execute Promises**: Executes all promises concurrently using `Promise.all`. +4. **Resolve Promise**: Resolves the promise with a success message if the clearing process is successful. + +### Throws +- Any error encountered during the clearing process, including errors from IndexedDB deletion or credential clearing. + +### Note +Ensure that the necessary functions (`compactIDB.deleteDB`, `compactIDB.removeData`, `floDapps.clearCredentials`, etc.) and IndexedDB (`compactIDB`) operations are correctly implemented for this function to successfully clear user data. + +## `messenger.createGroup(groupname, description = '')` + +Creates a new group, generates group keys, and stores group information in IndexedDB. + +### Description +This function creates a new group with the given `groupname` and optional `description`. It generates unique group identifiers, administers encryption keys, and adds the group information to IndexedDB categories (`groups` and `gkeys`). The function ensures data integrity and encryption for the newly created group. + +### Parameters +- `groupname` (String): The name of the group to be created. +- `description` (String, optional): A description of the group (default is an empty string). + +### Returns +A Promise that resolves to the group information object if the group creation is successful. + +### Function Behavior +1. **Validate Group Name**: Checks if `groupname` is provided; if not, rejects the promise with "Invalid Group Name" error. +2. **Generate Group Identifiers**: Generates a unique group FloID and public-private key pair. +3. **Create Group Information**: Forms an object containing essential group information, including group ID, public key, admin ID, name, description, creation timestamp, and member list. +4. **Create Group Hash**: Constructs a hash from specific group properties (`groupID`, `created`, `admin`) and signs it with the group's private key, ensuring data integrity. +5. **Generate Encryption Key**: Generates a random encryption key (`eKey`) for group messages. +6. **Store Data in IndexedDB**: Adds the group information to `groups` and the encrypted private key to `gkeys` in IndexedDB. +7. **Update Loaded Groups**: Adds the group information to the local loaded groups (`_loaded.groups`). +8. **Request Group Inbox**: Initiates the request for the group's inbox messages. +9. **Resolve Promise**: Resolves the promise with the created group information object if the creation process is successful. + +### Throws +- **"Invalid Group Name"**: If `groupname` is not provided or empty. +- Any error encountered during IndexedDB data storage or group creation process. + +### Note +Ensure that the necessary functions (`floCrypto.generateNewID`, `floCrypto.signData`, `encrypt`, `compactIDB.addData`, etc.) and IndexedDB (`compactIDB`) operations are correctly implemented for this function to successfully create and store group information. + +## `messenger.createGroup(groupname, description = '')` + +Creates a new group, generates group keys, and stores group information in IndexedDB. + +### Description +This function creates a new group with the given `groupname` and optional `description`. It generates unique group identifiers, administers encryption keys, and adds the group information to IndexedDB categories (`groups` and `gkeys`). The function ensures data integrity and encryption for the newly created group. + +### Parameters +- `groupname` (String): The name of the group to be created. +- `description` (String, optional): A description of the group (default is an empty string). + +### Returns +A Promise that resolves to the group information object if the group creation is successful. + +### Function Behavior +1. **Validate Group Name**: Checks if `groupname` is provided; if not, rejects the promise with "Invalid Group Name" error. +2. **Generate Group Identifiers**: Generates a unique group FloID and public-private key pair. +3. **Create Group Information**: Forms an object containing essential group information, including group ID, public key, admin ID, name, description, creation timestamp, and member list. +4. **Create Group Hash**: Constructs a hash from specific group properties (`groupID`, `created`, `admin`) and signs it with the group's private key, ensuring data integrity. +5. **Generate Encryption Key**: Generates a random encryption key (`eKey`) for group messages. +6. **Store Data in IndexedDB**: Adds the group information to `groups` and the encrypted private key to `gkeys` in IndexedDB. +7. **Update Loaded Groups**: Adds the group information to the local loaded groups (`_loaded.groups`). +8. **Request Group Inbox**: Initiates the request for the group's inbox messages. +9. **Resolve Promise**: Resolves the promise with the created group information object if the creation process is successful. + +### Throws +- **"Invalid Group Name"**: If `groupname` is not provided or empty. +- Any error encountered during IndexedDB data storage or group creation process. + +### Note +Ensure that the necessary functions (`floCrypto.generateNewID`, `floCrypto.signData`, `encrypt`, `compactIDB.addData`, etc.) and IndexedDB (`compactIDB`) operations are correctly implemented for this function to successfully create and store group information. + +## `messenger.addGroupMembers(groupID, newMem, note = undefined)` + +Adds new members to an existing group, ensuring valid addresses and pubKeys. + +### Description +This function allows the group admin to add new members (`newMem`) to an existing group identified by `groupID`. It validates the provided member addresses and pubKeys, sends group information to the new members for approval, and updates the group with the approved members. The function handles errors for invalid addresses, unavailable pubKeys, and unauthorized access. + +### Parameters +- `groupID` (String): The unique ID of the group to which new members will be added. +- `newMem` (Array or String): An array of FLO addresses or a single FLO address string representing the new members to be added. +- `note` (String, optional): An optional note or message associated with the addition of new members. + +### Returns +A Promise that resolves to a success message if the new members are added successfully. + +### Function Behavior +1. **Validate Member Data**: Checks if `newMem` is an array of strings or a single string; if not, converts it to an array. Validates each member address: + - Checks for valid FLO addresses and available pubKeys. Separates invalid members into two arrays (`imem1` for invalid FLO addresses and `imem2` for FLO addresses with missing pubKeys). +2. **Access Control**: Ensures that the user invoking the function is the group admin. If not, rejects the promise with "Access denied: Admin only!" error. +3. **Encrypt Group Info**: Encrypts the JSON representation of the group (`groupInfo`) using the group's encryption key (`k`). +4. **Send Group Info to New Members**: Sends the encrypted group information to each new member asynchronously using `sendRaw`. Collects the results into `success` and `failed` arrays based on the success of the operation. +5. **Encrypt and Send Approval Message**: Encrypts the approved new member list (`success.join("|")`) using the group's encryption key and sends it to the group. +6. **Resolve Promise**: Resolves the promise with a success message indicating the added members if the process is successful. + +### Throws +- **"Invalid Members(floIDs)"**: If any member address in `newMem` is not a valid FLO address. +- **"Invalid Members (pubKey not available)"**: If any member address in `newMem` does not have an available public key. +- **"Access denied: Admin only!"**: If the invoking user is not the admin of the specified group. +- Any error encountered during the member addition process. + +### Note +Ensure that the necessary functions (`floCrypto.validateAddr`, `floGlobals.pubKeys`, `sendRaw`, `encrypt`, etc.) and group data (`_loaded.groups[groupID]`) are correctly implemented for this function to successfully add new members to the group. + +## `messenger.rmGroupMembers(groupID, rmMem, note = undefined)` + +Removes members from an existing group, ensuring valid members and admin access. + +### Description +This function allows the group admin to remove specified members (`rmMem`) from an existing group identified by `groupID`. It validates the provided members, sends a removal request to the group, revokes access for the removed members, and updates the group membership. The function handles errors for invalid members, unauthorized access, and any issues during the removal process. + +### Parameters +- `groupID` (String): The unique ID of the group from which members will be removed. +- `rmMem` (Array or String): An array of FLO addresses or a single FLO address string representing the members to be removed. +- `note` (String, optional): An optional note or message associated with the member removal. + +### Returns +A Promise that resolves to a success message if the members are removed successfully. + +### Function Behavior +1. **Validate Member Data**: Checks if `rmMem` is an array of strings or a single string; if not, converts it to an array. Filters invalid members (`imem`) not present in the group. +2. **Access Control**: Ensures that the user invoking the function is the group admin. If not, rejects the promise with "Access denied: Admin only!" error. +3. **Encrypt and Send Removal Request**: Encrypts the list of members to be removed (`rmMem.join("|")`) using the group's encryption key and sends it as a removal request to the group using `sendRaw`. +4. **Update Group Membership**: Removes the specified members from the group's `members` array. +5. **Revoke Access for Removed Members**: Calls `revokeKey(groupID)` to revoke access for the removed members. +6. **Resolve Promise**: Resolves the promise with a success message indicating the removed members if the process is successful. + +### Throws +- **"Invalid members"**: If any member in `rmMem` is not present in the group. +- **"Access denied: Admin only!"**: If the invoking user is not the admin of the specified group. +- Any error encountered during the member removal process. + +### Note +Ensure that the necessary functions (`floCrypto.validateAddr`, `sendRaw`, `encrypt`, `revokeKey`, etc.) and group data (`_loaded.groups[groupID]`) are correctly implemented for this function to successfully remove members from the group. + +## `messenger.disableGroup(groupID)` + +Disables a specified group and closes the corresponding connection. + +### Description +This function allows the user to disable a specific group identified by `groupID`. Disabling a group marks it as inactive and also encrypts the group's encryption key (`eKey`). Additionally, it closes the active request connection associated with the group (`groupConnID[groupID]`). The function handles group disabling, encryption of the encryption key, and resolves with a success message or rejects with an error message in case of failure. + +### Parameters +- `groupID` (String): The unique ID of the group to be disabled. + +### Returns +A Promise that resolves with a success message indicating the successful disabling of the group and closure of the connection. + +### Function Behavior +1. **Validation**: Checks if the specified group (`_loaded.groups[groupID]`) exists. If not, rejects with an error message ("Group not found"). +2. **Disabling Group**: Marks the group as disabled by setting the `disabled` property to `true`. +3. **Encryption**: Encrypts the group's encryption key (`eKey`) to enhance security before storing it in the local database. +4. **Database Update**: Writes the updated group information to the local database (`compactIDB.writeData("groups", groupInfo, groupID)`). +5. **Connection Closure**: Closes the active request connection associated with the group (`floCloudAPI.closeRequest(groupConnID[groupID])`). +6. **Resolution**: Resolves the promise with a success message ("Group disabled") upon successful disabling and closure of the connection. +7. **Rejection**: Rejects the promise with an error message in case of any errors during validation, encryption, database update, or connection closure. + +### Throws +- "Group not found": If the specified group does not exist. +- "Group already disabled": If the specified group is already disabled. +- Any error encountered during encryption, database update, or connection closure. + +### Note +Ensure that the specified group (`_loaded.groups[groupID]`) exists and has an active connection (`groupConnID[groupID]`) before disabling it. + +## `processData.group(groupID)` + +Processes incoming group-specific data and updates the local database accordingly. + +### Description +This function processes incoming group-specific data and decrypts messages if necessary. It updates the local database (`compactIDB`) based on the type of incoming data, including group messages, member additions, description updates, member removals, and group name changes. The function handles different types of group-related actions, decrypts messages using the appropriate encryption key (`eKey`), and updates the local database and inbox accordingly. + +### Parameters +- `groupID` (String): The unique ID of the group to which the incoming data belongs. + +### Returns +A function that takes `unparsed` data and `newInbox` object as parameters. It processes the data, updates the local database, and returns a boolean value indicating whether there were changes in group information (`true` if there were changes, `false` otherwise). + +### Function Behavior +1. **Validation**: Checks if the sender (`unparsed.senderID`) is a member of the specified group (`_loaded.groups[groupID].members`). If not, ignores the incoming data. +2. **Public Key Storage**: Stores the sender's public key (`unparsed.pubKey`) if not stored already using `floDapps.storePubKey`. +3. **Data Preparation**: Prepares a `data` object containing essential information such as timestamp (`time`), sender (`sender`), and group ID (`groupID`). +4. **Encryption Key Retrieval**: Retrieves the appropriate encryption key (`eKey`) for decryption based on the group's expiry keys (`expiredKeys[groupID]`). If expiry keys exist, decrypts messages using the corresponding key. +5. **Message Decryption**: Decrypts incoming messages (`unparsed.message`) using the retrieved or default encryption key (`k`). +6. **Info Change Detection**: Detects changes in group information, such as member additions, description updates, member removals, or group name changes. Sets `infoChange` to `true` if there were changes; otherwise, sets it to `false`. +7. **Database Update**: Writes the processed data (`data`) to the local database (`compactIDB.addData("messages", Object.assign({}, data), `${groupID}|${vc}`)`). +8. **Inbox Update**: Updates the `newInbox` object with the processed data (`newInbox.messages[vc] = data`). +9. **Unread Marking**: Marks the group as unread if the sender is not the current user. +10. **Returns**: Returns `infoChange`, indicating whether there were changes in group information. + +### Throws +- No specific exceptions are thrown from this function. Errors are typically handled within the function logic and returned as `infoChange` value. + +### Note +Ensure that the sender is a valid member of the group before processing the incoming data. + +## `requestGroupInbox(groupID, _async = true)` + +Requests group-specific data from the server and updates the local group inbox accordingly. + +### Description +This function establishes a connection with the server to receive group-specific data. It processes incoming data using the `processData.group` function, updates the local group inbox, and writes relevant information to the local database (`compactIDB`). The function handles group messages, member additions, description updates, member removals, and group name changes. + +### Parameters +- `groupID` (String): The unique ID of the group for which the data is requested. +- `_async` (Boolean, Default: `true`): A flag indicating whether to request data asynchronously (`true`) or synchronously (`false`). When set to `false`, the function returns a promise. + +### Returns +- If `_async` is `true`, the function establishes a connection with the server and returns `undefined`. +- If `_async` is `false`, the function returns a promise that resolves with a success message if the connection is established successfully. Otherwise, it rejects with an error message. + +### Function Behavior +1. **Connection Closure**: Closes any existing request connection for the specified `groupID` (if any) using `floCloudAPI.closeRequest`. +2. **Data Processing**: Defines a `parseData` function using `processData.group(groupID)`. +3. **Callback Function**: Defines a `callbackFn` function that processes incoming data, updates the local group inbox (`newInbox`), and writes relevant information to the local database (`compactIDB`). If there are changes in group information (`infoChange` is `true`), updates the group information in the local database. +4. **Connection Request**: Requests group-specific data from the server using `floCloudAPI.requestApplicationData`. +5. **Asynchronous Mode**: If `_async` is `true`, establishes the connection asynchronously and stores the connection ID (`conn_id`) in `groupConnID[groupID]`. +6. **Synchronous Mode**: If `_async` is `false`, returns a promise that resolves with a success message if the connection is established successfully. Otherwise, it rejects with an error message. + +### Throws +- No specific exceptions are thrown from this function. Errors are typically logged to the console. + +### Note +Ensure that the `groupID` provided is valid, and the user has appropriate permissions to access the group's data. + +## `messenger.init()` + +Initializes the messenger application, loading user data, and establishing connections for direct messages, group messages, and pipeline data. + +### Description +This function sets up the messenger application by initializing the user's local database, loading data from IndexedDB, and requesting necessary data from the cloud and blockchain. It prepares the application state, loads chat and mail data, and establishes connections for receiving direct messages, group messages, and pipeline updates. + +### Function Behavior +1. **User Database Initialization**: Initializes the user's local database using the `initUserDB` function. +2. **Data Loading**: Loads data from IndexedDB using the `loadDataFromIDB` function. Updates the `_loaded` object with the loaded data for chats, mails, groups, pipeline, marked items, and blocked users. +3. **UI Rendering**: Calls UI rendering functions to display chat order, mails, and marked items. +4. **Data Requests**: Requests data from the cloud for direct messages and from groups and pipelines. Initiates connections for incoming messages and updates. +5. **Blockchain Data Loading**: Loads additional data from the blockchain using the `loadDataFromBlockchain` function. +6. **Promise Handling**: Resolves the promise if the initialization is successful, providing a success message. Rejects the promise if there are errors during the initialization process. + +### Throws +- This function may throw errors if there are issues with initializing the user's database, loading data from IndexedDB, establishing connections, or loading data from the blockchain. Specific error messages will be provided in the rejection of the returned promise. + +### Note +Ensure that the user has proper permissions and access to the required data sources (IndexedDB, cloud server, and blockchain) for successful initialization. + +## `messenger.loadDataFromBlockchain()` + +Loads additional data from the blockchain to supplement the user's local database. + +### Description +This function retrieves data related to the messenger application from the FLO blockchain. It queries the blockchain for relevant transactions, processes the data, and updates the local IndexedDB. This ensures that the messenger application has the latest information from the blockchain. + +### Function Behavior +1. **User Identification**: Converts the user's ID to a valid FLO address (`user_floID`) using `floCrypto.toFloID()`. If the user ID is invalid, the function rejects the promise. +2. **Query Preparation**: Prepares query options for reading data from the blockchain, including the application pattern and transaction tracking. +3. **Blockchain Data Retrieval**: Utilizes the FLO blockchain API (`floBlockchainAPI.readData()`) to fetch relevant data associated with the user's address. +4. **Data Processing and Storage**: Processes retrieved transactions, extracting application-specific data, and stores it in the local IndexedDB under the `flodata` table. Each transaction's content is stored along with its timestamp and transaction ID. +5. **Last Transaction Update**: Updates the `lastTx` entry in the local database, ensuring subsequent requests fetch only new transactions. +6. **Promise Handling**: Resolves the promise with `true` upon successful data retrieval and storage. Rejects the promise if there are errors during the process. + +### Throws +- This function may throw errors if there are issues with querying the blockchain or writing data to IndexedDB. Specific error messages will be provided in the rejection of the returned promise. + +### Note +Ensure that the user's address is valid and accessible on the FLO blockchain for successful data retrieval. + +## `MultiSig.createAddress(pubKeys: string[], minRequired: number)` + +Creates a multi-signature (multisig) Bitcoin address using the specified public keys and minimum required signatures. + +### Description +This function generates a multisig Bitcoin address by combining multiple public keys and setting a minimum required number of signatures for transactions. Multisig addresses enhance security by requiring authorization from multiple parties to access the funds associated with the address. + +### Parameters +- `pubKeys` (Array of Strings): An array of public keys (in hexadecimal format) belonging to the co-owners of the multisig address. +- `minRequired` (Number): The minimum number of signatures required to authorize a transaction from the multisig address. + +### Function Behavior +1. **Public Key Validation**: Validates each public key provided in the `pubKeys` array. If any key is invalid, the function rejects the promise with an error message. +2. **Private Key Retrieval**: Retrieves the user's private key asynchronously. +3. **Multisig Address Creation**: Utilizes the `btcOperator.multiSigAddress()` function to create a multisig address based on the provided public keys and minimum required signatures. +4. **Blockchain Transaction**: Writes the multisig address information to the FLO blockchain, associating it with the provided public keys. +5. **Local Storage**: Stores the multisig address data locally in the `flodata` table of IndexedDB for future reference. +6. **Promise Handling**: Resolves the promise with the generated multisig Bitcoin address upon successful creation. Rejects the promise with an error message if any step encounters issues. + +### Throws +- This function may throw errors if there are problems with public key validation, multisig address creation, blockchain transaction writing, or data storage. Specific error messages will be provided in the rejection of the returned promise. + +### Note +- Ensure that the provided public keys are valid and accessible for creating a multisig address. +- The multisig address generated by this function requires at least `minRequired` signatures from the specified public keys to authorize transactions. + +## `MultiSig.listAddress()` + +Lists all valid multi-signature (multisig) Bitcoin addresses associated with the user. + +### Description +This function retrieves and validates multisig Bitcoin addresses stored in the local IndexedDB database. It checks the validity of each multisig address and ensures that the user is a part of the multisig group. + +### Function Behavior +1. **Database Query**: Searches the local IndexedDB database (`flodata` table) for multisig addresses stored under the `TYPE_BTC_MULTISIG` key. +2. **Address Validation**: Validates each retrieved multisig address by decoding its redeem script and ensuring it meets the necessary criteria: + - It is a multisig address. + - The user's public key is part of the multisig group. + - The number of required signatures is valid. +3. **Data Extraction**: Extracts relevant information about the valid multisig addresses, including the redeem script, associated public keys, minimum required signatures, timestamp, and transaction ID. +4. **Result Formatting**: Constructs an object where the keys are valid multisig Bitcoin addresses, and the values are objects containing detailed information about each address. +5. **Promise Handling**: Resolves the promise with the object containing valid multisig addresses upon successful retrieval. Rejects the promise with an error message if any issues occur during the process. + +### Returns +- A Promise that resolves with an object where the keys are valid multisig Bitcoin addresses, and the values are objects containing detailed information about each address. + +### Throws +- This function may throw errors if there are problems with database querying, address validation, or data extraction. Specific error messages will be provided in the rejection of the returned promise. + +### Note +- Ensure that the multisig addresses stored in the local database are accurate and valid. +- This function is designed to provide information about multisig addresses associated with the user for user-specific use cases or display purposes. + +## `MultiSig.createTx_BTC(address, redeemScript, receivers, amounts, fee = null, options = {})` + +Creates a multi-signature (multisig) Bitcoin transaction and sends it to the specified receivers. + +### Description +This function generates a multisig Bitcoin transaction for a given multisig address (`address`) and its corresponding redeem script (`redeemScript`). It specifies the receivers of the transaction and the respective amounts to be sent to each receiver. Additionally, it allows for setting a custom transaction fee (`fee`) and additional transaction options (`options`). + +### Parameters +- `address`: Multisig Bitcoin address for the transaction. +- `redeemScript`: Redeem script corresponding to the multisig address. +- `receivers`: An array of recipient addresses to receive BTC from the transaction. +- `amounts`: An array specifying the amounts of BTC to be sent to each corresponding receiver address. +- `fee` (optional): Custom transaction fee (in BTC) to be included in the transaction. If not provided, the default fee will be calculated. +- `options` (optional): Additional options for the transaction (if any). + +### Function Behavior +1. **Address Validation**: Validates the provided sender `address` and its `redeemScript` to ensure it is a valid multisig address and the user is a part of the multisig group. +2. **Transaction Creation**: Creates a multisig Bitcoin transaction with the specified receivers, amounts, and optional transaction fee using the `btcOperator.createMultiSigTx` function. +3. **Transaction Signing**: Signs the created transaction with the user's private key. +4. **Pipeline Creation**: Creates a pipeline for secure communication using the `createPipeline` function. The encrypted transaction hex is sent through this pipeline. +5. **Transaction Encryption**: Encrypts the signed transaction hex with the pipeline encryption key. +6. **Transaction Transmission**: Sends the encrypted transaction to the network using the `sendRaw` function with the "TRANSACTION" type. +7. **Promise Handling**: Resolves the promise with the ID of the created pipeline upon successful transmission. Rejects the promise with an error message if any issues occur during the process. + +### Returns +- A Promise that resolves with the ID of the created pipeline for secure communication. + +### Throws +- This function may throw errors if the provided address or redeem script is invalid, transaction creation or signing fails, or there are issues with pipeline creation or transaction transmission. Specific error messages will be provided in the rejection of the returned promise. + +### Note +- Ensure that the provided address, redeem script, receivers, and amounts are accurate and valid. +- This function is designed for creating and sending multisig Bitcoin transactions securely. +- Properly handle the resolved pipeline ID for any future communication or retrieval of transaction status. + +## `MultiSig.signTx_BTC(pipeID)` + +Signs a multisig Bitcoin transaction and broadcasts it to the network. + +### Description +This function signs a multisig Bitcoin transaction received through the specified pipeline (`pipeID`) and broadcasts the signed transaction to the Bitcoin network. It ensures the transaction is properly signed before broadcasting it. + +### Parameters +- `pipeID`: ID of the pipeline containing the unsigned multisig Bitcoin transaction. + +### Function Behavior +1. **Pipeline Validation**: Validates the provided `pipeID` to ensure it corresponds to a BTC-multisig pipeline and is active. +2. **Transaction Retrieval**: Retrieves the latest unsigned multisig transaction hex from the specified pipeline. +3. **Transaction Signing**: Signs the retrieved transaction hex using the user's private key. +4. **Transaction Encryption**: Encrypts the signed transaction hex with the pipeline encryption key. +5. **Transaction Broadcasting**: Broadcasts the signed transaction to the Bitcoin network using the `btcOperator.broadcastTx` function. +6. **Broadcast Confirmation**: Sends a confirmation message containing the broadcasted transaction ID back through the pipeline. +7. **Promise Handling**: Resolves the promise with an object containing the signed transaction hex (`tx_hex`) and its corresponding transaction ID (`txid`) upon successful broadcasting. Rejects the promise with an error message if any issues occur during the process. + +### Returns +- A Promise that resolves with an object containing the signed transaction hex (`tx_hex`) and its corresponding transaction ID (`txid`). + +### Throws +- This function may throw errors if the provided `pipeID` is invalid, the pipeline is not a BTC-multisig pipeline, the transaction signing fails, or there are issues with broadcasting the transaction. Specific error messages will be provided in the rejection of the returned promise. + +### Note +- Ensure that the provided `pipeID` corresponds to a valid BTC-multisig pipeline with an unsigned transaction. +- Properly handle the resolved object containing the signed transaction hex and its corresponding transaction ID for further reference or confirmation. +- Monitor the Bitcoin network for the transaction's confirmation after broadcasting. + +## `MultiSig.createTx_FLO(address, redeemScript, receivers, amounts, floData = '', options = {})` + +Creates and signs a multisig FLO transaction and sends it through the pipeline. + +### Description +This function creates a multisig FLO transaction, signs it with the user's private key, and sends the signed transaction through the specified pipeline. It ensures the transaction is properly signed before sending it. + +### Parameters +- `address`: Multisig FLO address in the format `ADDRESS@NETWORK` (e.g., `F7DGuKp8ZrZah8K3QEVDSXKo9Pm6EVDqQ4@florincoin`) +- `redeemScript`: Redeem script corresponding to the multisig address. +- `receivers`: Array of recipient addresses. +- `amounts`: Array of corresponding amounts to be sent to the receivers. +- `floData` (optional): Additional data to be included in the FLO transaction. Default is an empty string. +- `options` (optional): Additional options for transaction creation. Default is an empty object. + +### Function Behavior +1. **Address Validation**: Validates the provided `address` to ensure it is a valid multisig FLO address. +2. **Redeem Script Decoding**: Decodes the provided `redeemScript` to verify its validity and obtain necessary information. +3. **User Validation**: Checks if the user's public key is part of the multisig group defined in the `redeemScript`. +4. **Transaction Creation**: Creates a multisig FLO transaction using `floBlockchainAPI.createMultisigTx`. +5. **Transaction Signing**: Signs the created transaction using the user's private key. +6. **Pipeline Creation**: Creates a pipeline with the specified co-owners and sends the signed transaction through it. +7. **Transaction Encryption**: Encrypts the signed transaction with the pipeline encryption key. +8. **Transaction Sending**: Sends the encrypted transaction through the pipeline. +9. **Promise Handling**: Resolves the promise with the ID of the created pipeline upon successful transaction sending. Rejects the promise with an error message if any issues occur during the process. + +### Returns +- A Promise that resolves with the ID of the created pipeline where the signed multisig FLO transaction is sent. + +### Throws +- This function may throw errors if the provided `address` is invalid, the `redeemScript` is incorrect, the user's public key is not part of the multisig group, or there are issues with transaction creation, signing, or sending. Specific error messages will be provided in the rejection of the returned promise. + +### Note +- Ensure that the provided `address` and `redeemScript` correspond to a valid multisig FLO address and its redeem script. +- Handle the resolved pipeline ID for further reference or confirmation. +- Monitor the FLO network for the transaction's confirmation after sending. + +## `MultiSig.signTx_FLO(pipeID)` + +Signs and broadcasts a multisig FLO transaction through the specified pipeline. + +### Description +This function signs the latest multisig FLO transaction in the specified pipeline with the user's private key and broadcasts it to the FLO network. It ensures the transaction is properly signed and broadcasted before resolving the promise. + +### Parameters +- `pipeID`: ID of the pipeline where the multisig FLO transaction is stored. + +### Function Behavior +1. **Pipeline Model Validation**: Checks if the specified pipeline's model is `TYPE_FLO_MULTISIG`. If not, rejects the promise with an error message. +2. **Pipeline Availability Check**: Verifies if the specified pipeline is not disabled. If it is disabled, rejects the promise with an error message. +3. **Latest Transaction Retrieval**: Retrieves the latest multisig FLO transaction from the specified pipeline. +4. **Transaction Signing**: Signs the latest transaction with the user's private key using `floBlockchainAPI.signTx`. +5. **Transaction Encryption**: Encrypts the signed transaction with the pipeline's encryption key. +6. **Transaction Sending**: Sends the encrypted transaction through the specified pipeline with the type `"TRANSACTION"`. +7. **Transaction Broadcasting**: Broadcasts the signed transaction to the FLO network using `floBlockchainAPI.broadcastTx`. +8. **Broadcasted Transaction Encryption**: Encrypts the broadcasted transaction ID with the pipeline's encryption key. +9. **Broadcasted Transaction Sending**: Sends the encrypted transaction ID through the pipeline with the type `"BROADCAST"`. +10. **Promise Handling**: Resolves the promise with an object containing the signed transaction's hexadecimal representation (`tx_hex`) and its corresponding transaction ID (`txid`) upon successful broadcasting. Rejects the promise with an error message if any issues occur during the signing, sending, or broadcasting process. + +### Returns +- A Promise that resolves with an object containing the signed transaction's hexadecimal representation (`tx_hex`) and its corresponding transaction ID (`txid`). + +### Throws +- This function may throw errors if the specified pipeline's model is incorrect, the pipeline is disabled, there are issues with transaction signing, encryption, sending, or broadcasting. Specific error messages will be provided in the rejection of the returned promise. + +### Note +- Handle the resolved object containing `tx_hex` and `txid` for further reference or confirmation. +- Monitor the FLO network for the transaction's confirmation after broadcasting. +- Use the resolved `txid` to track the transaction's status on the FLO blockchain. + +## `createPipeline(model, members, ekeySize = 16, pubkeys = null)` + +Creates a new pipeline for communication among specified members. + +### Description +This function creates a new communication pipeline with the specified model and members. It generates a unique ID for the pipeline and sends the pipeline information to the members. The pipeline can be optionally encrypted with a specified encryption key (eKey) and can include public keys for each member for secure communication. + +### Parameters +- `model`: The type of communication model for the pipeline. +- `members`: An array of FLO addresses representing the members of the pipeline. +- `ekeySize`: Optional. The size of the encryption key (eKey) in characters. Default is 16. +- `pubkeys`: Optional. An array of public keys corresponding to the members. Each public key must match the respective member's FLO address. Default is `null`. + +### Function Behavior +1. **Pubkey Validation (Optional)**: Validates the provided public keys if `pubkeys` parameter is not `null`. Ensures that the length of `pubkeys` matches the length of `members`. Rejects the promise with an error message if any validation fails. +2. **Member Validation**: Validates the FLO addresses in the `members` array. Rejects the promise with an error message if any FLO address is invalid. +3. **Pipeline Creation**: Generates a unique ID for the pipeline and creates a pipeline object with `id`, `model`, and `members`. If `ekeySize` is provided, generates an encryption key (eKey) and adds it to the pipeline object. +4. **Pipeline Information Sending**: Sends the pipeline information (stringified JSON) to each member using the type `"CREATE_PIPELINE"`. +5. **Pipeline Storage**: Stores the pipeline object in local memory and IndexedDB. +6. **Pipeline Inbox Request**: Requests the inbox for the created pipeline using `requestPipelineInbox`. +7. **Promise Handling**: Resolves the promise with the created pipeline object upon successful creation. Rejects the promise with an error message if any issues occur during the creation, member validation, or information sending process. + +### Returns +- A Promise that resolves with the created pipeline object, which includes `id`, `model`, `members`, and optionally `eKey`. + +### Throws +- This function may throw errors if the provided members are invalid, if public key validation fails, or if there are issues with information sending or storage. Specific error messages will be provided in the rejection of the returned promise. + +## `requestPipelineInbox(pipeID, model, _async = true)` + +Requests the inbox for a specific communication pipeline. + +### Description +This function establishes a connection with the cloud service to retrieve messages and updates for a particular communication pipeline identified by `pipeID`. The function provides real-time updates by using a callback function to handle incoming data. It also supports asynchronous and synchronous operation based on the `_async` parameter. + +### Parameters +- `pipeID`: The unique ID of the communication pipeline for which the inbox is requested. +- `model`: The communication model associated with the pipeline (e.g., "TYPE_A", "TYPE_B"). +- `_async`: Optional. A boolean parameter indicating whether the function should operate asynchronously. Default is `true`. + +### Function Behavior +1. **Connection Closure**: If there is an existing request connection for the specified `pipeID`, it is closed to ensure a fresh connection. +2. **Data Parsing**: The function utilizes the `processData.pipeline[model](pipeID)` function to parse the incoming data. It processes the received data using a callback function. +3. **Message Handling**: For each received message, it checks if the message is intended for the specified `pipeID`. If so, it updates the inbox, marks messages as read, and stores the last received vector clock. +4. **Data Storage**: The last received vector clock is stored in the local appendix to keep track of the latest received message. +5. **UI Update**: The function updates the user interface by rendering the received messages in the appropriate communication pipeline section. +6. **Connection Establishment**: The function establishes a connection with the cloud service using `floCloudAPI.requestApplicationData()`. +7. **Asynchronous Operation**: If `_async` is `true` (default), the function operates asynchronously by setting up the connection and resolves with the connection ID. If `_async` is `false`, the function operates synchronously and returns a Promise that resolves with a success message upon connection establishment. + +### Returns (Asynchronous Mode) +- A Promise that resolves with the connection ID upon successful establishment of the connection. + +### Returns (Synchronous Mode) +- A Promise that resolves with a success message indicating the successful connection establishment. + +### Throws +- This function may throw errors if there are issues with closing existing connections, parsing data, or establishing new connections. Specific error messages will be provided in the rejection of the returned promise. + +## `disablePipeline(pipeID)` + +Disables a communication pipeline, preventing further message exchanges. + +### Description +This function disables a specific communication pipeline identified by `pipeID`. Disabling a pipeline restricts any further message transmissions or receptions within that pipeline. + +### Parameters +- `pipeID`: The unique ID of the communication pipeline to be disabled. + +### Function Behavior +1. **Pipeline Validation**: The function first checks if the specified `pipeID` exists in the loaded pipelines. +2. **Disable Check**: If the pipeline is already disabled, the function resolves immediately with a message indicating that the pipeline is already disabled. +3. **Encryption**: The function encrypts the pipeline's encryption key (`eKey`) for secure storage before updating the pipeline information. +4. **Data Update**: The disabled status of the pipeline is set to `true`, and the updated pipeline information (including the encrypted `eKey`) is written to the local database using `compactIDB.writeData()`. +5. **Connection Closure**: The function closes the request connection associated with the specified `pipeID` to terminate any ongoing communication. +6. **Connection Deletion**: The connection ID associated with the `pipeID` is removed from the `pipeConnID` object to prevent future interactions with the disabled pipeline. +7. **Success Resolution**: The function resolves with a success message indicating that the specified pipeline has been disabled. + +### Returns +- A Promise that resolves with a success message indicating the successful disabling of the specified pipeline. + +### Throws +- This function may throw errors if there are issues with validating the pipeline, encrypting data, closing connections, or updating the local database. Specific error messages will be provided in the rejection of the returned promise. + +## `sendPipelineMessage(message, pipeID)` + +Sends a message through the specified communication pipeline. + +### Description +This function sends a text `message` through the communication pipeline identified by `pipeID`. + +### Parameters +- `message`: The text message to be sent through the pipeline. +- `pipeID`: The unique ID of the communication pipeline through which the message will be sent. + +### Function Behavior +1. **Encryption Check**: If the communication pipeline (`pipeID`) has an encryption key (`eKey`) associated with it, the provided `message` is encrypted using this key for secure transmission. +2. **Message Transmission**: The encrypted (or plain) message is sent using the `sendRaw` function, specifying the `pipeID` as the receiver and the message type as "MESSAGE". +3. **Success Resolution**: If the message is successfully sent, the function resolves with a success message indicating the successful transmission of the message through the specified pipeline. + +### Returns +- A Promise that resolves with a success message indicating the successful transmission of the message through the specified communication pipeline. + +### Throws +- This function may throw errors if there are issues with message encryption, sending the message, or other communication-related problems. Specific error messages will be provided in the rejection of the returned promise. + +## `processData.pipeline[TYPE_BTC_MULTISIG]` + +This function processes incoming data for the BTC multisig communication pipeline (`pipeID`). It handles different message types and updates the local inbox accordingly. + +### Parameters: +- `pipeID`: Unique identifier of the BTC multisig communication pipeline. + +### Function Behavior: + +1. **Member Verification**: + - Checks if the sender of the incoming message is a valid member of the specified BTC multisig pipeline. If not, the function returns, ignoring the message. + +2. **Data Preparation**: + - `data`: Object to store message information (including `time`, `sender`, `pipeID`, and specific properties). + - `vc`: Stores the vector clock of the incoming message. + - `k`: Retrieves the encryption key (`eKey`) associated with the pipeline for decrypting the incoming message. + +3. **Message Decryption**: + - For "TRANSACTION" messages: Decrypts the encrypted transaction hex (`tx_hex`) and stores it in `data`. + - For "BROADCAST" messages: Decrypts the received transaction ID (`txid`) and compares it with the locally stored initial transaction hex. If consistent, the pipeline is disabled. + - For "MESSAGE" messages: Decrypts the encrypted message and stores it in `data`. + +4. **Data Storage**: + - Adds processed message data to the local database under the `messages` collection, using `pipeID|vc` as the unique identifier. + - If the message contains an encrypted message, it is decrypted and stored back in the `data` object. + +5. **Inbox Update**: + - Adds the processed message data to the `newInbox` object, updating the local inbox for the specified BTC multisig pipeline. + +**Note**: +- The function performs various checks and processes different message types, ensuring secure transmission and consistent handling of transactions and messages within the BTC multisig pipeline. + +## `processData.pipeline[TYPE_FLO_MULTISIG]` + +This function processes incoming data for the FLO multisig communication pipeline (`pipeID`). It handles different message types and updates the local inbox accordingly. + +#### Parameters: +- `pipeID`: Unique identifier of the FLO multisig communication pipeline. + +#### Function Behavior: + +1. **Member Verification**: + - Checks if the sender of the incoming message is a valid member of the specified FLO multisig pipeline. If not, the function returns, ignoring the message. + +2. **Data Preparation**: + - `data`: Object to store message information (including `time`, `sender`, `pipeID`, and specific properties). + - `vc`: Stores the vector clock of the incoming message. + - `k`: Retrieves the encryption key (`eKey`) associated with the pipeline for decrypting the incoming message. + +3. **Message Decryption**: + - For "TRANSACTION" messages: Decrypts the encrypted transaction hex (`tx_hex`) and stores it in `data`. + - For "BROADCAST" messages: Decrypts the received transaction ID (`txid`) and compares it with the locally stored initial and final transaction hexes. If consistent, and the transaction ID matches, the pipeline is disabled. + - For "MESSAGE" messages: Decrypts the encrypted message and stores it in `data`. + +4. **Data Storage**: + - Adds processed message data to the local database under the `messages` collection, using `pipeID|vc` as the unique identifier. + - If the message contains an encrypted message, it is decrypted and stored back in the `data` object. + +5. **Inbox Update**: + - Adds the processed message data to the `newInbox` object, updating the local inbox for the specified FLO multisig pipeline. + +#### Notes: +- The function performs various checks and processes different message types, ensuring secure transmission and consistent handling of transactions and messages within the FLO multisig pipeline. +- The function also compares initial and final transaction hexes and verifies the transaction ID to ensure the integrity of broadcasted transactions. If all checks pass, the pipeline is disabled. + + +### `onLoadStartUp()` + +The `onLoadStartUp` function initializes the application upon startup. It handles various tasks such as setting up UI elements, loading data, and initializing the messenger functionality. + +#### Usage + +```javascript +onLoadStartUp(); +``` + +### Description +This function performs the following steps: +- Route to Loading Page: Redirects the user to the loading page and removes the hidden class from the body element. +- Set Custom Private Key Input: Configures a custom private key input handler for user sign-in. +- Append Emoji Picker Styles: Appends styles for the emoji picker to the shadow root of the emoji picker element. +- Launch Startup Sequence: Invokes the startup functions using floDapps.launchStartUp(). This includes setting user IDs, rendering UI elements, initializing messenger functionalities, and checking for available background images. +- Error Handling: Catches and handles errors that might occur during the startup process. Displays appropriate notifications for specific error cases. + +### Related Functions +- `routeTo(page, options)`: Handles client-side navigation to different pages within the application. +- `getSignedIn()`: Handles the user sign-in process. +- `setBgImage()`: Checks for and sets the background image for the application interface. +- `messenger.init()`: Initializes the messenger functionality and renders chat messages and other UI elements. + + +## `routeTo(targetPage, options = {})` + +This JavaScript function, routeTo(targetPage, options = {}), is an asynchronous function that is used for handling client-side navigation in a web application. It takes two parameters: targetPage, which represents the page to navigate to, and an optional options object with various configuration settings. The function performs different actions based on the specified targetPage and updates the application state and UI accordingly. + +### Function Signature +```javascript +async function routeTo(targetPage, options = {}) { + // function body +} +``` + +### Parameters +- `targetPage`: A string representing the page to navigate to. +- `options`: An object containing configuration settings. In the function, only the firstLoad property is extracted from this object. + +### Function Logic +- Page Identification: The function first checks the targetPage. If it's an empty string, it determines the appropriate pageId based on the user's status (if the user is logged in, it sets pageId to 'chat_page'; otherwise, it sets it to 'landing'). + +- Parsing Target Page: If targetPage is not empty, the function checks if it contains a slash (/). If it does, it splits the targetPage into parts (pageId, subPageId1, and subPageId2). If there is no slash, pageId is set to targetPage. + +- Validation and State Updates: The function validates the pageId and updates the application state (appState.currentPage) accordingly. Depending on the pageId, different actions are taken: + +1. For 'sign_in' page, it focuses on a specific input field. +2. For 'sign_up' page, it generates keys. +3. For 'chat_page', it handles various cases related to chat messages and notifications. +4. For 'mail_page', it handles different mailbox sections. +5. For 'settings', it shows specific panels based on subpage identifiers. +6. For other pages, no specific actions are taken. + +- Navigation and UI Updates: The function updates the browser history (history.replaceState) and animates UI elements based on the page transitions. It also handles the visibility of different UI components based on the current page. + +- Intersection Observer: There is an IntersectionObserver named indicatorObserver that observes elements and performs animations based on their visibility in the viewport. + +### Notes: +- The function uses asynchronous code (await keyword) for handling certain tasks, indicating that it can perform asynchronous operations. +- The function relies on various utility functions (getRef, createElement, showChildElement, etc.) These functions are assumed to be part of the application's codebase and are used for DOM manipulation and UI interactions. + + +## `class LazyLoader` +This class is designed to efficiently handle lazy loading of elements in a web application, meaning it loads elements (such as images or other content) only when they become visible to the user. Let me break down the class and its functionality step by step: + + +### Class Properties: +- container: Represents the HTML element inside which the lazy-loaded content will be placed. +- elementsToRender: A function or an array representing the elements to be lazy-loaded. +- renderFn: A function responsible for rendering individual elements. +- options: An object containing optional configuration settings like batchSize, freshRender, bottomFirst, and onEnd. + +### Class Methods: +- init(): Initializes the lazy loading functionality. It sets up IntersectionObserver and MutationObserver to handle element visibility and DOM changes. +- update(elementsToRender): Updates the array of elements to be lazy-loaded. +- render(options): Renders the elements based on visibility or user scrolling. It calculates the visible range of elements and renders them. +- clear(): Disconnects the IntersectionObserver and MutationObserver and clears the container's content. +- reset(): Resets the LazyLoader by reinitializing the array of elements and rendering them. + +### Class Workflow: +- Initialization (init()): When init() is called, the class sets up an IntersectionObserver to detect when elements come into view and a MutationObserver to watch for changes in the DOM. These observers work together to handle lazy loading efficiently. + +- Updating Elements (update(elementsToRender)): The update() method allows dynamically updating the elements that need to be lazy-loaded. It takes a new set of elements and updates the internal array. + +- Rendering Elements (render(options)): The render() method calculates the range of elements to render based on scrolling or visibility. It then calls the provided renderFn for each element in the calculated range, creating the DOM elements and appending them inside the specified container. If the lazy loading is triggered due to scrolling, it adjusts the scroll position to ensure smooth loading. + +- Clearing (clear()): The clear() method disconnects the observers and clears the content of the specified container. It's useful when you want to remove the lazy-loaded content from the DOM. + +- Resetting (reset()): The reset() method resets the LazyLoader by reinitializing the array of elements and rendering them again from scratch. + +## `getRef(elementId)` +This JavaScript function, getRef(elementId), is a custom utility function designed to improve the efficiency of accessing DOM elements by their IDs. It provides a way to cache references to DOM elements and reuse them, reducing the number of times the actual DOM is queried. + +### Parameters: +`elementId:` The ID of the DOM element that you want to retrieve. + +### Function Logic: +- The function checks if the `elementId` is present in the `domRefs` object. `domRefs` is an external object that serves as a cache for DOM elements. +- If the `elementId` is not present in `domRefs`, a new entry is created for it. This entry contains a count property initialized to 1 (indicating that the element has been accessed once) and a ref property initialized to null. +- If the `elementId` is already in `domRefs`, the function checks the count property: +- If the count is less than 3, it means the element has been accessed less than 3 times. In this case, the count is incremented, and the function returns the element obtained through `document.getElementById(elementId)`. +- If the count is 3 or more, it means the element has been accessed 3 or more times. In this case, the function checks if the ref property is null. If ref is null, it means the DOM element reference hasn't been cached yet. The function then retrieves the element using `document.getElementById(elementId)`, assigns it to `domRefs[elementId].ref`, and returns the cached reference. I +- If ref is not null, it directly returns the cached reference without querying the DOM again. + +### Caching Mechanism: +- The function caches DOM element references in the domRefs object to avoid redundant DOM queries. This caching mechanism helps improve the performance of the application, especially in cases where the same element is accessed multiple times. +### Usage Example: +- Instead of using `document.getElementById(elementId)` directly in the code, developers can use `getRef(elementId)` to access the DOM elements. The function takes care of efficient element retrieval and caching. + +## `debounce(callback, wait)` + +Debouncing is a technique used in web development to ensure that time-consuming tasks (such as API requests or UI updates) are executed only after the user has finished making changes, rather than triggering the task every time an event fires. + +### Function Parameters: + +- **callback**: The function that needs to be executed after the user finishes making changes. This function is passed as the first parameter to `debounce`. +- **wait**: The number of milliseconds to wait after the last invocation of the debounced function before executing `callback`. + +### Function Logic: + +#### Initialization: + +The `debounce` function returns an anonymous function that takes any number of arguments `(...args)`. + +#### Timeout Management: + +1. **Clear Existing Timeout**: + - When the debounced function is called, it first clears any existing timeout by calling `window.clearTimeout(timeoutId)`. This ensures that the execution of the debounced function is reset every time the debounced function is called within the specified `wait` period. + +2. **Set New Timeout**: + - Then, it sets a new timeout using `window.setTimeout(() => { /* callback execution */ }, wait)`. This new timeout will only execute after `wait` milliseconds have passed since the last invocation of the debounced function. + +3. **Callback Execution**: + - Inside the timeout function, the original `callback` function is executed using `callback.apply(null, args)`, where `args` contains the arguments passed to the debounced function. + +### Usage Example: + +```javascript +const debouncedFunction = debounce((arg) => { + console.log(`Debounced function called with argument: ${arg}`); +}, 1000); + +debouncedFunction("First call"); // This will not execute immediately +debouncedFunction("Second call"); // This will not execute immediately +// After 1000ms (1 second), the debounced function will execute with the last provided argument ("Second call") +``` + +## `getLastMessage(floID)` + +The `getLastMessage` function is an asynchronous function that retrieves the last message from a conversation specified by the provided `floID`. + +### Function Signature + +```javascript +function getLastMessage(floID: string): Promise +```` +### Parameters +`floID`: A string representing the unique identifier of the conversation (chat or group) from which to retrieve the last message. +### Returns +A Promise that resolves to an object containing information about the last message in the specified conversation. The object has the following properties: + +- message: The content of the last message. +- time: The timestamp of the last message. +- sender: The sender's ID of the last message. +- category: The category of the last message (e.g., 'sent', 'received'). +- lastText: A formatted string representing the last message for display purposes. + +### Function Logic +The function determines the type of conversation (chat or group) based on the provided floID. +- It retrieves the last message from the conversation using messenger.getChat(floID). +- If the conversation is a group and the last message has a timestamp of 0, it means the group was just created. In this case, the group creation time is used as the last message time. +- The function formats the last message for display purposes, adding sender information (or 'You' for sent messages) and handling blocked conversations appropriately. +- The formatted last message object is returned in the resolved Promise. + +## `delegate` Function Explanation + +The `delegate` function is a utility function in JavaScript used to implement event delegation. Event delegation is a technique where a single event listener is attached to a common ancestor element of multiple child elements. Instead of attaching event listeners to each child element individually, event delegation allows you to handle events for all child elements in a more efficient manner. When an event occurs, it bubbles up from the target element to the ancestor element, where the event listener is placed. The `delegate` function facilitates this pattern. + +### Function Parameters: +- `el`: The common ancestor element to which the event listener is attached. +- `event`: The type of event (e.g., "click", "mouseover", etc.) to listen for. +- `selector`: A CSS selector string specifying the child elements for which the event should be delegated. +- `fn`: The callback function to be executed when the event occurs on a matching child element. + +### Function Logic: +1. The `delegate` function takes in the ancestor element `el`, the event type `event`, the CSS selector `selector` for child elements, and the callback function `fn`. + +2. It attaches an event listener to the ancestor element (`el`) for the specified event type (`event`). + +3. When the event occurs on any descendant element of `el`, the event object (`e`) is passed to the event listener function. + +4. Inside the event listener function, `e.target` represents the element on which the event actually occurred. The `closest` method is used to find the closest ancestor of the target element that matches the specified `selector`. + +5. If a matching ancestor element is found (`potentialTarget`), the `delegateTarget` property is added to the event object (`e`). This property refers to the ancestor element that matches the specified selector. + +6. The callback function `fn` is then executed in the context of the ancestor element (`this`). The event object (`e`) is passed to the callback function, allowing you to handle the event on the matching child element. + +## `getFloIdType` Function + +The `getFloIdType` function is used to determine the type of a given FLO ID in the context of the messenger application. It checks whether the provided FLO ID belongs to a group, a pipeline, or if it is a plain individual FLO ID. + +### Function Parameters: +- `floID`: The FLO ID for which the type needs to be determined. + +### Return Value: +- `'group'`: If the provided FLO ID belongs to a group. +- `'pipeline'`: If the provided FLO ID belongs to a pipeline. +- `'plain'`: If the provided FLO ID is an individual, not associated with a group or pipeline. + +### Function Logic: +1. The `getFloIdType` function takes in a `floID` as a parameter. + +2. It checks if the `floID` exists in the `messenger.groups` object. If it does, the function determines that the `floID` belongs to a group and returns `'group'`. + +3. If the `floID` is not found in `messenger.groups`, the function checks if it exists in the `messenger.pipeline` object. If found, it indicates that the `floID` belongs to a pipeline and returns `'pipeline'`. + +4. If the `floID` is not found in either `messenger.groups` or `messenger.pipeline`, the function concludes that it is a plain individual FLO ID and returns `'plain'`. + +## `renderDirectUI(data)` + +The JavaScript function `renderDirectUI(data)` is designed to handle the rendering of direct messages and mails in a user interface. This function plays a crucial role in updating the UI and providing notifications to the user based on new messages and mails, ensuring a responsive and interactive user experience. + +### Parameters +- `data`: An object containing messages and mails data to be rendered in the UI. + +### Function Logic +1. **New Message Notifications:** + - If there are new messages in the `data.messages` object and the last visited page is not 'chat_page', it updates the document title to indicate the presence of new messages and adds a notification badge to the 'chat_page_button'. + +2. **New Mail Notifications:** + - If there are new mails in the `data.mails` object and the last visited page is not 'mail_page', it updates the document title to indicate the presence of new mails and adds a notification badge to the 'mail_page_button'. + +3. **Notification Panel Updates:** + - It queries the server for uncompleted requests (`messenger.list_request_received({ completed: false })`) and updates the notification badge on the 'notification_panel_button' based on the number of pending requests. + +4. **Message UI and Mail List Rendering:** + - It calls the `updateMessageUI(data.messages)` function to update the UI with new messages. + - It calls the `renderMailList(data.mails, true)` function to render the mail list, passing `true` as a parameter to indicate that these are new mails. + +### Usage Example: +```javascript +// Example usage of the renderDirectUI function +const data = { + messages: { /* ... */ }, // New messages data + mails: { /* ... */ } // New mails data +}; +renderDirectUI(data); +``` + +## `renderMailList(mails, markUnread = true)` + +The JavaScript function `renderMailList(mails, markUnread = true)` is responsible for rendering mail items into the user interface. This function dynamically updates the mail interface, ensuring that mails are displayed in the inbox and sent mail categories. + +### Parameters +- `mails`: An object containing mail data to be rendered in the UI. +- `markUnread` (optional, default: `true`): A boolean indicating whether unread mails should be marked. + +### Function Logic +1. **Initialization:** + - It initializes two DocumentFragments, `inboxMails` and `sentMails`, to store the rendered mail items for inbox and sent mail categories respectively. + - Variables `inboxCount` and `sentCount` are initialized to keep track of the number of unread mails in each category. + +2. **Mail Iteration and Rendering:** + - It iterates through the provided `mails` object and extracts mail details such as sender (`from`), receiver (`to`), subject, time, and content. + - Depending on whether the mail is sent by the user or received by the user, it renders the mail card using the `render.mailCard()` function. If `markUnread` is `true`, it increments the corresponding unread mail count. + - If there are replies (`prev` property), it removes the previous mail card from the rendering to avoid duplicates. + +3. **Notification Badges:** + - It updates notification badges on the mail type selector based on the number of unread mails. If the current mail type selector is 'inbox', it adds the unread count to the 'sent' category badge, and vice versa. + +4. **Rendering into UI:** + - It prepends the rendered inbox mails into the `inbox_mail_container` and sent mails into the `sent_mail_container` in the UI. + +## `getChatCard(floID)` + +This function retrieves the chat card element based on the given FloID (or Bitcoin address) from the chats list. + +#### Parameters + +- `floID` (string): The FloID or Bitcoin address for which the chat card needs to be retrieved. + +#### Returns + +- Returns the chat card element associated with the provided FloID or Bitcoin address. If no matching chat card is found, it returns `null`. + +#### Function logic +- The floID parameter is first converted to a FloID using floCrypto.toFloID(floID). +- The Bitcoin address corresponding to the FloID is obtained using btcOperator.convert.legacy2bech(floID). +- The function queries the chats list using the getRef('chats_list') function. +- It searches for an element with the attribute data-flo-address set to the provided FloID or Bitcoin address. +- If a matching element is found, it is returned. Otherwise, null is returned indicating no matching chat card was found. \ No newline at end of file diff --git a/messenger/docs/product-overview.md b/messenger/docs/product-overview.md new file mode 100644 index 0000000..45321f7 --- /dev/null +++ b/messenger/docs/product-overview.md @@ -0,0 +1,9 @@ +# FLO Messenger + +• Messenger is a blockchain-based decentralized messaging app that uses Bitcoin or FLO blockchain addresses as user identities. Instead of a centralized server, messages are encrypted and stored in the users' browsers. +• Bitcoin or FLO blockchain addresses can communicate with each other using a messaging interface +• Messenger comes with "Multisig" where users can create multi-sig addresses and using a messaging interface make transactions on the multi-sig. +• Switching browsers or devices won't bring back old messages. Remember to back up and import to access your messages in the new browser/device. That's the security of Messenger. + +#### Note: +Do not lose the private key. Copy and save it securely. Once a private key is lost, it cannot be recovered \ No newline at end of file diff --git a/messenger/docs/render.html b/messenger/docs/render.html new file mode 100644 index 0000000..a46b344 --- /dev/null +++ b/messenger/docs/render.html @@ -0,0 +1,41 @@ + + + + + + + + Dynamic List with uhtml + + + +
+ + + + + + diff --git a/messenger/docs/technical-architecture.md b/messenger/docs/technical-architecture.md new file mode 100644 index 0000000..7d2caa5 --- /dev/null +++ b/messenger/docs/technical-architecture.md @@ -0,0 +1,595 @@ +# Messenger Architecture + +### Standard Operations Configuration + + +### `floGlobals Object` +- floGlobals is a standard object used in Standard Operations based App +- floGlobals.adminID is the blockchain based address whose commands all browser clients will follow. Currently it is `floGlobals.adminID: "FMRsefPydWznGWneLqi4ABeQAJeFvtS3aQ"` +- floGlobals.name is "messenger" indicating name of application +- floGlobals.idInterval is the time interval used to switch display between your Bitcoin Address used to log-in, and FLO address +- floGlobals.subadmins are used to do regular access control in the mesenger app. Currently there are none set + +### `processData` object +- Understanding `processData` object is key to understand messenger product +- Overview of processDAta: `processData` is the processing part of messenger. There are kinds of `processData` activities: +1. `processData.direct` - This is used in processing individual mails and messages +2. `processData.group(groupID)` - This is used in processing group messages +3. `processData.pipeline[model](pipeID);` - This is used in processing any pipeline events foor + + +### Product Pipelines +1. The core feature of the product is pipelines. A pipeline is created by invloking inbuilt models +2. Right now we have models for Multisig creation for Bitcoin and FLO Multisigs. +3. What is pipeline ? +• It has an ID +• It has model like TYPE_BTC_MULTISIG +• It has members like different Bitcoin IDs or FLO IDs +• It has an encryption key unique to the pipeline, and known just to members of that pipeline +4. A pipeline sends custom messages defined as per a model to an attached group +5. Pipeline ID could be a recipient of a message. Then every Bitcoin or FLO Address will get the message with the action needed for that pipeline + + + +# Let us specify all DATA DEFINITIONS used in Messenger Application first +## The master object encapsulates all the functionalities + +``` + var obj = { + messages: {}, + mails: {}, + marked: {}, + chats: {}, + groups: {}, + gkeys: {}, + blocked: {}, + pipeline: {}, + request_sent: {}, + request_received: {}, + response_sent: {}, + response_received: {}, + flodata: {}, + appendix: {}, + userSettings: {}, + multisigLabels: {} + } +``` + +## Internal message `types` defined in application for usage in sendApplicationData + +### The key to understand messenger application is to start with message types first + +For sendraw function, `function sendRaw(message, recipient, type, encrypt = null, comment = undefined)` following types are defined +- "MESSAGE" +- "MAIL" +- "REQUEST" +- "RESPONSE" +- "UP_NAME" +- "UP_DESCRIPTION" +- "CREATE GROUP" +- "ADD_MEMBERS" +- "RM_MEMBERS" +- "REVOKE_KEY" +- "GROUP_MSG" +- "TRANSACTION" +- "BROADCAST" +- "CREATE_PIPELINE" + +#### function `sendRaw(message, recipient, type, encrypt = null, comment = undefined)` packages the input parameters into `floCloudAPI.sendApplicationData(message, type, options)` + +- options contain options.receiverID, and options.comment + +```options = { + receiverID: recipient, + } + if (comment) + options.comment = comment +``` +### Message Payload: When a message is sent it has a payload of vectorclock and data. + +``` + sendRaw(message, receiver, "MESSAGE").then(result => { + let vc = result.vectorClock; + let data = { + floID: receiver, + time: result.time, + category: 'sent', + message: encrypt(message) + } +``` + +### Description of Mail Object + +``` +let mail = { + subject: subject, + content: content, + ref: Date.now() + floCrypto.randString(8, true), + prev: prev + } + +let promises = recipients.map(r => sendRaw(JSON.stringify(mail), r, "MAIL")) + +``` + +### SendRequest types +- "PUBLIC_KEY" +- This is used in sendResponse as well + +``` +sendRequest(receiver, "PUBLIC_KEY", message, false); + +``` + +### Understanding GroupInfo +- Contains groupInfo.eKey, groupInfo.admin, groupInfo.members, groupInfo.disabled, groupInfo.name + +### Understanding DataList +- dataList = ["messages", "mails", "marked", "chats", "groups", "gkeys", "pipeline", "blocked", "appendix"] +- defaultList : dataList = ["mails", "marked", "groups", "pipeline", "chats", "blocked", "appendix"] + +### Understanding Mapping between functionalities and sendRaw messages +This section illustrates how functionaility is getting translated into sendRaw feature + +- messenger.changeGroupName = function (groupID, name) +- sendRaw(message, groupID, "UP_NAME", false) + +- messenger.changeGroupDescription = function (groupID, description) +- sendRaw(message, groupID, "UP_DESCRIPTION", false) + +- messenger.addGroupMembers = function (groupID, newMem, note = undefined) +- sendRaw(message, groupID, "ADD_MEMBERS", false, note) + +- messenger.rmGroupMembers = function (groupID, rmMem, note = undefined) +- p1 = sendRaw(message, groupID, "RM_MEMBERS", false, note) + +- messenger.revokeKey = function (groupID) +- groupInfo.members.map(m => sendRaw(JSON.stringify({ + newKey, + groupID + }), m, "REVOKE_KEY", true)) + +- messenger.sendGroupMessage = function (message, groupID) +- sendRaw(message, groupID, "GROUP_MSG", false) + +- message = encrypt(tx_hex, pipeline.eKey); +- sendRaw(message, pipeline.id, "TRANSACTION", false) + +- btcOperator.broadcastTx(tx_hex_signed) +- sendRaw(encrypt(txid, pipeline.eKey), pipeline.id, "BROADCAST", false) + +- messenger.createPipeline = function (model, members, ekeySize = 16, pubkeys = null) +- sendRaw(pipelineInfo, m, "CREATE_PIPELINE", true)); + +- messenger.sendPipelineMessage = function (message, pipeID) +- sendRaw(message, pipeID, "MESSAGE", false) + +### Understanding appendix +It has two usages in the application +- appendix.lastReceived to compare against what vectorClock data we have +- appendix.AESKey to check what encryption keys to be used + +### compactIDB.addData and compactIDB.writeData + +- The addData function is used to add new records to an object store. It will throw an error if the key already exists +- The writeData function is used to update or insert data into an object store. It will update if the key already exists + +### Why do we use uhtml + +uhtml is a small, efficient, and ultra-fast JavaScript library for creating web components and managing the DOM (Document Object Model). It allows developers to write templates directly in JavaScript without needing a separate build step. uhtml focuses on minimalism and performance, aiming to provide a lightweight solution for rendering dynamic content in web applications. + +## Here's a brief overview of what uhtml does: + +- Template Rendering: uhtml provides a way to create HTML templates directly in JavaScript code. These templates can include dynamic content and data-binding expressions. + +- Virtual DOM: uhtml utilizes a virtual DOM approach, where changes to the UI are first made in memory (virtual DOM), and then efficiently applied to the actual DOM, reducing unnecessary reflows and repaints. + +- Efficiency: uhtml is designed to be incredibly fast and efficient. It achieves this by minimizing the overhead associated with creating and updating DOM elements. + +- Reactivity: uhtml templates can be reactive, meaning they can automatically update when the underlying data changes. This reactivity is often used in modern web frameworks to create responsive and dynamic user interfaces. + +## Explain the UI process + +### `getRef(elementId)` +getRef is the cornerstone of UI in Messenger App. getRef(elementId), is a custom utility function designed to improve the efficiency of accessing DOM elements by their IDs. It provides a way to cache references to DOM elements and reuse them, reducing the number of times the actual DOM is queried. Instead of using `document.getElementById(elementId)` directly in the code, we use `getRef(elementId)` to access the DOM elements. The function takes care of efficient element retrieval and caching. + +getRef(elementId) Function: + +1. getRef(elementId) is a custom JavaScript function used to obtain references to DOM elements using their unique IDs. It optimizes the process of accessing DOM elements by caching the references, allowing for efficient reuse without the need to repeatedly query the DOM. When you call getRef('mail_contact_list'), it returns the DOM element with the ID 'mail_contact_list'. + +2. We use getRef(elementId) instead of document.getElementById + +### uhtml Library Usage: + +1. uhtml is a JavaScript library for efficient and lightweight HTML templating and rendering. It allows you to create HTML templates directly in your JavaScript code. + +2. The line const { html, render: renderElem } = uhtml; imports the html function and an alias for the render function from the uhtml library. The html function is used to create HTML template literals, and renderElem is used to render those templates into DOM elements efficiently. + +- Rendering HTML with renderElem(): + +1. The code renderElem(getRef('mail_contact_list'), html${mailingContacts}) combines the previously mentioned concepts. Here's what's happening: + +2. html${mailingContacts} creates an HTML template literal using the uhtml library. It represents the HTML structure defined by the mailingContacts variable. + +3. getRef('mail_contact_list') retrieves the DOM element with the ID 'mail_contact_list' using the custom getRef() function. + +4. renderElem(targetElement, htmlTemplate) is a call to the renderElem function from the uhtml library. It takes two parameters: the target DOM element where the rendered HTML will be appended (getRef('mail_contact_list') in this case) and the HTML template created with the html function. + +### A direct simple example explaining the render logic + +``` + + + + + + + Dynamic List with uhtml + + + +
+ + + + + + +``` + +### Functions created for Rendering + + +``` + messenger.renderUI.chats = renderChatList; + messenger.renderUI.directChat = renderDirectUI; + messenger.renderUI.groupChat = renderGroupUI; + messenger.renderUI.pipeline = renderPipelineUI; + messenger.renderUI.mails = m => renderMailList(m, false); + messenger.renderUI.marked = renderMarked; +``` + +### There are two meanings of render in the UI of messenger application + +1. In class lazyloader, this.render() is a method internal to lazyloader based objects +2. In uhtml invocations, render creates actual html when `html` parameter is fed to it +3. Sometimes in apps like BTCWallet, render is an object with collection of functions like addressDetails, and txDetails. However, its not used in messenger. + +### Example of uhtml html creation + +- METHOD 1: render.something () In the code below function takes floID as input parameter, and return uhtml compatible html. This can then be fed to `render.selectableContact(contact)` for actual HTML creation + + +``` +//HTML creation stage +selectableContact(floID) { + const name = getContactName(floID) + const initial = name.charAt(0) + return html` + + ` + } + +//At render stage +render.selectableContact(contact); +``` + +- METHOD 2: renderElem(getRef,html) method. Another way we have used to to achieve html from render is the code snippet below using renderElem function + +``` + const { html, render: renderElem } = uhtml; + + function renderContactList(contactList = floGlobals.contacts) { + const contacts = Object.keys(contactList) + .sort((a, b) => getContactName(a).localeCompare(getContactName(b))) + .map(floID => { + const isSelected = selectedMembers.has(floID) + return render.contactCard(floID, { type: 'contact', isSelected, ref: getRef('contacts_container') }) + }) + renderElem(getRef('contacts_container'), html`${contacts}`) + } +``` +## How do you create new models + +- Lets look at usage of createPipeline function which uses `TYPE_BTC_MULTISIG` model here. + +``` + createPipeline(TYPE_BTC_MULTISIG, co_owners, 32, decode.pubkeys).then(pipeline => { + let message = encrypt(tx_hex, pipeline.eKey); + sendRaw(message, pipeline.id, "TRANSACTION", false) + .then(result => resolve(pipeline.id)) + .catch(error => reject(error)) //SENDRAW + }).catch(error => reject(error)) //CREATE PIPELINE +``` + +- To design a new model, you need to define the states the pipeline in the model will take, and also define the message types that will move the state forward. +- A model tells what are the types of messages in that system, and how should they be handled. There is no explicit definition of model. Rather we straight define the processing logic as part of processData.pipeLine function, and it initiates the model. +- You need to define what are the type of messages in your model. For instance the message types for `TYPE_BTC_MULTISIG` model are "TRANSACTION", "BROADCAST" and "MESSAGE". +- If the model message type is "TRANSACTION", then it needs to be signed by anyone of the person who sees this first, and then needs to sign the BTC Multisig transaction. +- If the model message type is "BROADCAST", then all signatures are received, and multisig messaeg has to be broadcasted to the blockchain. +- If the model message type is "MESSAGE", then simply show the message to all members in the pipeline +- Similarly as part of model design, you need to identify the types of messages for the models first. +- Then you need to create the processing logic for each type of message in processData.pipeline[MODEL_NAME]. It will have a common processing section and switch case for each of the message type +- A pipelines moves its state forward as the message type changes due to action of members. +- The model processing logic specified how the state of pipeline changes. +- In this example the state of pipeline changes as soon as one member performs an action which leads him client to proclaim the state of pipeline has moved to the next message +- States in TYPE_BTC_MULTISIG model are Create -> Transact -> Transact -> Broadcast -> close if it is 2 of 3 BTC Multisig +- After the processing, the message has to be given to inboxes of correct recipients like using `newInbox.messages[vc] = data;` +- You should also store a copy of that message locally like using `compactIDB.addData("messages", Object.assign({}, data),${pipeID}|${vc});` +- Every pipeline is identified by a system generated pipeID which is made available to creator and members when a new pipleline is created + +``` +//pipeline model for btc multisig + processData.pipeline[TYPE_BTC_MULTISIG] = function (pipeID) { + return (unparsed, newInbox) => { + if (!_loaded.pipeline[pipeID].members.includes(floCrypto.toFloID(unparsed.senderID))) + return; + let data = { + time: unparsed.time, + sender: unparsed.senderID, + pipeID: unparsed.receiverID + } + let vc = unparsed.vectorClock, + k = _loaded.pipeline[pipeID].eKey; + unparsed.message = decrypt(unparsed.message, k) + //store the pubKey if not stored already + floDapps.storePubKey(unparsed.senderID, unparsed.pubKey); + data.type = unparsed.type; + switch (unparsed.type) { + case "TRANSACTION": { + data.tx_hex = unparsed.message; + break; + } + case "BROADCAST": { + data.txid = unparsed.message; + //the following check is done on parallel (in background) instead of sync + btcOperator.getTx.hex(data.txid).then(tx_hex_final => { + getChat(pipeID).then(result => { + let tx_hex_inital = Object.keys(result).sort().map(i => result[i].tx_hex).filter(x => x).shift(); + if (btcOperator.checkIfSameTx(tx_hex_inital, tx_hex_final)) + disablePipeline(pipeID); + }).catch(error => console.error(error)) + }).catch(error => console.error(error)) + break; + } + case "MESSAGE": { + data.message = encrypt(unparsed.message); + break; + } + } + compactIDB.addData("messages", Object.assign({}, data), `${pipeID}|${vc}`); + if (data.message) + data.message = decrypt(data.message); + newInbox.messages[vc] = data; + } + } +``` + +## Ok. The model is created. How do I use it + +- After a model has been created in processData.pipeline[YOUR_MODEL_NAME] with pipeID as the input parameter, the first action is creation of pipeline using that model. +- Pipeline creation is a standard function, and as new model creator, the system does it all for you. All you have to do is to use messenger.createPipeline with model parameter as YOUR_MODEL_NAME. +- Pipeline creation does not need a special message type. It will be created by the initiator, and sent as messages to everyone who is member of that pipeline. +- However once the message has been given to all users, a valid action from any of them can move the state of pipeline forward, and your model processing logic has to handle what do do next. +- Such sequence of actions has to be defined till the state reaches closure. +- You can also define role based messages in model processing logic, and assign those roles to specific Bitcoin or FLO IDs. Each of them will need their own message type. Switch case of message type has has handle the sequencing of hange of state. +- Role based implementation was not needed in Multisig cases. But it could be created if use case emerges by expanding the processing states. +- In theory role definition can be done at model stage. But role allocation will need the creator to act, and that would mean that createPipeline function will need an expansion. + +``` +const createPipeline = messenger.createPipeline = function (model, members, ekeySize = 16, pubkeys = null) { + return new Promise((resolve, reject) => { + //optional pubkey parameter + if (pubkeys !== null) { + if (!Array.isArray(pubkeys)) + return reject('pubkeys must be an array (if passed)'); + else if (pubkeys.length !== members.length) + return reject('pubkey length doesnot match members length'); + } + + //validate members + let imem1 = [], + imem2 = [] + members.forEach((m, i) => { + if (!floCrypto.validateAddr(m)) + imem1.push(m); + else if (!(m in floGlobals.pubKeys) && !floCrypto.isSameAddr(user.id, m)) { + if (pubkeys !== null && floCrypto.verifyPubKey(pubkeys[i], m)) + floGlobals.pubKeys[m] = pubkeys[i]; + else + imem2.push(m); + } + }); + if (imem1.length) + return reject(`Invalid Members(floIDs): ${imem1}`); + else if (imem2.length) + return reject(`Invalid Members (pubKey not available): ${imem2}`); + //create pipeline info + const id = floCrypto.tmpID; + let pipeline = { + id, + model, + members + } + if (ekeySize) + pipeline.eKey = floCrypto.randString(ekeySize); + //send pipeline info to members + let pipelineInfo = JSON.stringify(pipeline); + let promises = members.filter(m => !floCrypto.isSameAddr(m, user.id)).map(m => sendRaw(pipelineInfo, m, "CREATE_PIPELINE", true)); + Promise.allSettled(promises).then(results => { + console.debug(results.filter(r => r.status === "rejected").map(r => r.reason)); + _loaded.pipeline[pipeline.id] = Object.assign({}, pipeline); + if (pipeline.eKey) + pipeline.eKey = encrypt(pipeline.eKey); + compactIDB.addData("pipeline", pipeline, pipeline.id).then(result => { + requestPipelineInbox(pipeline.id, pipeline.model); + resolve(_loaded.pipeline[pipeline.id]) + }).catch(error => reject(error)) + }) + }) + } + + +``` + +## Explain me the full cycle of pipeline creation process + +- TYPE_BTC_MULTISIG pipeline is created in MultiSig.createTx_BTC function. And as part of creation process, the creator performs the first signature at `tx_hex = btcOperator.signTx(tx_hex, privateKey)`, and the proceeds to create the pipeline. So the pipleine starts at "TRANSACT" stage itself as seen in `sendRaw(message, pipeline.id, "TRANSACTION", false)` message. + +``` +MultiSig.createTx_BTC = function (address, redeemScript, receivers, amounts, fee = null, options = {}) { + return new Promise(async (resolve, reject) => { + let addr_type = btcOperator.validateAddress(address); + if (addr_type != "multisig" && addr_type != "multisigBech32") + return reject("Sender address is not a multisig"); + let decode = (addr_type == "multisig" ? + coinjs.script().decodeRedeemScript : coinjs.script().decodeRedeemScriptBech32)(redeemScript); + if (!decode || decode.address !== address || decode.type !== "multisig__") + return reject("Invalid redeem-script"); + else if (!decode.pubkeys.includes(user.public.toLowerCase()) && !decode.pubkeys.includes(user.public.toUpperCase())) + return reject("User is not a part of this multisig"); + else if (decode.pubkeys.length < decode.signaturesRequired) + return reject("Invalid multisig (required is greater than users)"); + let co_owners = decode.pubkeys.map(p => floCrypto.getFloID(p)); + let privateKey = await floDapps.user.private; + btcOperator.createMultiSigTx(address, redeemScript, receivers, amounts, fee, options).then(({ tx_hex }) => { + tx_hex = btcOperator.signTx(tx_hex, privateKey); + createPipeline(TYPE_BTC_MULTISIG, co_owners, 32, decode.pubkeys).then(pipeline => { + let message = encrypt(tx_hex, pipeline.eKey); + sendRaw(message, pipeline.id, "TRANSACTION", false) + .then(result => resolve(pipeline.id)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + +``` + +- Similarly all role based definitions can be created at initial `MultiSig.createTx_BTC` equivalent function at pipeline creation stage. The initial start is needed here. Then the processing logic of model will dictate how will the state move forward. +- Right now all members are flat. But a system can be created where the message goes to a specific role. + +### Callback functions used + +- callbackFn are used to push data in localIDB and invoke new UI after main fetching of data has been done from Supernodes + +- In requestDirectInbox() + +``` +let callbackFn = function (dataSet, error) { + if (error) + return console.error(error) + let newInbox = { + messages: {}, + requests: {}, + responses: {}, + mails: {}, + newgroups: [], + keyrevoke: [], + pipeline: {} + } + for (let vc in dataSet) { + try { + parseData(dataSet[vc], newInbox); + } catch (error) { + //if (error !== "blocked-user") + console.log(error); + } finally { + if (_loaded.appendix.lastReceived < vc) + _loaded.appendix.lastReceived = vc; + } + } + compactIDB.writeData("appendix", _loaded.appendix.lastReceived, "lastReceived"); + console.debug(newInbox); + UI.direct(newInbox) + } +``` + +- In `requestGroupInbox(groupID, _async = true)` + +``` +let callbackFn = function (dataSet, error) { + if (error) + return console.error(error) + console.info(dataSet) + let newInbox = { + messages: {} + } + let infoChange = false; + for (let vc in dataSet) { + if (groupID !== dataSet[vc].receiverID) + continue; + try { + infoChange = parseData(dataSet[vc], newInbox) || infoChange; + if (!_loaded.appendix[`lastReceived_${groupID}`] || + _loaded.appendix[`lastReceived_${groupID}`] < vc) + _loaded.appendix[`lastReceived_${groupID}`] = vc; + } catch (error) { + console.log(error) + } + } + compactIDB.writeData("appendix", _loaded.appendix[`lastReceived_${groupID}`], `lastReceived_${groupID}`); + if (infoChange) { + let newInfo = Object.assign({}, _loaded.groups[groupID]); + newInfo.eKey = encrypt(newInfo.eKey) + compactIDB.writeData("groups", newInfo, groupID) + } + console.debug(newInbox); + UI.group(newInbox); + } +``` + +- In `requestPipelineInbox(pipeID, model, _async = true)` + +``` +let callbackFn = function (dataSet, error) { + if (error) + return console.error(error); + console.info(dataSet) + let newInbox = { + messages: {} + } + for (let vc in dataSet) { + if (pipeID !== dataSet[vc].receiverID) + continue; + try { + parseData(dataSet[vc], newInbox); + if (!floCrypto.isSameAddr(dataSet[vc].senderID, user.id)) + addMark(pipeID, "unread") + if (!_loaded.appendix[`lastReceived_${pipeID}`] || + _loaded.appendix[`lastReceived_${pipeID}`] < vc) + _loaded.appendix[`lastReceived_${pipeID}`] = vc; + } catch (error) { + console.log(error) + } + } + compactIDB.writeData("appendix", _loaded.appendix[`lastReceived_${pipeID}`], `lastReceived_${pipeID}`); + console.debug(newInbox); + UI.pipeline(model, newInbox); + } +``` diff --git a/messenger/docs/usage.md b/messenger/docs/usage.md new file mode 100644 index 0000000..889b222 --- /dev/null +++ b/messenger/docs/usage.md @@ -0,0 +1,38 @@ +## How to use Messenger +### General messaging +1. Go to the homepage of Messenger +2. Sign in using a Bitcoin or FLO blockchain private key +3. In case you don't have the private key, generate using + FLO Wallet (for FLO address and private key): https://ranchimall.github.io/flowallet/ + BTC Wallet (for Bitcoin address and private key): https://ranchimall.github.io/btcwallet/ +** Note: FLO address or FLO ID and private key can be created from Messenger's homepage as well +4. To start a new message or chat, click on the "New chat" button +5. Add a FLO ID or a Bitcoin address as a contact +6. Select the contact to start messaging +** Note: Until the receiver replies, the message is not encrypted. + +### Mail +1. Mail is similar to Messaging except the user can send messages to multiple FLO IDs or Bitcoin addresses at the same time +2. Go to "Mail" and enter the recipient's FLO or Bitcoin address +3. Separate multiple addresses with a comma +4. Type a mail and send + +### Multisig messaging +1. Go to "Multisig" on the homepage +2. To create a Bitcoin multisig, click on "BTC" +3. To create a FLO multisig, click on "FLO" +4. To add BTC or FLO addresses in the new multisig, select contacts that are to be added +5. Contacts have to be saved in advance before creating a multisig address +6. After selecting the contacts, click "Next" and give the multisig address a label name +7. Select the minimum number of signatures required for the multisig +8. Click "Create" and the multisig address will be created + +### Sending a multisig transaction +1. The user must have some balance in the multisig address +2. Go to "Multisig" and click on "init transaction" +3. Enter the receiver's BTC address for a Bitcoin multisig or FLO address for a FLO multisig +4. Enter the amount to be transferred +5. Multiple addresses can be added as receivers with different amounts for each address +6. Click on "Initiate" to initiate the transaction from the multisig address +7. Associated multisig owners will be notified of this transaction +8. Once the required number of signatures is approved, the transaction will take place from the multisig address diff --git a/messenger/index.html b/messenger/index.html new file mode 100644 index 0000000..8343677 --- /dev/null +++ b/messenger/index.html @@ -0,0 +1,5237 @@ + + + + + + + RanchiMall Messenger + + + + + + + + + + + + + + + + + + + + + + + +

+

+
+ + +
+
+ +

+

+ + +
+ + +
+
+
+
+ +
+
+
+
+
+ + +
+ + Chat + Multisig + Requests + +
+
+
+
+ + + + + + + + + Create new group + + +
+
+
+ + + + +

Start your first conversation

+

Tap/click on 'New chat' to add or select a contact.

+
+ +
+ + +
+
+ +
+
+

Chat, Mail and use Bitcoin Multisig

+
+
+ + + +
+ +
+ + + + + + + + + + + +

You can send mail to multiple FLO addresses by entering commas

+
+ + +
+ + +
+ +
+
+
+ + + + + + + + + + + +
+
+ + + +
+
+ +
+ + +
+
+
+

No saved contacts

+

Use 'Add contact' to add new FLO/BTC/ETH address as a contact.

+
+
+
+
+ + +
+
+
+
+

*Contacts that haven't yet replied to you, can't be selected. you + can send a request or message them.

+
+
+

Select members

+ +
+ +
+
+
+
+
+

No saved contacts.

+

Use 'Add contact' to add new FLO/BTC/ETH address as a contact.

+ +
+
+
+ + +
+
+ + + + + +

*Contacts that haven't yet replied to you, can't be added to a group. So they + won't be + visible here.

+ +
+

You don't have any other contacts to show.

+
+
+ + + + + + + + + + +
+
+ + + + +
+ Selected multisig address +

+
+
+
+ Enter receiver(s) +
+ +
+
+
+ +
+
+
+ + +
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/messenger/scripts/btcOperator.js b/messenger/scripts/btcOperator.js new file mode 100644 index 0000000..b0e8f6d --- /dev/null +++ b/messenger/scripts/btcOperator.js @@ -0,0 +1,1135 @@ +(function (EXPORTS) { //btcOperator v1.1.3c + /* BTC Crypto and API Operator */ + const btcOperator = EXPORTS; + + //This library uses API provided by chain.so (https://chain.so/) + const URL = "https://blockchain.info/"; + + const DUST_AMT = 546, + MIN_FEE_UPDATE = 219; + + const fetch_api = btcOperator.fetch = function (api, json_res = true) { + return new Promise((resolve, reject) => { + console.debug(URL + api); + fetch(URL + api).then(response => { + if (response.ok) { + (json_res ? response.json() : response.text()) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + response.json() + .then(result => reject(result)) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + }; + + const SATOSHI_IN_BTC = 1e8; + + const util = btcOperator.util = {}; + + util.Sat_to_BTC = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8)); + util.BTC_to_Sat = value => parseInt(value * SATOSHI_IN_BTC); + + function get_fee_rate() { + return new Promise((resolve, reject) => { + fetch('https://api.blockchain.info/mempool/fees').then(response => { + if (response.ok) + response.json() + .then(result => resolve(util.Sat_to_BTC(result.regular))) + .catch(error => reject(error)); + else + reject(response); + }).catch(error => reject(error)) + }) + } + + const broadcastTx = btcOperator.broadcastTx = rawTxHex => new Promise((resolve, reject) => { + let url = 'https://coinb.in/api/?uid=1&key=12345678901234567890123456789012&setmodule=bitcoin&request=sendrawtransaction'; + fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: "rawtx=" + rawTxHex + }).then(response => { + response.text().then(resultText => { + let r = resultText.match(/.*<\/result>/); + if (!r) + reject(resultText); + else { + r = r.pop().replace('', '').replace('', ''); + if (r == '1') { + let txid = resultText.match(/.*<\/txid>/).pop().replace('', '').replace('', ''); + resolve(txid); + } else if (r == '0') { + let error = resultText.match(/.*<\/response>/).pop().replace('', '').replace('', ''); + reject(decodeURIComponent(error.replace(/\+/g, " "))); + } else reject(resultText); + } + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }); + + Object.defineProperties(btcOperator, { + newKeys: { + get: () => { + let r = coinjs.newKeys(); + r.segwitAddress = coinjs.segwitAddress(r.pubkey).address; + r.bech32Address = coinjs.bech32Address(r.pubkey).address; + return r; + } + }, + pubkey: { + value: key => key.length >= 66 ? key : (key.length == 64 ? coinjs.newPubkey(key) : coinjs.wif2pubkey(key).pubkey) + }, + address: { + value: (key, prefix = undefined) => coinjs.pubkey2address(btcOperator.pubkey(key), prefix) + }, + segwitAddress: { + value: key => coinjs.segwitAddress(btcOperator.pubkey(key)).address + }, + bech32Address: { + value: key => coinjs.bech32Address(btcOperator.pubkey(key)).address + }, + bech32mAddress: { + value: key => segwit_addr.encode("bc", 1, key) + } + }); + + coinjs.compressed = true; + + const verifyKey = btcOperator.verifyKey = function (addr, key) { + if (!addr || !key) + return undefined; + switch (coinjs.addressDecode(addr).type) { + case "standard": + return btcOperator.address(key) === addr; + case "multisig": + return btcOperator.segwitAddress(key) === addr; + case "bech32": + return btcOperator.bech32Address(key) === addr; + case "bech32m": + return btcOperator.bech32mAddress(key) === addr; // Key is a byte array of 32 bytes + default: + return null; + } + } + + const validateAddress = btcOperator.validateAddress = function (addr) { + if (!addr) + return undefined; + let type = coinjs.addressDecode(addr).type; + if (["standard", "multisig", "bech32", "multisigBech32", "bech32m"].includes(type)) + return type; + else + return false; + } + + btcOperator.multiSigAddress = function (pubKeys, minRequired, bech32 = true) { + if (!Array.isArray(pubKeys)) + throw "pubKeys must be an array of public keys"; + else if (pubKeys.length < minRequired) + throw "minimum required should be less than the number of pubKeys"; + if (bech32) + return coinjs.pubkeys2MultisigAddressBech32(pubKeys, minRequired); + else + return coinjs.pubkeys2MultisigAddress(pubKeys, minRequired); + } + + btcOperator.decodeRedeemScript = function (redeemScript, bech32 = true) { + let script = coinjs.script(); + let decoded = (bech32) ? + script.decodeRedeemScriptBech32(redeemScript) : + script.decodeRedeemScript(redeemScript); + if (!decoded) + return null; + return { + address: decoded.address, + pubKeys: decoded.pubkeys, + redeemScript: decoded.redeemscript, + required: decoded.signaturesRequired + } + + } + + //convert from one blockchain to another blockchain (target version) + btcOperator.convert = {}; + + btcOperator.convert.wif = function (source_wif, target_version = coinjs.priv) { + let keyHex = util.decodeLegacy(source_wif).hex; + if (!keyHex || keyHex.length < 66 || !/01$/.test(keyHex)) + return null; + else + return util.encodeLegacy(keyHex, target_version); + } + + btcOperator.convert.legacy2legacy = function (source_addr, target_version = coinjs.pub) { + let rawHex = util.decodeLegacy(source_addr).hex; + if (!rawHex) + return null; + else + return util.encodeLegacy(rawHex, target_version); + } + + btcOperator.convert.legacy2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) { + let rawHex = util.decodeLegacy(source_addr).hex; + if (!rawHex) + return null; + else + return util.encodeBech32(rawHex, target_version, target_hrp); + } + + btcOperator.convert.bech2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) { + let rawHex = util.decodeBech32(source_addr).hex; + if (!rawHex) + return null; + else + return util.encodeBech32(rawHex, target_version, target_hrp); + } + + btcOperator.convert.bech2legacy = function (source_addr, target_version = coinjs.pub) { + let rawHex = util.decodeBech32(source_addr).hex; + if (!rawHex) + return null; + else + return util.encodeLegacy(rawHex, target_version); + } + + btcOperator.convert.multisig2multisig = function (source_addr, target_version = coinjs.multisig) { + let rawHex = util.decodeLegacy(source_addr).hex; + if (!rawHex) + return null; + else + return util.encodeLegacy(rawHex, target_version); + } + + btcOperator.convert.bech2multisig = function (source_addr, target_version = coinjs.multisig) { + let rawHex = util.decodeBech32(source_addr).hex; + if (!rawHex) + return null; + else { + rawHex = Crypto.util.bytesToHex(ripemd160(Crypto.util.hexToBytes(rawHex), { asBytes: true })); + return util.encodeLegacy(rawHex, target_version); + } + } + + util.decodeLegacy = function (source) { + var decode = coinjs.base58decode(source); + var raw = decode.slice(0, decode.length - 4), + checksum = decode.slice(decode.length - 4); + var hash = Crypto.SHA256(Crypto.SHA256(raw, { + asBytes: true + }), { + asBytes: true + }); + if (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3]) + return false; + let version = raw.shift(); + return { + version: version, + hex: Crypto.util.bytesToHex(raw) + } + } + + util.encodeLegacy = function (hex, version) { + var bytes = Crypto.util.hexToBytes(hex); + bytes.unshift(version); + var hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + return coinjs.base58encode(bytes.concat(checksum)); + } + + util.decodeBech32 = function (source) { + let decode = coinjs.bech32_decode(source); + if (!decode) + return false; + var raw = decode.data; + let version = raw.shift(); + raw = coinjs.bech32_convert(raw, 5, 8, false); + return { + hrp: decode.hrp, + version: version, + hex: Crypto.util.bytesToHex(raw) + } + } + + util.encodeBech32 = function (hex, version, hrp) { + var bytes = Crypto.util.hexToBytes(hex); + bytes = coinjs.bech32_convert(bytes, 8, 5, true); + bytes.unshift(version) + return coinjs.bech32_encode(hrp, bytes); + } + + //BTC blockchain APIs + + btcOperator.getBalance = addr => new Promise((resolve, reject) => { + fetch_api(`q/addressbalance/${addr}`) + .then(result => resolve(util.Sat_to_BTC(result))) + .catch(error => reject(error)) + }); + + const BASE_TX_SIZE = 12, + BASE_INPUT_SIZE = 41, + LEGACY_INPUT_SIZE = 107, + BECH32_INPUT_SIZE = 27, + BECH32_MULTISIG_INPUT_SIZE = 35, + SEGWIT_INPUT_SIZE = 59, + MULTISIG_INPUT_SIZE_ES = 351, + BASE_OUTPUT_SIZE = 9, + LEGACY_OUTPUT_SIZE = 25, + BECH32_OUTPUT_SIZE = 23, + BECH32_MULTISIG_OUTPUT_SIZE = 34, + SEGWIT_OUTPUT_SIZE = 23; + BECH32M_OUTPUT_SIZE = 35; // Check this later + + function _redeemScript(addr, key) { + let decode = coinjs.addressDecode(addr); + switch (decode.type) { + case "standard": + return false; + case "multisig": + return key ? coinjs.segwitAddress(btcOperator.pubkey(key)).redeemscript : null; + case "bech32": + return decode.redeemscript; + case "'multisigBech32": + return decode.redeemscript; //Multisig-edit-fee-change1 + case "bech32m": + return decode.outstring; //Maybe the redeemscript will come when input processing happens for bech32m + default: + return null; + } + } + btcOperator._redeemScript = _redeemScript; + + function _sizePerInput(addr, rs) { + switch (coinjs.addressDecode(addr).type) { + case "standard": + return BASE_INPUT_SIZE + LEGACY_INPUT_SIZE; + case "bech32": + return BASE_INPUT_SIZE + BECH32_INPUT_SIZE; + case "multisigBech32": + return BASE_INPUT_SIZE + BECH32_MULTISIG_INPUT_SIZE; + case "multisig": + switch (coinjs.script().decodeRedeemScript(rs).type) { + case "segwit__": + return BASE_INPUT_SIZE + SEGWIT_INPUT_SIZE; + case "multisig__": + return BASE_INPUT_SIZE + MULTISIG_INPUT_SIZE_ES; + default: + return null; + }; + default: + return null; + } + } + + function _sizePerOutput(addr) { + switch (coinjs.addressDecode(addr).type) { + case "standard": + return BASE_OUTPUT_SIZE + LEGACY_OUTPUT_SIZE; + case "bech32": + return BASE_OUTPUT_SIZE + BECH32_OUTPUT_SIZE; + case "multisigBech32": + return BASE_OUTPUT_SIZE + BECH32_MULTISIG_OUTPUT_SIZE; + case "multisig": + return BASE_OUTPUT_SIZE + SEGWIT_OUTPUT_SIZE; + case "bech32m": + return BASE_OUTPUT_SIZE + BECH32M_OUTPUT_SIZE; + default: + return null; + } + } + + function validateTxParameters(parameters) { + let invalids = []; + //sender-ids + if (parameters.senders) { + if (!Array.isArray(parameters.senders)) + parameters.senders = [parameters.senders]; + parameters.senders.forEach(id => !validateAddress(id) ? invalids.push(id) : null); + if (invalids.length) + throw "Invalid senders:" + invalids; + } + if (parameters.privkeys) { + if (!Array.isArray(parameters.privkeys)) + parameters.privkeys = [parameters.privkeys]; + if (parameters.senders.length != parameters.privkeys.length) + throw "Array length for senders and privkeys should be equal"; + parameters.senders.forEach((id, i) => { + let key = parameters.privkeys[i]; + if (!verifyKey(id, key)) //verify private-key + invalids.push(id); + if (key.length === 64) //convert Hex to WIF if needed + parameters.privkeys[i] = coinjs.privkey2wif(key); + }); + if (invalids.length) + throw "Invalid private key for address:" + invalids; + } + //receiver-ids (and change-id) + if (!Array.isArray(parameters.receivers)) + parameters.receivers = [parameters.receivers]; + parameters.receivers.forEach(id => !validateAddress(id) ? invalids.push(id) : null); + if (invalids.length) + throw "Invalid receivers:" + invalids; + if (parameters.change_address && !validateAddress(parameters.change_address)) + throw "Invalid change_address:" + parameters.change_address; + //fee and amounts + if ((typeof parameters.fee !== "number" || parameters.fee <= 0) && parameters.fee !== null) //fee = null (auto calc) + throw "Invalid fee:" + parameters.fee; + if (!Array.isArray(parameters.amounts)) + parameters.amounts = [parameters.amounts]; + if (parameters.receivers.length != parameters.amounts.length) + throw "Array length for receivers and amounts should be equal"; + parameters.amounts.forEach(a => typeof a !== "number" || a <= 0 ? invalids.push(a) : null); + if (invalids.length) + throw "Invalid amounts:" + invalids; + //return + return parameters; + } + btcOperator.validateTxParameters = validateTxParameters; + + function createTransaction(senders, redeemScripts, receivers, amounts, fee, change_address, fee_from_receiver) { + return new Promise((resolve, reject) => { + let total_amount = parseFloat(amounts.reduce((t, a) => t + a, 0).toFixed(8)); + const tx = coinjs.transaction(); + let output_size = addOutputs(tx, receivers, amounts, change_address); + addInputs(tx, senders, redeemScripts, total_amount, fee, output_size, fee_from_receiver).then(result => { + if (result.change_amount > 0 && result.change_amount > result.fee) //add change amount if any (ignore dust change) + tx.outs[tx.outs.length - 1].value = util.BTC_to_Sat(result.change_amount); //values are in satoshi + if (fee_from_receiver) { //deduce fee from receivers if fee_from_receiver + let fee_remaining = util.BTC_to_Sat(result.fee); + for (let i = 0; i < tx.outs.length - 1 && fee_remaining > 0; i++) { + if (fee_remaining < tx.outs[i].value) { + tx.outs[i].value -= fee_remaining; + fee_remaining = 0; + } else { + fee_remaining -= tx.outs[i].value; + tx.outs[i].value = 0; + } + } + if (fee_remaining > 0) + return reject("Send amount is less than fee"); + + } + //remove all output with value less than DUST amount + let filtered_outputs = [], dust_value = 0; + tx.outs.forEach(o => o.value >= DUST_AMT ? filtered_outputs.push(o) : dust_value += o.value); + tx.outs = filtered_outputs; + //update result values + result.fee += util.Sat_to_BTC(dust_value); + result.output_size = output_size; + result.output_amount = total_amount - (fee_from_receiver ? result.fee : 0); + result.total_size = BASE_TX_SIZE + output_size + result.input_size; + result.transaction = tx; + resolve(result); + }).catch(error => reject(error)) + }) + } + btcOperator.createTransaction = createTransaction; + + function addInputs(tx, senders, redeemScripts, total_amount, fee, output_size, fee_from_receiver) { + return new Promise((resolve, reject) => { + if (fee !== null) { + addUTXOs(tx, senders, redeemScripts, fee_from_receiver ? total_amount : total_amount + fee, false).then(result => { + result.fee = fee; + resolve(result); + }).catch(error => reject(error)) + } else { + get_fee_rate().then(fee_rate => { + let net_fee = BASE_TX_SIZE * fee_rate; + net_fee += (output_size * fee_rate); + (fee_from_receiver ? + addUTXOs(tx, senders, redeemScripts, total_amount, false) : + addUTXOs(tx, senders, redeemScripts, total_amount + net_fee, fee_rate) + ).then(result => { + result.fee = parseFloat((net_fee + (result.input_size * fee_rate)).toFixed(8)); + result.fee_rate = fee_rate; + resolve(result); + }).catch(error => reject(error)) + }).catch(error => reject(error)) + } + }) + } + btcOperator.addInputs = addInputs; + + function addUTXOs(tx, senders, redeemScripts, required_amount, fee_rate, rec_args = {}) { + return new Promise((resolve, reject) => { + required_amount = parseFloat(required_amount.toFixed(8)); + if (typeof rec_args.n === "undefined") { + rec_args.n = 0; + rec_args.input_size = 0; + rec_args.input_amount = 0; + } + if (required_amount <= 0) + return resolve({ + input_size: rec_args.input_size, + input_amount: rec_args.input_amount, + change_amount: required_amount * -1 //required_amount will be -ve of change_amount + }); + else if (rec_args.n >= senders.length) + return reject("Insufficient Balance"); + let addr = senders[rec_args.n], + rs = redeemScripts[rec_args.n]; + let addr_type = coinjs.addressDecode(addr).type; + let size_per_input = _sizePerInput(addr, rs); + fetch_api(`unspent?active=${addr}`).then(result => { + let utxos = result.unspent_outputs; + //console.debug("add-utxo", addr, rs, required_amount, utxos); + for (let i = 0; i < utxos.length && required_amount > 0; i++) { + if (!utxos[i].confirmations) //ignore unconfirmed utxo + continue; + var script; + if (!rs || !rs.length) //legacy script + script = utxos[i].script; + else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi)) || addr_type === 'multisigBech32') { + //redeemScript for segwit/bech32 and multisig (bech32) + let s = coinjs.script(); + s.writeBytes(Crypto.util.hexToBytes(rs)); + s.writeOp(0); + s.writeBytes(coinjs.numToBytes(utxos[i].value.toFixed(0), 8)); + script = Crypto.util.bytesToHex(s.buffer); + } else //redeemScript for multisig (segwit) + script = rs; + tx.addinput(utxos[i].tx_hash_big_endian, utxos[i].tx_output_n, script, 0xfffffffd /*sequence*/); //0xfffffffd for Replace-by-fee + //update track values + rec_args.input_size += size_per_input; + rec_args.input_amount += util.Sat_to_BTC(utxos[i].value); + required_amount -= util.Sat_to_BTC(utxos[i].value); + if (fee_rate) //automatic fee calculation (dynamic) + required_amount += size_per_input * fee_rate; + } + rec_args.n += 1; + addUTXOs(tx, senders, redeemScripts, required_amount, fee_rate, rec_args) + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + btcOperator.addUTXOs = addUTXOs; + + function addOutputs(tx, receivers, amounts, change_address) { + let size = 0; + for (let i in receivers) { + tx.addoutput(receivers[i], amounts[i]); + size += _sizePerOutput(receivers[i]); + } + tx.addoutput(change_address, 0); + size += _sizePerOutput(change_address); + return size; + } + btcOperator.addOutputs = addOutputs; + + /* + function autoFeeCalc(tx) { + return new Promise((resolve, reject) => { + get_fee_rate().then(fee_rate => { + let tx_size = tx.size(); + for (var i = 0; i < this.ins.length; i++) + switch (tx.extractScriptKey(i).type) { + case 'scriptpubkey': + tx_size += SIGN_SIZE; + break; + case 'segwit': + case 'multisig': + tx_size += SIGN_SIZE * 0.25; + break; + default: + console.warn('Unknown script-type'); + tx_size += SIGN_SIZE; + } + resolve(tx_size * fee_rate); + }).catch(error => reject(error)) + }) + } + + function editFee(tx, current_fee, target_fee, index = -1) { + //values are in satoshi + index = parseInt(index >= 0 ? index : tx.outs.length - index); + if (index < 0 || index >= tx.outs.length) + throw "Invalid index"; + let edit_value = parseInt(current_fee - target_fee), //rip of any decimal places + current_value = tx.outs[index].value; //could be BigInterger + if (edit_value < 0 && edit_value > current_value) + throw "Insufficient value at vout"; + tx.outs[index].value = current_value instanceof BigInteger ? + current_value.add(new BigInteger('' + edit_value)) : parseInt(current_value + edit_value); + } + */ + + function tx_fetch_for_editing(tx) { + return new Promise((resolve, reject) => { + if (typeof tx == 'string' && /^[0-9a-f]{64}$/i.test(tx)) { //tx is txid + getTx.hex(tx) + .then(txhex => resolve(deserializeTx(txhex))) + .catch(error => reject(error)) + } else resolve(deserializeTx(tx)); + }) + } + btcOperator.tx_fetch_for_editing = tx_fetch_for_editing; + + const extractLastHexStrings = btcOperator.extractLastHexStrings = function (arr) { + const result = []; + for (let i = 0; i < arr.length; i++) { + const innerArray = arr[i]; + if (innerArray.length > 0) { + const lastHexString = innerArray[innerArray.length - 1]; + result.push(lastHexString); + } + } + return result; + } + + btcOperator.editFee = function (tx_hex, new_fee, private_keys, change_only = true) { + return new Promise((resolve, reject) => { + if (!Array.isArray(private_keys)) + private_keys = [private_keys]; + tx_fetch_for_editing(tx_hex).then(tx => { + parseTransaction(tx).then(tx_parsed => { + if (tx_parsed.fee >= new_fee) + return reject("Fees can only be increased"); + + //editable addresses in output values (for fee increase) + var edit_output_address = new Set(); + if (change_only === true) //allow only change values (ie, sender address) to be edited to inc fee + tx_parsed.inputs.forEach(inp => edit_output_address.add(inp.address)); + else if (change_only === false) //allow all output values to be edited + tx_parsed.outputs.forEach(out => edit_output_address.add(out.address)); + else if (typeof change_only == 'string') // allow only given receiver id output to be edited + edit_output_address.add(change_only); + else if (Array.isArray(change_only)) //allow only given set of receiver id outputs to be edited + change_only.forEach(id => edit_output_address.add(id)); + + //edit output values to increase fee + let inc_fee = util.BTC_to_Sat(new_fee - tx_parsed.fee); + if (inc_fee < MIN_FEE_UPDATE) + return reject(`Insufficient additional fee. Minimum increment: ${MIN_FEE_UPDATE}`); + for (let i = tx.outs.length - 1; i >= 0 && inc_fee > 0; i--) //reduce in reverse order + if (edit_output_address.has(tx_parsed.outputs[i].address)) { + let current_value = tx.outs[i].value; + if (current_value instanceof BigInteger) //convert BigInteger class to inv value + current_value = current_value.intValue(); + //edit the value as required + if (current_value > inc_fee) { + tx.outs[i].value = current_value - inc_fee; + inc_fee = 0; + } else { + inc_fee -= current_value; + tx.outs[i].value = 0; + } + } + if (inc_fee > 0) { + let max_possible_fee = util.BTC_to_Sat(new_fee) - inc_fee; //in satoshi + return reject(`Insufficient output values to increase fee. Maximum fee possible: ${util.Sat_to_BTC(max_possible_fee)}`); + } + tx.outs = tx.outs.filter(o => o.value >= DUST_AMT); //remove all output with value less than DUST amount + + //remove existing signatures and reset the scripts + let wif_keys = []; + for (let i in tx.ins) { + var addr = tx_parsed.inputs[i].address, + value = util.BTC_to_Sat(tx_parsed.inputs[i].value); + let addr_decode = coinjs.addressDecode(addr); + //find the correct key for addr + var privKey = private_keys.find(pk => verifyKey(addr, pk)); + if (!privKey) + return reject(`Private key missing for ${addr}`); + //find redeemScript (if any) + const rs = _redeemScript(addr, privKey); + rs === false ? wif_keys.unshift(privKey) : wif_keys.push(privKey); //sorting private-keys (wif) + //reset the script for re-signing + var script; + if (!rs || !rs.length) { + //legacy script (derive from address) + let s = coinjs.script(); + s.writeOp(118); //OP_DUP + s.writeOp(169); //OP_HASH160 + s.writeBytes(addr_decode.bytes); + s.writeOp(136); //OP_EQUALVERIFY + s.writeOp(172); //OP_CHECKSIG + script = Crypto.util.bytesToHex(s.buffer); + } else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi)) || addr_decode.type === 'multisigBech32') { + //redeemScript for segwit/bech32 and multisig (bech32) + let s = coinjs.script(); + s.writeBytes(Crypto.util.hexToBytes(rs)); + s.writeOp(0); + s.writeBytes(coinjs.numToBytes(value.toFixed(0), 8)); + script = Crypto.util.bytesToHex(s.buffer); + } else //redeemScript for multisig (segwit) + script = rs; + tx.ins[i].script = coinjs.script(script); + } + tx.witness = false; //remove all witness signatures + console.debug("Unsigned:", tx.serialize()); + //re-sign the transaction + new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF + resolve(tx.serialize()); + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + btcOperator.editFee_corewallet = function(tx_hex, new_fee, private_keys, change_only = true) { + return new Promise((resolve, reject) => { + if (!Array.isArray(private_keys)) + private_keys = [private_keys]; + tx_fetch_for_editing(tx_hex).then(tx => { + parseTransaction(tx).then(tx_parsed => { + if (tx_parsed.fee >= new_fee) + return reject("Fees can only be increased"); + + //editable addresses in output values (for fee increase) + var edit_output_address = new Set(); + if (change_only === true) //allow only change values (ie, sender address) to be edited to inc fee + tx_parsed.inputs.forEach(inp => edit_output_address.add(inp.address)); + else if (change_only === false) //allow all output values to be edited + tx_parsed.outputs.forEach(out => edit_output_address.add(out.address)); + else if (typeof change_only == 'string') // allow only given receiver id output to be edited + edit_output_address.add(change_only); + else if (Array.isArray(change_only)) //allow only given set of receiver id outputs to be edited + change_only.forEach(id => edit_output_address.add(id)); + + //edit output values to increase fee + let inc_fee = util.BTC_to_Sat(new_fee - tx_parsed.fee); + if (inc_fee < MIN_FEE_UPDATE) + return reject(`Insufficient additional fee. Minimum increment: ${MIN_FEE_UPDATE}`); + for (let i = tx.outs.length - 1; i >= 0 && inc_fee > 0; i--) //reduce in reverse order + if (edit_output_address.has(tx_parsed.outputs[i].address)) { + let current_value = tx.outs[i].value; + if (current_value instanceof BigInteger) //convert BigInteger class to inv value + current_value = current_value.intValue(); + //edit the value as required + if (current_value > inc_fee) { + tx.outs[i].value = current_value - inc_fee; + inc_fee = 0; + } else { + inc_fee -= current_value; + tx.outs[i].value = 0; + } + } + if (inc_fee > 0) { + let max_possible_fee = util.BTC_to_Sat(new_fee) - inc_fee; //in satoshi + return reject(`Insufficient output values to increase fee. Maximum fee possible: ${util.Sat_to_BTC(max_possible_fee)}`); + } + tx.outs = tx.outs.filter(o => o.value >= DUST_AMT); //remove all output with value less than DUST amount + + //remove existing signatures and reset the scripts + let wif_keys = []; + let witness_position = 0; + for (let i in tx.ins) { + var addr = tx_parsed.inputs[i].address, + value = util.BTC_to_Sat(tx_parsed.inputs[i].value); + let addr_decode = coinjs.addressDecode(addr); + + //find the correct key for addr + var privKey = private_keys.find(pk => verifyKey(addr, pk)); + if (!privKey) + return reject(`Private key missing for ${addr}`); + //find redeemScript (if any) + const rs = _redeemScript(addr, privKey); + rs === false ? wif_keys.unshift(privKey) : wif_keys.push(privKey); //sorting private-keys (wif) + //reset the script for re-signing + var script; + if (!rs || !rs.length) { + //legacy script (derive from address) + let s = coinjs.script(); + s.writeOp(118); //OP_DUP + s.writeOp(169); //OP_HASH160 + s.writeBytes(addr_decode.bytes); + s.writeOp(136); //OP_EQUALVERIFY + s.writeOp(172); //OP_CHECKSIG + script = Crypto.util.bytesToHex(s.buffer); + } else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi))) { + //redeemScript for segwit/bech32 + let s = coinjs.script(); + s.writeBytes(Crypto.util.hexToBytes(rs)); + s.writeOp(0); + s.writeBytes(coinjs.numToBytes(value.toFixed(0), 8)); + script = Crypto.util.bytesToHex(s.buffer); + if (addr_decode == "bech32") {witness_position = witness_position + 1;} //bech32 has witness + } else if (addr_decode.type === 'multisigBech32') { + var rs_array = []; + rs_array = btcOperator.extractLastHexStrings(tx.witness); + let redeemScript = rs_array[witness_position]; + witness_position = witness_position + 1; + + //redeemScript multisig (bech32) + let s = coinjs.script(); + s.writeBytes(Crypto.util.hexToBytes(redeemScript)); + s.writeOp(0); + s.writeBytes(coinjs.numToBytes(value.toFixed(0), 8)); + script = Crypto.util.bytesToHex(s.buffer); + } else //redeemScript for multisig (segwit) + script = rs; + tx.ins[i].script = coinjs.script(script); + } + tx.witness = false; //remove all witness signatures + console.debug("Unsigned:", tx.serialize()); + //re-sign the transaction + new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/ )); //Sign the tx using private key WIF + if (btcOperator.checkSigned(tx)) { + resolve(tx.serialize()); + } else { + reject("All private keys not present"); + } + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) +} + + + btcOperator.sendTx = function (senders, privkeys, receivers, amounts, fee = null, options = {}) { + return new Promise((resolve, reject) => { + createSignedTx(senders, privkeys, receivers, amounts, fee, options).then(result => { + // debugger; + broadcastTx(result.transaction.serialize()) + .then(txid => resolve(txid)) + .catch(error => reject(error)); + }).catch(error => reject(error)) + }) + } + + const createSignedTx = btcOperator.createSignedTx = function (senders, privkeys, receivers, amounts, fee = null, options = {}) { + return new Promise((resolve, reject) => { + try { + ({ + senders, + privkeys, + receivers, + amounts + } = validateTxParameters({ + senders, + privkeys, + receivers, + amounts, + fee, + change_address: options.change_address + })); + } catch (e) { + return reject(e) + } + let redeemScripts = [], + wif_keys = []; + for (let i in senders) { + let rs = _redeemScript(senders[i], privkeys[i]); //get redeem-script (segwit/bech32) + redeemScripts.push(rs); + rs === false ? wif_keys.unshift(privkeys[i]) : wif_keys.push(privkeys[i]); //sorting private-keys (wif) + } + if (redeemScripts.includes(null)) //TODO: segwit + return reject("Unable to get redeem-script"); + //create transaction + createTransaction(senders, redeemScripts, receivers, amounts, fee, options.change_address || senders[0], options.fee_from_receiver).then(result => { + let tx = result.transaction; + console.debug("Unsigned:", tx.serialize()); + new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF + console.debug("Signed:", tx.serialize()); + resolve(result); + }).catch(error => reject(error)); + }) + } + + btcOperator.createTx = function (senders, receivers, amounts, fee = null, options = {}) { + return new Promise((resolve, reject) => { + try { + ({ + senders, + receivers, + amounts + } = validateTxParameters({ + senders, + receivers, + amounts, + fee, + change_address: options.change_address + })); + } catch (e) { + return reject(e) + } + let redeemScripts = senders.map(id => _redeemScript(id)); + if (redeemScripts.includes(null)) //TODO: segwit + return reject("Unable to get redeem-script"); + //create transaction + createTransaction(senders, redeemScripts, receivers, amounts, fee, options.change_address || senders[0], options.fee_from_receiver).then(result => { + result.tx_hex = result.transaction.serialize(); + delete result.transaction; + resolve(result); + }).catch(error => reject(error)) + }) + } + + btcOperator.createMultiSigTx = function (sender, redeemScript, receivers, amounts, fee = null, options = {}) { + return new Promise((resolve, reject) => { + //validate tx parameters + let addr_type = validateAddress(sender); + if (!(["multisig", "multisigBech32"].includes(addr_type))) + return reject("Invalid sender (multisig):" + sender); + else { + let script = coinjs.script(); + let decode = (addr_type == "multisig") ? + script.decodeRedeemScript(redeemScript) : + script.decodeRedeemScriptBech32(redeemScript); + if (!decode || decode.address !== sender) + return reject("Invalid redeem-script"); + } + try { + ({ + receivers, + amounts + } = validateTxParameters({ + receivers, + amounts, + fee, + change_address: options.change_address + })); + } catch (e) { + return reject(e) + } + //create transaction + createTransaction([sender], [redeemScript], receivers, amounts, fee, options.change_address || sender, options.fee_from_receiver).then(result => { + result.tx_hex = result.transaction.serialize(); + delete result.transaction; + resolve(result); + }).catch(error => reject(error)) + + }) + } + + const deserializeTx = btcOperator.deserializeTx = function (tx) { + if (typeof tx === 'string' || Array.isArray(tx)) { + try { + tx = coinjs.transaction().deserialize(tx); + } catch { + throw "Invalid transaction hex"; + } + } else if (typeof tx !== 'object' || typeof tx.sign !== 'function') + throw "Invalid transaction object"; + return tx; + } + + btcOperator.signTx = function (tx, privkeys, sighashtype = 1) { + tx = deserializeTx(tx); + if (!Array.isArray(privkeys)) + privkeys = [privkeys]; + for (let i in privkeys) + if (privkeys[i].length === 64) + privkeys[i] = coinjs.privkey2wif(privkeys[i]); + new Set(privkeys).forEach(key => tx.sign(key, sighashtype)); //Sign the tx using private key WIF + return tx.serialize(); + } + + const checkSigned = btcOperator.checkSigned = function (tx, bool = true) { + tx = deserializeTx(tx); + let n = []; + for (let i in tx.ins) { + var s = tx.extractScriptKey(i); + if (s['type'] !== 'multisig' && s['type'] !== 'multisig_bech32') + n.push(s.signed == 'true' || (tx.witness[i] && tx.witness[i].length == 2)) + else { + var rs = coinjs.script().decodeRedeemScript(s.script); //will work for bech32 too, as only address is diff + let x = { + s: s['signatures'], + r: rs['signaturesRequired'], + t: rs['pubkeys'].length + }; + if (x.r > x.t) + throw "signaturesRequired is more than publicKeys"; + else if (x.s < x.r) + n.push(x); + else + n.push(true); + } + } + return bool ? !(n.filter(x => x !== true).length) : n; + } + + btcOperator.checkIfSameTx = function (tx1, tx2) { + tx1 = deserializeTx(tx1); + tx2 = deserializeTx(tx2); + //compare input and output length + if (tx1.ins.length !== tx2.ins.length || tx1.outs.length !== tx2.outs.length) + return false; + //compare inputs + for (let i = 0; i < tx1.ins.length; i++) + if (tx1.ins[i].outpoint.hash !== tx2.ins[i].outpoint.hash || tx1.ins[i].outpoint.index !== tx2.ins[i].outpoint.index) + return false; + //compare outputs + for (let i = 0; i < tx1.outs.length; i++) + if (tx1.outs[i].value !== tx2.outs[i].value || Crypto.util.bytesToHex(tx1.outs[i].script.buffer) !== Crypto.util.bytesToHex(tx2.outs[i].script.buffer)) + return false; + return true; + } + + const getTxOutput = (txid, i) => new Promise((resolve, reject) => { + fetch_api(`rawtx/${txid}`) + .then(result => resolve(result.out[i])) + .catch(error => reject(error)) + }); + + const parseTransaction = btcOperator.parseTransaction = function (tx) { + return new Promise((resolve, reject) => { + tx = deserializeTx(tx); + let result = {}; + let promises = []; + //Parse Inputs + for (let i = 0; i < tx.ins.length; i++) + promises.push(getTxOutput(tx.ins[i].outpoint.hash, tx.ins[i].outpoint.index)); + Promise.all(promises).then(inputs => { + result.inputs = inputs.map(inp => Object({ + address: inp.addr, + value: util.Sat_to_BTC(inp.value) + })); + let signed = checkSigned(tx, false); + result.inputs.forEach((inp, i) => inp.signed = signed[i]); + //Parse Outputs + result.outputs = tx.outs.map(out => { + var address; + switch (out.script.chunks[0]) { + case 0: //bech32, multisig-bech32 + address = util.encodeBech32(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.bech32.version, coinjs.bech32.hrp); + break; + case 169: //segwit, multisig-segwit + address = util.encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.multisig); + break; + case 118: //legacy + address = util.encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[2]), coinjs.pub); + } + return { + address, + value: util.Sat_to_BTC(out.value) + } + }); + //Parse Totals + result.total_input = parseFloat(result.inputs.reduce((a, inp) => a += inp.value, 0).toFixed(8)); + result.total_output = parseFloat(result.outputs.reduce((a, out) => a += out.value, 0).toFixed(8)); + result.fee = parseFloat((result.total_input - result.total_output).toFixed(8)); + resolve(result); + }).catch(error => reject(error)) + }) + } + + btcOperator.transactionID = function (tx) { + tx = deserializeTx(tx); + let clone = coinjs.clone(tx); + clone.witness = null; + let raw_bytes = Crypto.util.hexToBytes(clone.serialize()); + let txid = Crypto.SHA256(Crypto.SHA256(raw_bytes, { asBytes: true }), { asBytes: true }).reverse(); + return Crypto.util.bytesToHex(txid); + } + + const getLatestBlock = btcOperator.getLatestBlock = () => new Promise((resolve, reject) => { + fetch_api(`q/getblockcount`) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + + const getTx = btcOperator.getTx = txid => new Promise((resolve, reject) => { + fetch_api(`rawtx/${txid}`).then(result => { + getLatestBlock().then(latest_block => resolve({ + block: result.block_height, + txid: result.hash, + time: result.time * 1000, + confirmations: result.block_height === null ? 0 : latest_block - result.block_height, //calculate confirmations using latest block number as api doesnt relay it + size: result.size, + fee: util.Sat_to_BTC(result.fee), + inputs: result.inputs.map(i => Object({ address: i.prev_out.addr, value: util.Sat_to_BTC(i.prev_out.value) })), + total_input_value: util.Sat_to_BTC(result.inputs.reduce((a, i) => a + i.prev_out.value, 0)), + outputs: result.out.map(o => Object({ address: o.addr, value: util.Sat_to_BTC(o.value) })), + total_output_value: util.Sat_to_BTC(result.out.reduce((a, o) => a += o.value, 0)), + })) + }).catch(error => reject(error)) + }); + + getTx.hex = btcOperator.getTx.hex = txid => new Promise((resolve, reject) => { + fetch_api(`rawtx/${txid}?format=hex`, false) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + + btcOperator.getAddressData = address => new Promise((resolve, reject) => { + fetch_api(`rawaddr/${address}`).then(data => { + let details = {}; + details.balance = util.Sat_to_BTC(data.final_balance); + details.address = data.address; + details.txs = data.txs.map(tx => { + let d = { + txid: tx.hash, + time: tx.time * 1000, //s to ms + block: tx.block_height, + } + //sender list + d.tx_senders = {}; + tx.inputs.forEach(i => { + if (i.prev_out.addr in d.tx_senders) + d.tx_senders[i.prev_out.addr] += i.prev_out.value; + else d.tx_senders[i.prev_out.addr] = i.prev_out.value; + }); + d.tx_input_value = 0; + for (let s in d.tx_senders) { + let val = d.tx_senders[s]; + d.tx_senders[s] = util.Sat_to_BTC(val); + d.tx_input_value += val; + } + d.tx_input_value = util.Sat_to_BTC(d.tx_input_value); + //receiver list + d.tx_receivers = {}; + tx.out.forEach(o => { + if (o.addr in d.tx_receivers) + d.tx_receivers[o.addr] += o.value; + else d.tx_receivers[o.addr] = o.value; + }); + d.tx_output_value = 0; + for (let r in d.tx_receivers) { + let val = d.tx_receivers[r]; + d.tx_receivers[r] = util.Sat_to_BTC(val); + d.tx_output_value += val; + } + d.tx_output_value = util.Sat_to_BTC(d.tx_output_value); + d.tx_fee = util.Sat_to_BTC(tx.fee); + //tx type + if (tx.result > 0) { //net > 0, balance inc => type=in + d.type = "in"; + d.amount = util.Sat_to_BTC(tx.result); + d.sender = Object.keys(d.tx_senders).filter(s => s !== address); + } else if (Object.keys(d.tx_receivers).some(r => r !== address)) { //net < 0, balance dec & receiver present => type=out + d.type = "out"; + d.amount = util.Sat_to_BTC(tx.result * -1); + d.receiver = Object.keys(d.tx_receivers).filter(r => r !== address); + d.fee = d.tx_fee; + } else { //net < 0 (fee) & no other id in receiver list => type=self + d.type = "self"; + d.amount = d.tx_receivers[address]; + d.address = address + } + return d; + }) + resolve(details); + }).catch(error => reject(error)) + }); + + btcOperator.getBlock = block => new Promise((resolve, reject) => { + fetch_api(`rawblock/${block}`).then(result => resolve({ + height: result.height, + hash: result.hash, + merkle_root: result.mrkl_root, + prev_block: result.prev_block, + next_block: result.next_block[0], + size: result.size, + time: result.time * 1000, //s to ms + txs: result.tx.map(t => Object({ + fee: t.fee, + size: t.size, + inputs: t.inputs.map(i => Object({ address: i.prev_out.addr, value: util.Sat_to_BTC(i.prev_out.value) })), + total_input_value: util.Sat_to_BTC(t.inputs.reduce((a, i) => a + i.prev_out.value, 0)), + outputs: t.out.map(o => Object({ address: o.addr, value: util.Sat_to_BTC(o.value) })), + total_output_value: util.Sat_to_BTC(t.out.reduce((a, o) => a += o.value, 0)), + })) + + })).catch(error => reject(error)) + }); + +})('object' === typeof module ? module.exports : window.btcOperator = {}); diff --git a/messenger/scripts/compactIDB.js b/messenger/scripts/compactIDB.js new file mode 100644 index 0000000..ba843ec --- /dev/null +++ b/messenger/scripts/compactIDB.js @@ -0,0 +1,257 @@ +(function (EXPORTS) { //compactIDB v2.1.2 + /* Compact IndexedDB operations */ + 'use strict'; + const compactIDB = EXPORTS; + + var defaultDB; + + const indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; + const IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction; + const IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange; + + if (!indexedDB) { + console.error("Your browser doesn't support a stable version of IndexedDB."); + return; + } + + compactIDB.setDefaultDB = dbName => defaultDB = dbName; + + Object.defineProperty(compactIDB, 'default', { + get: () => defaultDB, + set: dbName => defaultDB = dbName + }); + + function getDBversion(dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + resolve(db.version) + db.close() + }).catch(error => reject(error)) + }) + } + + function upgradeDB(dbName, createList = null, deleteList = null) { + return new Promise((resolve, reject) => { + getDBversion(dbName).then(version => { + var idb = indexedDB.open(dbName, version + 1); + idb.onerror = (event) => reject("Error in opening IndexedDB"); + idb.onupgradeneeded = (event) => { + let db = event.target.result; + if (createList instanceof Object) { + if (Array.isArray(createList)) { + let tmp = {} + createList.forEach(o => tmp[o] = {}) + createList = tmp + } + for (let o in createList) { + let obs = db.createObjectStore(o, createList[o].options || {}); + if (createList[o].indexes instanceof Object) + for (let i in createList[o].indexes) + obs.createIndex(i, i, createList[o].indexes || {}); + } + } + if (Array.isArray(deleteList)) + deleteList.forEach(o => db.deleteObjectStore(o)); + resolve('Database upgraded') + } + idb.onsuccess = (event) => event.target.result.close(); + }).catch(error => reject(error)) + }) + } + + compactIDB.initDB = function (dbName, objectStores = {}) { + return new Promise((resolve, reject) => { + if (!(objectStores instanceof Object)) + return reject('ObjectStores must be an object or array') + defaultDB = defaultDB || dbName; + var idb = indexedDB.open(dbName); + idb.onerror = (event) => reject("Error in opening IndexedDB"); + idb.onsuccess = (event) => { + var db = event.target.result; + let cList = Object.values(db.objectStoreNames); + var obs = {}, + a_obs = {}, + d_obs = []; + if (!Array.isArray(objectStores)) + var obs = objectStores + else + objectStores.forEach(o => obs[o] = {}) + let nList = Object.keys(obs) + for (let o of nList) + if (!cList.includes(o)) + a_obs[o] = obs[o] + for (let o of cList) + if (!nList.includes(o)) + d_obs.push(o) + if (!Object.keys(a_obs).length && !d_obs.length) + resolve("Initiated IndexedDB"); + else + upgradeDB(dbName, a_obs, d_obs) + .then(result => resolve(result)) + .catch(error => reject(error)) + db.close(); + } + }); + } + + const openDB = compactIDB.openDB = function (dbName = defaultDB) { + return new Promise((resolve, reject) => { + var idb = indexedDB.open(dbName); + idb.onerror = (event) => reject("Error in opening IndexedDB"); + idb.onupgradeneeded = (event) => { + event.target.result.close(); + deleteDB(dbName).then(_ => null).catch(_ => null).finally(_ => reject("Datebase not found")) + } + idb.onsuccess = (event) => resolve(event.target.result); + }); + } + + const deleteDB = compactIDB.deleteDB = function (dbName = defaultDB) { + return new Promise((resolve, reject) => { + var deleteReq = indexedDB.deleteDatabase(dbName);; + deleteReq.onerror = (event) => reject("Error deleting database!"); + deleteReq.onsuccess = (event) => resolve("Database deleted successfully"); + }); + } + + compactIDB.writeData = function (obsName, data, key = false, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readwrite").objectStore(obsName); + let writeReq = (key ? obs.put(data, key) : obs.put(data)); + writeReq.onsuccess = (evt) => resolve(`Write data Successful`); + writeReq.onerror = (evt) => reject( + `Write data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.addData = function (obsName, data, key = false, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readwrite").objectStore(obsName); + let addReq = (key ? obs.add(data, key) : obs.add(data)); + addReq.onsuccess = (evt) => resolve(`Add data successful`); + addReq.onerror = (evt) => reject( + `Add data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.removeData = function (obsName, key, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readwrite").objectStore(obsName); + let delReq = obs.delete(key); + delReq.onsuccess = (evt) => resolve(`Removed Data ${key}`); + delReq.onerror = (evt) => reject( + `Remove data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.clearData = function (obsName, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readwrite").objectStore(obsName); + let clearReq = obs.clear(); + clearReq.onsuccess = (evt) => resolve(`Clear data Successful`); + clearReq.onerror = (evt) => reject(`Clear data Unsuccessful`); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.readData = function (obsName, key, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + let getReq = obs.get(key); + getReq.onsuccess = (evt) => resolve(evt.target.result); + getReq.onerror = (evt) => reject( + `Read data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.readAllData = function (obsName, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + var tmpResult = {} + let curReq = obs.openCursor(); + curReq.onsuccess = (evt) => { + var cursor = evt.target.result; + if (cursor) { + tmpResult[cursor.primaryKey] = cursor.value; + cursor.continue(); + } else + resolve(tmpResult); + } + curReq.onerror = (evt) => reject( + `Read-All data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + /* compactIDB.searchData = function (obsName, options = {}, dbName = defaultDB) { + + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + var filteredResult = {} + let keyRange; + if(options.lowerKey!==null && options.upperKey!==null) + keyRange = IDBKeyRange.bound(options.lowerKey, options.upperKey); + else if(options.lowerKey!==null) + keyRange = IDBKeyRange.lowerBound(options.lowerKey); + else if (options.upperKey!==null) + keyRange = IDBKeyRange.upperBound(options.upperBound); + else if (options.atKey) + let curReq = obs.openCursor(keyRange, ) + }).catch(error => reject(error)) + }) + }*/ + + compactIDB.searchData = function (obsName, options = {}, dbName = defaultDB) { + options.lowerKey = options.atKey || options.lowerKey || 0 + options.upperKey = options.atKey || options.upperKey || false + options.patternEval = options.patternEval || ((k, v) => true); + options.limit = options.limit || false; + options.reverse = options.reverse || false; + options.lastOnly = options.lastOnly || false + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + var filteredResult = {} + let curReq = obs.openCursor( + options.upperKey ? IDBKeyRange.bound(options.lowerKey, options.upperKey) : IDBKeyRange.lowerBound(options.lowerKey), + options.lastOnly || options.reverse ? "prev" : "next"); + curReq.onsuccess = (evt) => { + var cursor = evt.target.result; + if (!cursor || (options.limit && options.limit <= Object.keys(filteredResult).length)) + return resolve(filteredResult); //reached end of key list or limit reached + else if (options.patternEval(cursor.primaryKey, cursor.value)) { + filteredResult[cursor.primaryKey] = cursor.value; + options.lastOnly ? resolve(filteredResult) : cursor.continue(); + } else + cursor.continue(); + } + curReq.onerror = (evt) => reject(`Search unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`); + db.close(); + }).catch(error => reject(error)); + }); + } + + +})(window.compactIDB = {}); \ No newline at end of file diff --git a/messenger/scripts/components.js b/messenger/scripts/components.js new file mode 100644 index 0000000..99be7bc --- /dev/null +++ b/messenger/scripts/components.js @@ -0,0 +1,388 @@ +/*jshint esversion: 6 */ +// Components downloaded: chips,copy,form,input,menu,notifications,popup,switch,select,spinner,textarea,text-field,theme-toggle +const smChips = document.createElement("template"); smChips.innerHTML = '
', customElements.define("sm-chips", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smChips.content.cloneNode(!0)), this.chipsWrapper = this.shadowRoot.querySelector(".sm-chips"), this.coverLeft = this.shadowRoot.querySelector(".cover--left"), this.coverRight = this.shadowRoot.querySelector(".cover--right"), this.navButtonLeft = this.shadowRoot.querySelector(".nav-button--left"), this.navButtonRight = this.shadowRoot.querySelector(".nav-button--right"), this.slottedOptions = void 0, this._value = void 0, this.scrollDistance = 0, this.assignedElements = [], this.scrollLeft = this.scrollLeft.bind(this), this.scrollRight = this.scrollRight.bind(this), this.fireEvent = this.fireEvent.bind(this), this.setSelectedOption = this.setSelectedOption.bind(this) } get value() { return this._value } set value(t) { this.setSelectedOption(t) } scrollLeft() { this.chipsWrapper.scrollBy({ left: -this.scrollDistance, behavior: "smooth" }) } scrollRight() { this.chipsWrapper.scrollBy({ left: this.scrollDistance, behavior: "smooth" }) } setSelectedOption(t) { this._value !== t && (this._value = t, this.assignedElements.forEach(e => { e.value == t ? (e.setAttribute("selected", ""), e.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" })) : e.removeAttribute("selected") })) } fireEvent() { this.dispatchEvent(new CustomEvent("change", { bubbles: !0, composed: !0, detail: { value: this._value } })) } connectedCallback() { this.setAttribute("role", "listbox"); const t = this.shadowRoot.querySelector("slot"); t.addEventListener("slotchange", e => { n.disconnect(), i.disconnect(), this.observeSelf.disconnect(), clearTimeout(this.slotChangeTimeout), this.slotChangeTimeout = setTimeout(() => { this.assignedElements = t.assignedElements(), this.assignedElements.forEach(t => { t.hasAttribute("selected") && (this._value = t.value) }), this.observeSelf.observe(this) }, 0) }); const e = new ResizeObserver(t => { t.forEach(t => { if (t.contentBoxSize) { const e = Array.isArray(t.contentBoxSize) ? t.contentBoxSize[0] : t.contentBoxSize; this.scrollDistance = .6 * e.inlineSize } else this.scrollDistance = .6 * t.contentRect.width }) }); e.observe(this), this.observeSelf = new IntersectionObserver((t, e) => { t.forEach(t => { t.isIntersecting && !this.hasAttribute("multiline") && this.assignedElements.length > 0 && (n.observe(this.assignedElements[0]), i.observe(this.assignedElements[this.assignedElements.length - 1]), e.unobserve(this)) }) }, { threshold: 1 }), this.chipsWrapper.addEventListener("option-clicked", t => { this._value !== t.target.value && (this.setSelectedOption(t.target.value), this.fireEvent()) }); const n = new IntersectionObserver(t => { t.forEach(t => { t.isIntersecting ? (this.navButtonLeft.classList.add("hide"), this.coverLeft.classList.add("hide")) : (this.navButtonLeft.classList.remove("hide"), this.coverLeft.classList.remove("hide")) }) }, { threshold: 1, root: this }), i = new IntersectionObserver(t => { t.forEach(t => { t.isIntersecting ? (this.navButtonRight.classList.add("hide"), this.coverRight.classList.add("hide")) : (this.navButtonRight.classList.remove("hide"), this.coverRight.classList.remove("hide")) }) }, { threshold: 1, root: this }); this.navButtonLeft.addEventListener("click", this.scrollLeft), this.navButtonRight.addEventListener("click", this.scrollRight) } disconnectedCallback() { this.navButtonLeft.removeEventListener("click", this.scrollLeft), this.navButtonRight.removeEventListener("click", this.scrollRight) } }); const smChip = document.createElement("template"); smChip.innerHTML = ' ', customElements.define("sm-chip", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smChip.content.cloneNode(!0)), this._value = void 0, this.radioButton = this.shadowRoot.querySelector("input"), this.fireEvent = this.fireEvent.bind(this), this.handleKeyDown = this.handleKeyDown.bind(this) } get value() { return this._value } fireEvent() { this.dispatchEvent(new CustomEvent("option-clicked", { bubbles: !0, composed: !0, detail: { value: this._value } })) } handleKeyDown(t) { "Enter" !== t.key && "Space" !== t.key || this.fireEvent() } connectedCallback() { this.setAttribute("role", "option"), this.setAttribute("tabindex", "0"), this._value = this.getAttribute("value"), this.addEventListener("click", this.fireEvent), this.addEventListener("keydown", this.handleKeyDown) } disconnectedCallback() { this.removeEventListener("click", this.fireEvent), this.removeEventListener("keydown", this.handleKeyDown) } }); +const smCopy = document.createElement("template"); smCopy.innerHTML = '

', customElements.define("sm-copy", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smCopy.content.cloneNode(!0)), this.copyContent = this.shadowRoot.querySelector(".copy-content"), this.copyButton = this.shadowRoot.querySelector(".copy-button"), this.copy = this.copy.bind(this) } static get observedAttributes() { return ["value"] } set value(t) { this.setAttribute("value", t) } get value() { return this.getAttribute("value") } fireEvent() { this.dispatchEvent(new CustomEvent("copy", { composed: !0, bubbles: !0, cancelable: !0 })) } copy() { navigator.clipboard.writeText(this.getAttribute("value")).then(t => this.fireEvent()).catch(t => console.error(t)) } connectedCallback() { this.copyButton.addEventListener("click", this.copy) } attributeChangedCallback(t, n, o) { if ("value" === t) { const t = this.copyContent.querySelector("slot"); if (!t) return; const n = t.assignedNodes(); n && n.length || (t.textContent = o) } } disconnectedCallback() { this.copyButton.removeEventListener("click", this.copy) } }); +const smForm = document.createElement("template"); smForm.innerHTML = '
', customElements.define("sm-form", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smForm.content.cloneNode(!0)), this.form = this.shadowRoot.querySelector("form"), this.invalidFieldsCount, this.skipSubmit = !1, this.isFormValid = void 0, this.supportedElements = "input, sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio", this.formElements = [], this._requiredElements = [] } static get observedAttributes() { return ["skip-submit"] } get validity() { return this.isFormValid } debounce = (callback, wait) => { let timeoutId = null; return (...args) => { window.clearTimeout(timeoutId), timeoutId = window.setTimeout((() => { callback.apply(null, args) }), wait) } }; _checkValidity = () => { this.submitButton && 0 !== this._requiredElements.length && (this.invalidFieldsCount = 0, this._requiredElements.forEach((([elem, isWC]) => { (!elem.disabled && isWC && !elem.isValid || !isWC && !elem.checkValidity()) && this.invalidFieldsCount++ })), this.isFormValid !== (0 === this.invalidFieldsCount) && (this.isFormValid = 0 === this.invalidFieldsCount, this.dispatchEvent(new CustomEvent(this.isFormValid ? "valid" : "invalid", { bubbles: !0, composed: !0 })), this.skipSubmit || (this.submitButton.disabled = !this.isFormValid))) }; handleKeydown = e => { if ("Enter" === e.key && e.target.tagName.includes("INPUT")) if (0 === this.invalidFieldsCount) this.submitButton && this.submitButton.click(), this.dispatchEvent(new CustomEvent("submit", { bubbles: !0, composed: !0 })); else for (const [elem, isWC] of this._requiredElements) { if (isWC ? !elem.isValid : !elem.checkValidity()) { (elem?.shadowRoot?.lastElementChild || elem).animate([{ transform: "translateX(-1rem)" }, { transform: "translateX(1rem)" }, { transform: "translateX(-0.5rem)" }, { transform: "translateX(0.5rem)" }, { transform: "translateX(0)" }], { duration: 300, easing: "ease" }), isWC ? (elem.focusIn(), "SM-INPUT" === elem.tagName && "" === elem.value.trim() && elem.showError()) : elem.focus(); break } } }; reset = () => { this.formElements.forEach((([elem, isWC]) => { if (isWC) elem.reset(); else switch (elem.type) { case "checkbox": case "radio": elem.checked = !1; break; default: elem.value = "" } })), this._checkValidity() }; elementsChanged = () => { this.formElements = [...this.querySelectorAll(this.supportedElements)].map((elem => [elem, elem.tagName.includes("-")])), this._requiredElements = this.formElements.filter((([elem]) => elem.hasAttribute("required"))), this.submitButton = this.querySelector('[variant="primary"], [type="submit"]'), this.resetButton = this.querySelector('[type="reset"]'), this.resetButton && this.resetButton.addEventListener("click", this.reset), this._checkValidity() }; checkIfSupported = elem => 1 === elem.nodeType && (elem.tagName.includes("-") || "input" === elem.tagName || elem.querySelector(this.supportedElements)); connectedCallback() { const updateFormDecedents = this.debounce(this.elementsChanged, 100); this.addEventListener("input", this.debounce(this._checkValidity, 100)), this.addEventListener("keydown", this.debounce(this.handleKeydown, 100)), this.shadowRoot.querySelector("slot").addEventListener("slotchange", updateFormDecedents), this.mutationObserver = new MutationObserver((mutations => { mutations.forEach((mutation => { ("childList" === mutation.type && [...mutation.addedNodes].some((node => this.checkIfSupported(node))) || [...mutation.removedNodes].some((node => this.checkIfSupported(node)))) && updateFormDecedents() })) })), this.mutationObserver.observe(this, { childList: !0, subtree: !0 }) } attributeChangedCallback(name, oldValue, newValue) { "skip-submit" === name && (this.skipSubmit = this.hasAttribute("skip-submit")) } disconnectedCallback() { this.removeEventListener("input", this.debounce(this._checkValidity, 100)), this.removeEventListener("keydown", this.debounce(this.handleKeydown, 100)), this.mutationObserver.disconnect() } }); +const smInput = document.createElement("template"); smInput.innerHTML = '
', customElements.define("sm-input", class extends HTMLElement { #validationState = { validatedFor: void 0, isValid: !1, errorMessage: "Please fill out this field." }; constructor() { super(), this.attachShadow({ mode: "open" }).append(smInput.content.cloneNode(!0)), this.inputParent = this.shadowRoot.querySelector(".input"), this.input = this.shadowRoot.querySelector("input"), this.clearBtn = this.shadowRoot.querySelector(".clear"), this.label = this.shadowRoot.querySelector(".label"), this.feedbackText = this.shadowRoot.querySelector(".feedback-text"), this.outerContainer = this.shadowRoot.querySelector(".outer-container"), this.optionList = this.shadowRoot.querySelector(".datalist"), this._helperText = "", this.isRequired = !1, this.datalist = [], this.validationFunction = void 0, this.reflectedAttributes = ["value", "required", "disabled", "type", "inputmode", "readonly", "min", "max", "pattern", "minlength", "maxlength", "step", "list", "autocomplete"] } static get observedAttributes() { return ["value", "placeholder", "required", "disabled", "type", "inputmode", "readonly", "min", "max", "pattern", "minlength", "maxlength", "step", "helper-text", "error-text", "list"] } get value() { return this.input.value } set value(val) { val !== this.input.value && (this.input.value = val, this._value = val, this.checkInput()) } get placeholder() { return this.getAttribute("placeholder") } set placeholder(val) { this.setAttribute("placeholder", val) } get type() { return this.getAttribute("type") } set type(val) { this.setAttribute("type", val) } get validity() { return this.input.validity } get disabled() { return this.hasAttribute("disabled") } set disabled(value) { value ? (this.inputParent.classList.add("disabled"), this.setAttribute("disabled", "")) : (this.inputParent.classList.remove("disabled"), this.removeAttribute("disabled")) } get readOnly() { return this.hasAttribute("readonly") } set readOnly(value) { value ? this.setAttribute("readonly", "") : this.removeAttribute("readonly") } set customValidation(val) { val && (this.validationFunction = val) } set errorText(val) { this.#validationState.errorText = val } showError = (errorText = this.#validationState.errorText) => { this.feedbackText.className = "feedback-text error", this.feedbackText.innerHTML = ` ${errorText}` }; set helperText(val) { this._helperText = val } get isValid() { if (this.#validationState.validatedFor === this.input.value) return this.#validationState.isValid; const _isValid = this.input.checkValidity(); let _validity = { isValid: !0, errorText: "" }; return this.validationFunction && (_validity = this.validationFunction(this.input.value)), _isValid && _validity.isValid ? (this.feedbackText.className = "feedback-text success", this.feedbackText.textContent = "") : "" !== this.value.trim() && (_validity.errorText || this.#validationState.errorText) && this.showError(_validity.errorText || this.#validationState.errorText), this.#validationState.validatedFor = this.input.value, this.#validationState.isValid = _isValid && _validity.isValid, this.#validationState.errorText = _validity.errorText || this.#validationState.errorText, this.#validationState.isValid } reset = () => { this.value = "" }; clear = () => { this.value = "", this.input.focus(), this.fireEvent() }; focusIn = () => { this.input.focus() }; focusOut = () => { this.input.blur() }; fireEvent = () => { let event = new Event("input", { bubbles: !0, cancelable: !0, composed: !0 }); this.dispatchEvent(event) }; searchDatalist = searchKey => { const filteredData = this.datalist.filter((item => item.toLowerCase().includes(searchKey.toLowerCase()))); if (filteredData.sort(((a, b) => a.toLowerCase().indexOf(searchKey.toLowerCase()) - b.toLowerCase().indexOf(searchKey.toLowerCase()))), filteredData.length) { if (this.optionList.children.length > filteredData.length) { const optionsToRemove = this.optionList.children.length - filteredData.length; for (let i = 0; i < optionsToRemove; i++)this.optionList.removeChild(this.optionList.lastChild) } filteredData.forEach(((item, index) => { if (this.optionList.children[index]) this.optionList.children[index].textContent = item; else { const option = document.createElement("li"); option.textContent = item, option.classList.add("datalist-item"), option.setAttribute("tabindex", "0"), this.optionList.appendChild(option) } })), this.optionList.classList.remove("hidden") } else this.optionList.classList.add("hidden") }; checkInput = e => { this.hasAttribute("readonly") || ("" !== this.input.value ? this.clearBtn.classList.remove("hidden") : this.clearBtn.classList.add("hidden")), this.hasAttribute("placeholder") && "" !== this.getAttribute("placeholder").trim() && ("" !== this.input.value ? (this.shouldAnimateLabel && this.inputParent.classList.add("animate-placeholder"), this.label.classList.toggle("hidden", !this.shouldAnimateLabel), this.datalist.length && (this.searchTimeout && clearTimeout(this.searchTimeout), this.searchTimeout = setTimeout((() => { this.searchDatalist(this.input.value.trim()) }), 100))) : (this.shouldAnimateLabel && this.inputParent.classList.remove("animate-placeholder"), this.label.classList.remove("hidden"), this.feedbackText.textContent = "", this.datalist.length && (this.optionList.innerHTML = "", this.optionList.classList.add("hidden")))) }; allowOnlyNum = e => { e.ctrlKey || 1 === e.key.length && (("." !== e.key || !e.target.value.includes(".") && 0 !== e.target.value.length) && ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."].includes(e.key) || e.preventDefault()) }; handleOptionClick = e => { this.input.value = e.target.textContent, this.optionList.classList.add("hidden"), this.input.focus() }; handleInputNavigation = e => { "ArrowDown" === e.key ? (e.preventDefault(), this.optionList.children.length && this.optionList.children[0].focus()) : "ArrowUp" === e.key && (e.preventDefault(), this.optionList.children.length && this.optionList.children[this.optionList.children.length - 1].focus()) }; handleDatalistNavigation = e => { "ArrowUp" === e.key ? (e.preventDefault(), this.shadowRoot.activeElement.previousElementSibling ? this.shadowRoot.activeElement.previousElementSibling.focus() : this.input.focus()) : "ArrowDown" === e.key ? (e.preventDefault(), this.shadowRoot.activeElement.nextElementSibling ? this.shadowRoot.activeElement.nextElementSibling.focus() : this.input.focus()) : "Enter" !== e.key && " " !== e.key || (e.preventDefault(), this.input.value = e.target.textContent, this.optionList.classList.add("hidden"), this.input.focus()) }; handleFocus = e => { this.datalist.length && this.searchDatalist(this.input.value.trim()) }; handleBlur = e => { this.datalist.length && this.optionList.classList.add("hidden") }; connectedCallback() { const uuid = crypto.randomUUID(); if (this.input.id = uuid, this.label.htmlFor = uuid, this.shouldAnimateLabel = this.hasAttribute("animate"), this.shouldAnimateLabel && "" !== this.placeholder && this.value && (this.inputParent.classList.add("animate-placeholder"), this.label.classList.remove("hidden")), this.setAttribute("role", "textbox"), "undefined" != typeof smCompConfig && smCompConfig["sm-input"]) { const config = smCompConfig["sm-input"].find((config => this.matches(config.selector))); this.customValidation = config?.customValidation } this.input.addEventListener("input", this.checkInput), this.clearBtn.addEventListener("click", this.clear), this.datalist.length && (this.optionList.addEventListener("click", this.handleOptionClick), this.input.addEventListener("keydown", this.handleInputNavigation), this.optionList.addEventListener("keydown", this.handleDatalistNavigation)), this.input.addEventListener("focusin", this.handleFocus), this.addEventListener("focusout", this.handleBlur) } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) switch (this.reflectedAttributes.includes(name) && (this.hasAttribute(name) ? this.input.setAttribute(name, this.getAttribute(name) ? this.getAttribute(name) : "") : this.input.removeAttribute(name)), name) { case "placeholder": this.label.textContent = newValue, this.setAttribute("aria-label", newValue); break; case "value": this.checkInput(); break; case "type": this.hasAttribute("type") && "number" === this.getAttribute("type") ? (this.input.setAttribute("inputmode", "decimal"), this.input.addEventListener("keydown", this.allowOnlyNum)) : this.input.removeEventListener("keydown", this.allowOnlyNum); break; case "helper-text": this._helperText = newValue; break; case "error-text": this.#validationState.errorText = newValue; break; case "required": this.isRequired = this.hasAttribute("required"), this.isRequired ? this.setAttribute("aria-required", "true") : this.setAttribute("aria-required", "false"); break; case "readonly": this.hasAttribute("readonly") ? this.inputParent.classList.add("readonly") : this.inputParent.classList.remove("readonly"); break; case "disabled": this.hasAttribute("disabled") ? this.inputParent.classList.add("disabled") : this.inputParent.classList.remove("disabled"); break; case "list": this.hasAttribute("list") && "" !== this.getAttribute("list").trim() && (this.datalist = this.getAttribute("list").split(",")) } } disconnectedCallback() { this.input.removeEventListener("input", this.checkInput), this.clearBtn.removeEventListener("click", this.clear), this.input.removeEventListener("keydown", this.allowOnlyNum), this.optionList.removeEventListener("click", this.handleOptionClick), this.input.removeEventListener("keydown", this.handleInputNavigation), this.optionList.removeEventListener("keydown", this.handleDatalistNavigation), this.input.removeEventListener("focusin", this.handleFocus), this.removeEventListener("focusout", this.handleBlur) } }); +const smMenu = document.createElement("template"); smMenu.innerHTML = '
', customElements.define("sm-menu", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smMenu.content.cloneNode(!0)), this.isOpen = !1, this.availableOptions, this.containerDimensions, this.animOptions = { duration: 200, easing: "ease" }, this.optionList = this.shadowRoot.querySelector(".options"), this.menu = this.shadowRoot.querySelector(".menu"), this.icon = this.shadowRoot.querySelector(".icon"), this.expand = this.expand.bind(this), this.collapse = this.collapse.bind(this), this.toggle = this.toggle.bind(this), this.handleKeyDown = this.handleKeyDown.bind(this), this.handleClick = this.handleClick.bind(this), this.handleClickOutside = this.handleClickOutside.bind(this) } static get observedAttributes() { return ["value"] } get value() { return this.getAttribute("value") } set value(n) { this.setAttribute("value", n) } expand() { this.isOpen || (this.optionList.classList.remove("hide"), this.optionList.animate([{ transform: "translateY(-1rem)", opacity: "0" }, { transform: "none", opacity: "1" }], this.animOptions).onfinish = (() => { this.isOpen = !0, this.icon.classList.add("focused"), document.addEventListener("mousedown", this.handleClickOutside); const n = this.optionList.firstElementChild.assignedElements().find(n => "MENU-OPTION" === n.tagName); n && n.focus() })) } collapse() { this.isOpen && (this.optionList.animate([{ transform: "none", opacity: "1" }, { transform: "translateY(-1rem)", opacity: "0" }], this.animOptions).onfinish = (() => { this.isOpen = !1, this.icon.classList.remove("focused"), this.optionList.classList.add("hide"), document.removeEventListener("mousedown", this.handleClickOutside) })) } toggle() { this.isOpen ? this.collapse() : this.expand() } handleClick(n) { n.target === this ? this.toggle() : this.collapse() } handleKeyDown(n) { n.target === this ? "ArrowDown" === n.key ? (n.preventDefault(), this.availableOptions[0].focus()) : " " === n.key && (n.preventDefault(), this.toggle()) : "ArrowUp" === n.key ? (n.preventDefault(), document.activeElement.previousElementSibling ? document.activeElement.previousElementSibling.focus() : this.availableOptions[this.availableOptions.length - 1].focus()) : "ArrowDown" === n.key ? (n.preventDefault(), document.activeElement.nextElementSibling ? document.activeElement.nextElementSibling.focus() : this.availableOptions[0].focus()) : "Enter" !== n.key && " " !== n.key || (n.preventDefault(), n.target.click(), this.collapse(), this.menu.focus()) } handleClickOutside(n) { this.contains(n.target) || 2 === n.button || this.collapse() } connectedCallback() { this.setAttribute("role", "listbox"), this.setAttribute("aria-label", "dropdown menu"); const n = this.shadowRoot.querySelector(".options slot"); n.addEventListener("slotchange", n => { this.availableOptions = n.target.assignedElements(), this.containerDimensions = this.optionList.getBoundingClientRect() }), this.addEventListener("click", this.handleClick), this.addEventListener("keydown", this.handleKeyDown) } disconnectedCallback() { this.removeEventListener("click", this.handleClick), this.removeEventListener("keydown", this.handleKeyDown), document.removeEventListener("mousedown", this.handleClickOutside) } }); const menuOption = document.createElement("template"); menuOption.innerHTML = '
', customElements.define("menu-option", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(menuOption.content.cloneNode(!0)) } connectedCallback() { this.setAttribute("role", "option"), this.setAttribute("tabindex", "0"), this.addEventListener("keyup", n => { "Enter" !== n.key && " " !== n.key || (n.preventDefault(), this.click()) }) } }); +const smNotifications = document.createElement("template"); smNotifications.innerHTML = "
", customElements.define("sm-notifications", class extends HTMLElement { constructor() { super(), this.shadow = this.attachShadow({ mode: "open" }).append(smNotifications.content.cloneNode(!0)), this.notificationPanel = this.shadowRoot.querySelector(".notification-panel"), this.animationOptions = { duration: 300, fill: "forwards", easing: "cubic-bezier(0.175, 0.885, 0.32, 1.275)" }, this.push = this.push.bind(this), this.createNotification = this.createNotification.bind(this), this.removeNotification = this.removeNotification.bind(this), this.clearAll = this.clearAll.bind(this), this.remove = this.remove.bind(this), this.handleTouchMove = this.handleTouchMove.bind(this), this.startX = 0, this.currentX = 0, this.endX = 0, this.swipeDistance = 0, this.swipeDirection = "", this.swipeThreshold = 0, this.startTime = 0, this.swipeTime = 0, this.swipeTimeThreshold = 200, this.currentTarget = null, this.notificationTimeout = 5e3, this.mediaQuery = window.matchMedia("(min-width: 640px)"), this.handleOrientationChange = this.handleOrientationChange.bind(this), this.isBigViewport = !1 } set timeout(value) { isNaN(value) || (this.notificationTimeout = value) } randString(length) { let result = ""; const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; for (let i = 0; i < length; i++)result += characters.charAt(Math.floor(52 * Math.random())); return result } createNotification(message, options = {}) { const { pinned: pinned = !1, icon: icon, action: action, timeout: timeout = this.notificationTimeout } = options, notification = document.createElement("div"); return notification.id = this.randString(8), notification.className = "notification " + (pinned ? "pinned" : ""), notification.style.setProperty("--timeout", `${timeout}ms`), notification.innerHTML = ` ${icon ? `
${icon}
` : ""} ${message} ${action ? `` : ""} `, action && notification.querySelector(".action").addEventListener("click", action.callback), notification.querySelector(".close").addEventListener("click", (() => { this.removeNotification(notification) })), pinned || setTimeout((() => { this.removeNotification(notification, this.isBigViewport ? "left" : "top") }), timeout), notification } push(message, options = {}) { const notification = this.createNotification(message, options); return this.isBigViewport ? this.notificationPanel.append(notification) : this.notificationPanel.prepend(notification), notification.scrollIntoView({ behavior: "smooth" }), this.notificationPanel.animate([{ transform: `translateY(${this.isBigViewport ? "" : "-"}${notification.clientHeight}px)` }, { transform: "none" }], this.animationOptions), notification.animate([{ transform: "translateY(-1rem)", opacity: "0" }, { transform: "none", opacity: "1" }], this.animationOptions).onfinish = e => { e.target.commitStyles(), e.target.cancel() }, notification.id } removeNotification(notification, direction = "left") { if (!notification) return; const sign = "left" === direction || "top" === direction ? "-" : "+"; this.isBigViewport || "top" !== direction ? notification.animate([{ transform: this.currentX ? `translateX(${this.currentX}px)` : "none", opacity: "1" }, { transform: `translateX(calc(${sign}${Math.abs(this.currentX)}px ${sign} 1rem))`, opacity: "0" }], this.animationOptions).onfinish = () => { notification.remove() } : notification.animate([{ transform: this.currentX ? `translateY(${this.currentX}px)` : "none", opacity: "1" }, { transform: `translateY(calc(${sign}${Math.abs(this.currentX)}px ${sign} 1rem))`, opacity: "0" }], this.animationOptions).onfinish = () => { notification.remove() } } remove(id) { const notification = this.notificationPanel.querySelector(`#${id}`); notification && this.removeNotification(notification) } clearAll() { Array.from(this.notificationPanel.children).forEach((child => { this.removeNotification(child) })) } handleTouchMove(e) { this.currentX = e.touches[0].clientX - this.startX, this.currentTarget.style.transform = `translateX(${this.currentX}px)` } handleOrientationChange(e) { this.isBigViewport = e.matches, e.matches } connectedCallback() { this.handleOrientationChange(this.mediaQuery), this.mediaQuery.addEventListener("change", this.handleOrientationChange), this.notificationPanel.addEventListener("touchstart", (e => { e.target.closest(".close") ? this.removeNotification(e.target.closest(".notification")) : e.target.closest(".notification") && (this.swipeThreshold = e.target.closest(".notification").getBoundingClientRect().width / 2, this.currentTarget = e.target.closest(".notification"), this.startTime = Date.now(), this.startX = e.touches[0].clientX, this.startY = e.touches[0].clientY, this.notificationPanel.addEventListener("touchmove", this.handleTouchMove, { passive: !0 })) }), { passive: !0 }), this.notificationPanel.addEventListener("touchend", (e => { this.endX = e.changedTouches[0].clientX, this.endY = e.changedTouches[0].clientY, this.swipeDistance = Math.abs(this.endX - this.startX), this.swipeTime = Date.now() - this.startTime, this.endX > this.startX ? this.swipeDirection = "right" : this.swipeDirection = "left", this.swipeTime < this.swipeTimeThreshold ? this.swipeDistance > 50 && this.removeNotification(this.currentTarget, this.swipeDirection) : this.swipeDistance > this.swipeThreshold ? this.removeNotification(this.currentTarget, this.swipeDirection) : this.currentTarget.animate([{ transform: `translateX(${this.currentX}px)` }, { transform: "none" }], this.animationOptions).onfinish = e => { e.target.commitStyles(), e.target.cancel() }, this.notificationPanel.removeEventListener("touchmove", this.handleTouchMove), this.currentX = 0 })) } disconnectedCallback() { mediaQueryList.removeEventListener("change", handleOrientationChange) } }); +class Stack { constructor() { this.items = [] } push(t) { this.items.push(t) } pop() { return 0 == this.items.length ? "Underflow" : this.items.pop() } peek() { return this.items[this.items.length - 1] } } const popupStack = new Stack, smPopup = document.createElement("template"); smPopup.innerHTML = ` `, customElements.define("sm-popup", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smPopup.content.cloneNode(!0)), this.allowClosing = !1, this.isOpen = !1, this.offset = 0, this.touchStartY = 0, this.touchEndY = 0, this.touchStartTime = 0, this.touchEndTime = 0, this.touchEndAnimation = void 0, this.focusable, this.autoFocus, this.mutationObserver, this.popupContainer = this.shadowRoot.querySelector(".popup-container"), this.backdrop = this.shadowRoot.querySelector(".backdrop"), this.dialogBox = this.shadowRoot.querySelector(".popup"), this.popupBodySlot = this.shadowRoot.querySelector(".popup-body slot"), this.popupHeader = this.shadowRoot.querySelector(".popup-top"), this.resumeScrolling = this.resumeScrolling.bind(this), this.setStateOpen = this.setStateOpen.bind(this), this.show = this.show.bind(this), this.hide = this.hide.bind(this), this.handleTouchStart = this.handleTouchStart.bind(this), this.handleTouchMove = this.handleTouchMove.bind(this), this.handleTouchEnd = this.handleTouchEnd.bind(this), this.detectFocus = this.detectFocus.bind(this), this.handleSoftDismiss = this.handleSoftDismiss.bind(this), this.debounce = this.debounce.bind(this) } static get observedAttributes() { return ["open"] } get open() { return this.isOpen } animateTo(t, e, i) { let s = t.animate(e, { ...i, fill: "both" }); return s.finished.then(() => { s.commitStyles(), s.cancel() }), s } resumeScrolling() { let t = document.body.style.top; window.scrollTo(0, -1 * parseInt(t || "0")), document.body.style.overflow = "", document.body.style.top = "initial" } setStateOpen() { if (!this.isOpen || this.offset) { let t = window.innerWidth > 640 ? "scale(1.1)" : `translateY(${this.offset ? `${this.offset}px` : "100%"})`; this.animateTo(this.dialogBox, [{ opacity: this.offset ? 1 : 0, transform: t }, { opacity: 1, transform: "none" },], { duration: 300, easing: "ease" }) } } show(t = {}) { let { pinned: e = !1, payload: i } = t; if (this.isOpen) return; let s = { duration: 300, easing: "ease" }; return this.payload = i, popupStack.push({ popup: this, permission: e }), popupStack.items.length > 1 && this.animateTo(popupStack.items[popupStack.items.length - 2].popup.shadowRoot.querySelector(".popup"), [{ transform: "none" }, { transform: window.innerWidth > 640 ? "scale(0.95)" : "translateY(-1.5rem)" },], s), this.popupContainer.classList.remove("hide"), this.offset || (this.backdrop.animate([{ opacity: 0 }, { opacity: 1 },], s).onfinish = () => { this.resolveOpen(this.payload) }, this.dispatchEvent(new CustomEvent("popupopened", { bubbles: !0, composed: !0, detail: { payload: this.payload } })), document.body.style.overflow = "hidden", document.body.style.top = `-${window.scrollY}px`), this.setStateOpen(), this.pinned = e, this.isOpen = !0, setTimeout(() => { let t = this.autoFocus || this.focusable?.[0] || this.dialogBox; t && (t.tagName.includes("-") ? t.focusIn() : t.focus()) }, 0), this.hasAttribute("open") || (this.setAttribute("open", ""), this.addEventListener("keydown", this.detectFocus), this.resizeObserver.observe(this), this.mutationObserver.observe(this, { attributes: !0, childList: !0, subtree: !0 }), this.popupHeader.addEventListener("touchstart", this.handleTouchStart, { passive: !0 }), this.backdrop.addEventListener("mousedown", this.handleSoftDismiss)), { opened: new Promise(t => { this.resolveOpen = t }), closed: new Promise(t => { this.resolveClose = t }) } } hide(t = {}) { let { payload: e } = t, i = { duration: 150, easing: "ease" }; this.backdrop.animate([{ opacity: 1 }, { opacity: 0 }], i), this.animateTo(this.dialogBox, [{ opacity: 1, transform: window.innerWidth > 640 ? "none" : `translateY(${this.offset ? `${this.offset}px` : "0"})` }, { opacity: 0, transform: window.innerWidth > 640 ? "scale(1.1)" : "translateY(100%)" },], i).finished.finally(() => { this.popupContainer.classList.add("hide"), this.dialogBox.style = "", this.removeAttribute("open"), this.forms.length && this.forms.forEach(t => t.reset()), this.dispatchEvent(new CustomEvent("popupclosed", { bubbles: !0, composed: !0, detail: { payload: e || this.payload } })), this.resolveClose(e || this.payload), this.isOpen = !1 }), popupStack.pop(), popupStack.items.length ? this.animateTo(popupStack.items[popupStack.items.length - 1].popup.shadowRoot.querySelector(".popup"), [{ transform: window.innerWidth > 640 ? "scale(0.95)" : "translateY(-1.5rem)" }, { transform: "none" },], i) : this.resumeScrolling(), this.resizeObserver.disconnect(), this.mutationObserver.disconnect(), this.removeEventListener("keydown", this.detectFocus), this.popupHeader.removeEventListener("touchstart", this.handleTouchStart, { passive: !0 }), this.backdrop.removeEventListener("mousedown", this.handleSoftDismiss) } handleTouchStart(t) { this.offset = 0, this.popupHeader.addEventListener("touchmove", this.handleTouchMove, { passive: !0 }), this.popupHeader.addEventListener("touchend", this.handleTouchEnd, { passive: !0 }), this.touchStartY = t.changedTouches[0].clientY, this.touchStartTime = t.timeStamp } handleTouchMove(t) { this.touchStartY < t.changedTouches[0].clientY && (this.offset = t.changedTouches[0].clientY - this.touchStartY, this.touchEndAnimation = window.requestAnimationFrame(() => { this.dialogBox.style.transform = `translateY(${this.offset}px)` })) } handleTouchEnd(t) { if (this.touchEndTime = t.timeStamp, cancelAnimationFrame(this.touchEndAnimation), this.touchEndY = t.changedTouches[0].clientY, this.threshold = .3 * this.dialogBox.getBoundingClientRect().height, this.touchEndTime - this.touchStartTime > 200) { if (this.touchEndY - this.touchStartY > this.threshold) { if (this.pinned) { this.setStateOpen(); return } this.hide() } else this.setStateOpen() } else if (this.touchEndY > this.touchStartY) { if (this.pinned) { this.setStateOpen(); return } this.hide() } this.popupHeader.removeEventListener("touchmove", this.handleTouchMove, { passive: !0 }), this.popupHeader.removeEventListener("touchend", this.handleTouchEnd, { passive: !0 }) } detectFocus(t) { if ("Tab" === t.key && this.focusable.length) { if (!this.firstFocusable) { for (let e = 0; e < this.focusable.length; e++)if (!this.focusable[e].disabled) { this.firstFocusable = this.focusable[e]; break } } if (!this.lastFocusable) { for (let i = this.focusable.length - 1; i >= 0; i--)if (!this.focusable[i].disabled) { this.lastFocusable = this.focusable[i]; break } } t.shiftKey && document.activeElement === this.firstFocusable ? (t.preventDefault(), this.lastFocusable.tagName.includes("SM-") ? this.lastFocusable.focusIn() : this.lastFocusable.focus()) : t.shiftKey || document.activeElement !== this.lastFocusable || (t.preventDefault(), this.firstFocusable.tagName.includes("SM-") ? this.firstFocusable.focusIn() : this.firstFocusable.focus()) } } updateFocusableList() { this.focusable = this.querySelectorAll('sm-button:not([disabled]), button:not([disabled]), [href], sm-input, input:not([readonly]), sm-select, select, sm-checkbox, sm-textarea, textarea, [tabindex]:not([tabindex="-1"])'), this.autoFocus = this.querySelector("[autofocus]"), this.firstFocusable = null, this.lastFocusable = null } handleSoftDismiss() { this.pinned ? this.dialogBox.animate([{ transform: "translateX(-1rem)" }, { transform: "translateX(1rem)" }, { transform: "translateX(-0.5rem)" }, { transform: "translateX(0.5rem)" }, { transform: "translateX(0)" },], { duration: 300, easing: "ease" }) : this.hide() } debounce(t, e) { let i = null; return (...s) => { window.clearTimeout(i), i = window.setTimeout(() => { t.apply(null, s) }, e) } } connectedCallback() { this.popupBodySlot.addEventListener("slotchange", this.debounce(() => { this.forms = this.querySelectorAll("sm-form"), this.updateFocusableList() }, 0)), this.resizeObserver = new ResizeObserver(t => { t.forEach(t => { if (t.contentBoxSize) { let e = Array.isArray(t.contentBoxSize) ? t.contentBoxSize[0] : t.contentBoxSize; this.threshold = .3 * e.blockSize.height } else this.threshold = .3 * t.contentRect.height }) }), this.mutationObserver = new MutationObserver(t => { this.updateFocusableList() }) } disconnectedCallback() { this.resizeObserver.disconnect(), this.mutationObserver.disconnect(), this.removeEventListener("keydown", this.detectFocus), this.popupHeader.removeEventListener("touchstart", this.handleTouchStart, { passive: !0 }), this.backdrop.removeEventListener("mousedown", this.handleSoftDismiss) } attributeChangedCallback(t) { "open" === t && this.hasAttribute("open") && this.show() } }); +const smSwitch = document.createElement("template"); smSwitch.innerHTML = '\t', customElements.define("sm-switch", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smSwitch.content.cloneNode(!0)), this.switch = this.shadowRoot.querySelector(".switch"), this.input = this.shadowRoot.querySelector("input"), this.isChecked = !1, this.isDisabled = !1, this.dispatch = this.dispatch.bind(this) } static get observedAttributes() { return ["disabled", "checked"] } get disabled() { return this.isDisabled } set disabled(e) { e ? this.setAttribute("disabled", "") : this.removeAttribute("disabled") } get checked() { return this.isChecked } set checked(e) { e ? this.setAttribute("checked", "") : this.removeAttribute("checked") } get value() { return this.isChecked } reset() { } dispatch() { this.dispatchEvent(new CustomEvent("change", { bubbles: !0, composed: !0, detail: { value: this.isChecked } })) } connectedCallback() { this.addEventListener("keydown", e => { " " !== e.key || this.isDisabled || (e.preventDefault(), this.input.click()) }), this.input.addEventListener("click", e => { this.input.checked ? this.checked = !0 : this.checked = !1, this.dispatch() }) } attributeChangedCallback(e, t, n) { t !== n && ("disabled" === e ? this.hasAttribute("disabled") ? this.disabled = !0 : this.disabled = !1 : "checked" === e && (this.hasAttribute("checked") ? (this.isChecked = !0, this.input.checked = !0) : (this.isChecked = !1, this.input.checked = !1))) } }); +const smSelect = document.createElement("template"); smSelect.innerHTML = '
', customElements.define("sm-select", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smSelect.content.cloneNode(!0)), this.focusIn = this.focusIn.bind(this), this.reset = this.reset.bind(this), this.open = this.open.bind(this), this.collapse = this.collapse.bind(this), this.toggle = this.toggle.bind(this), this.handleOptionsNavigation = this.handleOptionsNavigation.bind(this), this.handleOptionSelection = this.handleOptionSelection.bind(this), this.handleKeydown = this.handleKeydown.bind(this), this.handleClickOutside = this.handleClickOutside.bind(this), this.selectOption = this.selectOption.bind(this), this.debounce = this.debounce.bind(this), this.elementsChanged = this.elementsChanged.bind(this), this.availableOptions = [], this.previousOption, this.isOpen = !1, this.label = "", this.defaultSelected = "", this.isUnderViewport = !1, this.animationOptions = { duration: 300, fill: "forwards", easing: "ease" }, this.optionList = this.shadowRoot.querySelector(".options"), this.selection = this.shadowRoot.querySelector(".selection"), this.selectedOptionText = this.shadowRoot.querySelector(".selected-option-text") } static get observedAttributes() { return ["disabled", "label", "readonly"] } get value() { return this.getAttribute("value") } set value(t) { const e = this.availableOptions.find(e => e.getAttribute("value") === t); e ? (this.setAttribute("value", t), this.selectOption(e)) : console.warn(`There is no option with ${t} as value`) } debounce(t, e) { let n = null; return (...i) => { window.clearTimeout(n), n = window.setTimeout(() => { t.apply(null, i) }, e) } } reset(t = !0) { if (this.availableOptions[0] && this.previousOption !== this.availableOptions[0]) { const e = this.availableOptions.find(t => t.hasAttribute("selected")) || this.availableOptions[0]; this.value = e.getAttribute("value"), t && this.fireEvent() } } selectOption(t) { this.previousOption !== t && (this.querySelectorAll("[selected").forEach(t => t.removeAttribute("selected")), this.selectedOptionText.textContent = `${this.label}${t.textContent}`, t.setAttribute("selected", ""), this.previousOption = t) } focusIn() { this.selection.focus() } open() { this.availableOptions.forEach(t => t.setAttribute("tabindex", 0)), this.optionList.classList.remove("hidden"), this.isUnderViewport = this.getBoundingClientRect().bottom + this.optionList.getBoundingClientRect().height > window.innerHeight, this.isUnderViewport ? this.setAttribute("isUnder", "") : this.removeAttribute("isUnder"), this.optionList.animate([{ transform: `translateY(${this.isUnderViewport ? "" : "-"}0.5rem)`, opacity: 0 }, { transform: "translateY(0)", opacity: 1 }], this.animationOptions), this.setAttribute("open", ""), this.style.zIndex = 1e3, (this.availableOptions.find(t => t.hasAttribute("selected")) || this.availableOptions[0]).focus(), document.addEventListener("mousedown", this.handleClickOutside), this.isOpen = !0 } collapse() { this.removeAttribute("open"), this.optionList.animate([{ transform: "translateY(0)", opacity: 1 }, { transform: `translateY(${this.isUnderViewport ? "" : "-"}0.5rem)`, opacity: 0 }], this.animationOptions).onfinish = (() => { this.availableOptions.forEach(t => t.removeAttribute("tabindex")), document.removeEventListener("mousedown", this.handleClickOutside), this.optionList.classList.add("hidden"), this.isOpen = !1, this.style.zIndex = "auto" }) } toggle() { this.isOpen || this.hasAttribute("disabled") ? this.collapse() : this.open() } fireEvent() { this.dispatchEvent(new CustomEvent("change", { bubbles: !0, composed: !0, detail: { value: this.value } })) } handleOptionsNavigation(t) { "ArrowUp" === t.key ? (t.preventDefault(), document.activeElement.previousElementSibling ? document.activeElement.previousElementSibling.focus() : this.availableOptions[this.availableOptions.length - 1].focus()) : "ArrowDown" === t.key && (t.preventDefault(), document.activeElement.nextElementSibling ? document.activeElement.nextElementSibling.focus() : this.availableOptions[0].focus()) } handleOptionSelection(t) { this.previousOption !== document.activeElement && (this.value = document.activeElement.getAttribute("value"), this.fireEvent()) } handleClick(t) { t.target === this ? this.toggle() : (this.handleOptionSelection(), this.collapse()) } handleKeydown(t) { t.target === this ? this.isOpen && "ArrowDown" === t.key ? (t.preventDefault(), (this.availableOptions.find(t => t.hasAttribute("selected")) || this.availableOptions[0]).focus(), this.handleOptionSelection(t)) : " " === t.key && (t.preventDefault(), this.toggle()) : (this.handleOptionsNavigation(t), this.handleOptionSelection(t), ["Enter", " ", "Escape", "Tab"].includes(t.key) && (t.preventDefault(), this.collapse(), this.focusIn())) } handleClickOutside(t) { this.isOpen && !this.contains(t.target) && this.collapse() } elementsChanged() { this.availableOptions = [...this.querySelectorAll("sm-option")], this.reset(!1), this.defaultSelected = this.value } connectedCallback() { this.setAttribute("role", "listbox"), this.hasAttribute("disabled") || this.hasAttribute("readonly") || (this.selection.setAttribute("tabindex", "0"), this.addEventListener("click", this.handleClick), this.addEventListener("keydown", this.handleKeydown)); const t = this.debounce(this.elementsChanged, 100); this.shadowRoot.querySelector("slot").addEventListener("slotchange", t), this.mutationObserver = new MutationObserver(e => { let n = !1; if (e.forEach(e => { switch (e.type) { case "childList": t(); break; case "attributes": n = !0 } }), n) { const t = this.availableOptions.find(t => t.hasAttribute("selected")) || this.availableOptions[0]; this.selectedOptionText.textContent = `${this.label}${t.textContent}`, this.setAttribute("value", t.getAttribute("value")) } }), this.mutationObserver.observe(this, { subtree: !0, childList: !0, attributeFilter: ["selected"] }), new IntersectionObserver((t, e) => { t.forEach(t => { if (t.isIntersecting) { const t = this.selection.getBoundingClientRect().left; t < window.innerWidth / 2 ? this.setAttribute("align-select", "left") : this.setAttribute("align-select", "right") } }) }).observe(this) } disconnectedCallback() { this.removeEventListener("click", this.handleClick), this.removeEventListener("keydown", this.handleKeydown) } attributeChangedCallback(t) { "disabled" === t || "readonly" === t ? this.hasAttribute("disabled") || this.hasAttribute("readonly") ? (this.selection.removeAttribute("tabindex"), this.removeEventListener("click", this.handleClick), this.removeEventListener("keydown", this.handleKeydown)) : (this.selection.setAttribute("tabindex", "0"), this.addEventListener("click", this.handleClick), this.addEventListener("keydown", this.handleKeydown)) : "label" === t && (this.label = this.hasAttribute("label") ? `${this.getAttribute("label")} ` : "") } }); const smOption = document.createElement("template"); smOption.innerHTML = '
', customElements.define("sm-option", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smOption.content.cloneNode(!0)) } connectedCallback() { this.setAttribute("role", "option") } }); +const spinner = document.createElement("template"); spinner.innerHTML = ''; class SpinnerLoader extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(spinner.content.cloneNode(!0)) } } window.customElements.define("sm-spinner", SpinnerLoader); +const smTextarea = document.createElement("template"); smTextarea.innerHTML = ' ', customElements.define("sm-textarea", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smTextarea.content.cloneNode(!0)), this.textarea = this.shadowRoot.querySelector("textarea"), this.textareaBox = this.shadowRoot.querySelector(".textarea"), this.placeholder = this.shadowRoot.querySelector(".placeholder"), this.reflectedAttributes = ["disabled", "required", "readonly", "rows", "minlength", "maxlength"], this.reset = this.reset.bind(this), this.focusIn = this.focusIn.bind(this), this.fireEvent = this.fireEvent.bind(this), this.checkInput = this.checkInput.bind(this) } static get observedAttributes() { return ["disabled", "value", "placeholder", "required", "readonly", "rows", "minlength", "maxlength"] } get value() { return this.textarea.value } set value(e) { this.setAttribute("value", e), this.fireEvent() } get disabled() { return this.hasAttribute("disabled") } set disabled(e) { e ? this.setAttribute("disabled", "") : this.removeAttribute("disabled") } get isValid() { return this.textarea.checkValidity() } reset() { this.setAttribute("value", "") } focusIn() { this.textarea.focus() } fireEvent() { let e = new Event("input", { bubbles: !0, cancelable: !0, composed: !0 }); this.dispatchEvent(e) } checkInput() { this.hasAttribute("placeholder") && "" !== this.getAttribute("placeholder") && ("" !== this.textarea.value ? this.placeholder.classList.add("hide") : this.placeholder.classList.remove("hide")) } connectedCallback() { this.textarea.addEventListener("input", e => { this.textareaBox.dataset.value = this.textarea.value, this.checkInput() }) } attributeChangedCallback(e, t, n) { this.reflectedAttributes.includes(e) ? this.hasAttribute(e) ? this.textarea.setAttribute(e, this.getAttribute(e) ? this.getAttribute(e) : "") : this.textContent.removeAttribute(e) : "placeholder" === e ? this.placeholder.textContent = this.getAttribute("placeholder") : "value" === e && (this.textarea.value = n, this.textareaBox.dataset.value = n, this.checkInput()) } }); +const textField = document.createElement("template"); textField.innerHTML = '
', customElements.define("text-field", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(textField.content.cloneNode(!0)), this.textField = this.shadowRoot.querySelector(".text-field"), this.textContainer = this.textField.children[0], this.editButton = this.textField.querySelector(".edit-button"), this.isTextEditable = !1, this.isDisabled = !1 } static get observedAttributes() { return ["disabled"] } get value() { return this.text } set value(val) { this.text = val, this.textContainer.textContent = val, this.setAttribute("value", val) } set disabled(val) { this.isDisabled = val, this.isDisabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled") } setEditable = () => { this.isTextEditable || (this.textContainer.contentEditable = !0, this.setAttribute("editable", ""), this.textContainer.focus(), document.execCommand("selectAll", !1, null), this.editButton.textContent = "Done", this.isTextEditable = !0) }; setNonEditable = () => { this.isTextEditable && (this.textContainer.contentEditable = !1, this.removeAttribute("editable"), this.text !== this.textContainer.textContent.trim() && (this.setAttribute("value", this.textContainer.textContent), this.text = this.textContainer.textContent.trim(), this.dispatchEvent(new CustomEvent("change", { bubbles: !0, cancelable: !0, composed: !0 }))), this.editButton.textContent = "Edit", this.isTextEditable = !1) }; toggleEditable = () => { this.isTextEditable ? this.setNonEditable() : this.setEditable() }; revert = () => { this.textContainer.isContentEditable && (this.value = this.text, this.setNonEditable()) }; connectedCallback() { this.text, this.hasAttribute("value") && (this.text = this.getAttribute("value"), this.textContainer.textContent = this.text), this.hasAttribute("disabled") ? this.isDisabled = !0 : this.isDisabled = !1, this.isDisabled || (this.textContainer.addEventListener("dblclick", this.setEditable), this.editButton.addEventListener("click", this.toggleEditable)) } attributeChangedCallback(name) { "disabled" === name && (this.hasAttribute("disabled") ? (this.editButton.classList.add("hide"), this.textContainer.removeEventListener("dblclick", this.setEditable), this.editButton.removeEventListener("click", this.toggleEditable), this.revert()) : (this.editButton.classList.remove("hide"), this.textContainer.addEventListener("dblclick", this.setEditable), this.editButton.addEventListener("click", this.toggleEditable))) } disconnectedCallback() { this.textContainer.removeEventListener("dblclick", this.setEditable), this.editButton.removeEventListener("click", this.toggleEditable) } }); +const themeToggle = document.createElement("template"); themeToggle.innerHTML = ' '; class ThemeToggle extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(themeToggle.content.cloneNode(!0)), this.isChecked = !1, this.hasTheme = "light", this.toggleState = this.toggleState.bind(this), this.fireEvent = this.fireEvent.bind(this), this.handleThemeChange = this.handleThemeChange.bind(this) } static get observedAttributes() { return ["checked"] } daylight() { this.hasTheme = "light", document.body.dataset.theme = "light", this.setAttribute("aria-checked", "false") } nightlight() { this.hasTheme = "dark", document.body.dataset.theme = "dark", this.setAttribute("aria-checked", "true") } toggleState() { if (!document.startViewTransition) return this.toggleAttribute("checked"), void this.fireEvent(); document.startViewTransition(() => { this.toggleAttribute("checked"), this.fireEvent() }) } handleKeyDown(e) { " " === e.key && this.toggleState() } handleThemeChange(e) { e.detail.theme !== this.hasTheme && ("dark" === e.detail.theme ? this.setAttribute("checked", "") : this.removeAttribute("checked")) } fireEvent() { this.dispatchEvent(new CustomEvent("themechange", { bubbles: !0, composed: !0, detail: { theme: this.hasTheme } })) } connectedCallback() { this.setAttribute("role", "switch"), this.setAttribute("aria-label", "theme toggle"), "dark" === localStorage.getItem(`${window.location.hostname}-theme`) ? (this.nightlight(), this.setAttribute("checked", "")) : "light" === localStorage.getItem(`${window.location.hostname}-theme`) ? (this.daylight(), this.removeAttribute("checked")) : window.matchMedia("(prefers-color-scheme: dark)").matches ? (this.nightlight(), this.setAttribute("checked", "")) : (this.daylight(), this.removeAttribute("checked")), this.addEventListener("click", this.toggleState), this.addEventListener("keydown", this.handleKeyDown), document.addEventListener("themechange", this.handleThemeChange) } disconnectedCallback() { this.removeEventListener("click", this.toggleState), this.removeEventListener("keydown", this.handleKeyDown), document.removeEventListener("themechange", this.handleThemeChange) } attributeChangedCallback(e, t, n) { "checked" === e && (this.hasAttribute("checked") ? (this.nightlight(), localStorage.setItem(`${window.location.hostname}-theme`, "dark")) : (this.daylight(), localStorage.setItem(`${window.location.hostname}-theme`, "light"))) } } window.customElements.define("theme-toggle", ThemeToggle); +//Color Grid +const colorGrid = document.createElement('template'); +colorGrid.innerHTML = ` + +
+
`; + +customElements.define('color-grid', + class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(colorGrid.content.cloneNode(true)) + + this.colorArray = [] + this.container = this.shadowRoot.querySelector('.color-tile-container') + this.handleChange = this.handleChange.bind(this) + this.setCheckMark = this.setCheckMark.bind(this) + } + + set colors(arr) { + this.colorArray = arr + this.renderTiles() + } + + set selectedColor(color) { + if (this.colorArray.includes(color) && this.container.querySelector(`[data-color="${color}"]`)) { + const selectedTile = this.container.querySelector(`[data-color="${color}"]`) + if (selectedTile) { + selectedTile.querySelector('input').checked = true + this.setCheckMark(selectedTile) + } + } + } + + randString(length) { + let result = ''; + let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + for (let i = 0; i < length; i++) + result += characters.charAt(Math.floor(Math.random() * characters.length)); + return result; + } + + renderTiles() { + this.container.innerHTML = '' + const frag = document.createDocumentFragment() + const groupName = this.randString(6) + this.colorArray.forEach(color => { + const label = document.createElement('label') + label.classList.add('color-tile') + label.setAttribute('data-color', color) + if (color.includes('--')) + label.setAttribute('style', `background-color: var(${color})`) + else + label.setAttribute('style', `background-color: ${color}`) + label.innerHTML = ` + + ` + frag.append(label) + }) + this.container.append(frag) + } + setCheckMark(target) { + target.parentNode.querySelectorAll('.checkmark').forEach(checkmark => checkmark.remove()) + const checkMark = document.createElement('div') + checkMark.classList.add('checkmark') + checkMark.innerHTML = ` + + ` + target.append(checkMark) + } + + handleChange(e) { + const clickedTile = e.target.closest('.color-tile') + this.setCheckMark(clickedTile) + const clickedTileColor = clickedTile.dataset.color + const tileSelected = new CustomEvent('colorselected', { + bubbles: true, + composed: true, + detail: { + value: clickedTileColor, + } + }) + this.dispatchEvent(tileSelected) + } + + connectedCallback() { + this.container.addEventListener('change', this.handleChange) + } + + disconnectedCallback() { + this.container.removeEventListener('change', this.handleChange) + } + }) + +window.customElements.define('keys-generator', class extends HTMLElement { + constructor() { + super(); + this.innerHTML = ` + +
+
+ +
+ +

Keep your keys safe!

+
+

Don't share with anyone. Once lost private key can't be recovered.

+
+
+
+
+
FLO address
+ +
+
+
Private key
+ +
+
+ +

You can use these FLO credentials with other RanchiMall apps too.

+
+ ` + } + get keys() { + return { + floID: this.querySelector('#generated_flo_address').value, + privKey: this.querySelector('#generated_private_key').value + } + } + + generateKeys() { + const { floID, privKey } = floCrypto.generateNewID() + this.querySelector('#generated_flo_address').value = floID + this.querySelector('#generated_private_key').value = privKey + } + clearKeys() { + this.querySelector('#generated_flo_address').value = '' + this.querySelector('#generated_private_key').value = '' + } +}); + +const adBlockerWarning = document.createElement('template') +adBlockerWarning.innerHTML = ` + +
+ `; +window.customElements.define('adblocker-warning', class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(adBlockerWarning.content.cloneNode(true)); + } + connectedCallback() { + const isBrave = navigator.brave !== undefined + this.shadowRoot.querySelector('#adblocker_warning').innerHTML = ` + +

Ad-Blocker Detected!

+

+ Please disable your ad-blocker for optimal experience. Our app doesn't show ads or track activity. +

+ ${isBrave ? `If you have enabled Brave shield then disable it also.` : ''} + `; + } +}); + +const IDBsupport = document.createElement('template') +IDBsupport.innerHTML = ` + + + `; +window.customElements.define('idb-support', class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(IDBsupport.content.cloneNode(true)); + } + + connectedCallback() { + const thisComponent = this; + if ('indexedDB' in window) { + const request = window.indexedDB.open('testDB', 1); + request.onerror = function (event) { + // IndexedDB is not allowed or an error occurred + console.log('IndexedDB is not allowed or encountered an error.'); + thisComponent.shadowRoot.querySelector('#idb_support').style.display = 'flex'; + }; + request.onsuccess = function (event) { + // IndexedDB is allowed and successfully opened the database + thisComponent.remove(); + }; + } else { + // IndexedDB is not supported in this browser + console.log('IndexedDB is not supported.'); + } + } +}) \ No newline at end of file diff --git a/messenger/scripts/components.min.js b/messenger/scripts/components.min.js new file mode 100644 index 0000000..60156f0 --- /dev/null +++ b/messenger/scripts/components.min.js @@ -0,0 +1 @@ +const smChips=document.createElement("template");smChips.innerHTML='
',customElements.define("sm-chips",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smChips.content.cloneNode(!0)),this.chipsWrapper=this.shadowRoot.querySelector(".sm-chips"),this.coverLeft=this.shadowRoot.querySelector(".cover--left"),this.coverRight=this.shadowRoot.querySelector(".cover--right"),this.navButtonLeft=this.shadowRoot.querySelector(".nav-button--left"),this.navButtonRight=this.shadowRoot.querySelector(".nav-button--right"),this.slottedOptions=void 0,this._value=void 0,this.scrollDistance=0,this.assignedElements=[],this.scrollLeft=this.scrollLeft.bind(this),this.scrollRight=this.scrollRight.bind(this),this.fireEvent=this.fireEvent.bind(this),this.setSelectedOption=this.setSelectedOption.bind(this)}get value(){return this._value}set value(t){this.setSelectedOption(t)}scrollLeft(){this.chipsWrapper.scrollBy({left:-this.scrollDistance,behavior:"smooth"})}scrollRight(){this.chipsWrapper.scrollBy({left:this.scrollDistance,behavior:"smooth"})}setSelectedOption(t){this._value!==t&&(this._value=t,this.assignedElements.forEach((e=>{e.value==t?(e.setAttribute("selected",""),e.scrollIntoView({behavior:"smooth",block:"nearest",inline:"center"})):e.removeAttribute("selected")})))}fireEvent(){this.dispatchEvent(new CustomEvent("change",{bubbles:!0,composed:!0,detail:{value:this._value}}))}connectedCallback(){this.setAttribute("role","listbox");const t=this.shadowRoot.querySelector("slot");t.addEventListener("slotchange",(e=>{n.disconnect(),i.disconnect(),this.observeSelf.disconnect(),clearTimeout(this.slotChangeTimeout),this.slotChangeTimeout=setTimeout((()=>{this.assignedElements=t.assignedElements(),this.assignedElements.forEach((t=>{t.hasAttribute("selected")&&(this._value=t.value)})),this.observeSelf.observe(this)}),0)}));const e=new ResizeObserver((t=>{t.forEach((t=>{if(t.contentBoxSize){const e=Array.isArray(t.contentBoxSize)?t.contentBoxSize[0]:t.contentBoxSize;this.scrollDistance=.6*e.inlineSize}else this.scrollDistance=.6*t.contentRect.width}))}));e.observe(this),this.observeSelf=new IntersectionObserver(((t,e)=>{t.forEach((t=>{t.isIntersecting&&!this.hasAttribute("multiline")&&this.assignedElements.length>0&&(n.observe(this.assignedElements[0]),i.observe(this.assignedElements[this.assignedElements.length-1]),e.unobserve(this))}))}),{threshold:1}),this.chipsWrapper.addEventListener("option-clicked",(t=>{this._value!==t.target.value&&(this.setSelectedOption(t.target.value),this.fireEvent())}));const n=new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting?(this.navButtonLeft.classList.add("hide"),this.coverLeft.classList.add("hide")):(this.navButtonLeft.classList.remove("hide"),this.coverLeft.classList.remove("hide"))}))}),{threshold:1,root:this}),i=new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting?(this.navButtonRight.classList.add("hide"),this.coverRight.classList.add("hide")):(this.navButtonRight.classList.remove("hide"),this.coverRight.classList.remove("hide"))}))}),{threshold:1,root:this});this.navButtonLeft.addEventListener("click",this.scrollLeft),this.navButtonRight.addEventListener("click",this.scrollRight)}disconnectedCallback(){this.navButtonLeft.removeEventListener("click",this.scrollLeft),this.navButtonRight.removeEventListener("click",this.scrollRight)}});const smChip=document.createElement("template");smChip.innerHTML=' ',customElements.define("sm-chip",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smChip.content.cloneNode(!0)),this._value=void 0,this.radioButton=this.shadowRoot.querySelector("input"),this.fireEvent=this.fireEvent.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this)}get value(){return this._value}fireEvent(){this.dispatchEvent(new CustomEvent("option-clicked",{bubbles:!0,composed:!0,detail:{value:this._value}}))}handleKeyDown(t){"Enter"!==t.key&&"Space"!==t.key||this.fireEvent()}connectedCallback(){this.setAttribute("role","option"),this.setAttribute("tabindex","0"),this._value=this.getAttribute("value"),this.addEventListener("click",this.fireEvent),this.addEventListener("keydown",this.handleKeyDown)}disconnectedCallback(){this.removeEventListener("click",this.fireEvent),this.removeEventListener("keydown",this.handleKeyDown)}});const smCopy=document.createElement("template");smCopy.innerHTML='

',customElements.define("sm-copy",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smCopy.content.cloneNode(!0)),this.copyContent=this.shadowRoot.querySelector(".copy-content"),this.copyButton=this.shadowRoot.querySelector(".copy-button"),this.copy=this.copy.bind(this)}static get observedAttributes(){return["value"]}set value(t){this.setAttribute("value",t)}get value(){return this.getAttribute("value")}fireEvent(){this.dispatchEvent(new CustomEvent("copy",{composed:!0,bubbles:!0,cancelable:!0}))}copy(){navigator.clipboard.writeText(this.getAttribute("value")).then((t=>this.fireEvent())).catch((t=>console.error(t)))}connectedCallback(){this.copyButton.addEventListener("click",this.copy)}attributeChangedCallback(t,n,o){if("value"===t){const t=this.copyContent.querySelector("slot");if(!t)return;const n=t.assignedNodes();n&&n.length||(t.textContent=o)}}disconnectedCallback(){this.copyButton.removeEventListener("click",this.copy)}});const smForm=document.createElement("template");smForm.innerHTML='
',customElements.define("sm-form",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smForm.content.cloneNode(!0)),this.form=this.shadowRoot.querySelector("form"),this.invalidFieldsCount,this.skipSubmit=!1,this.isFormValid=void 0,this.supportedElements="input, sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio",this.formElements=[],this._requiredElements=[]}static get observedAttributes(){return["skip-submit"]}get validity(){return this.isFormValid}debounce=(callback,wait)=>{let timeoutId=null;return(...args)=>{window.clearTimeout(timeoutId),timeoutId=window.setTimeout((()=>{callback.apply(null,args)}),wait)}};_checkValidity=()=>{this.submitButton&&0!==this._requiredElements.length&&(this.invalidFieldsCount=0,this._requiredElements.forEach((([elem,isWC])=>{(!elem.disabled&&isWC&&!elem.isValid||!isWC&&!elem.checkValidity())&&this.invalidFieldsCount++})),this.isFormValid!==(0===this.invalidFieldsCount)&&(this.isFormValid=0===this.invalidFieldsCount,this.dispatchEvent(new CustomEvent(this.isFormValid?"valid":"invalid",{bubbles:!0,composed:!0})),this.skipSubmit||(this.submitButton.disabled=!this.isFormValid)))};handleKeydown=e=>{if("Enter"===e.key&&e.target.tagName.includes("INPUT"))if(0===this.invalidFieldsCount)this.submitButton&&this.submitButton.click(),this.dispatchEvent(new CustomEvent("submit",{bubbles:!0,composed:!0}));else for(const[elem,isWC]of this._requiredElements)if(isWC?!elem.isValid:!elem.checkValidity()){(elem?.shadowRoot?.lastElementChild||elem).animate([{transform:"translateX(-1rem)"},{transform:"translateX(1rem)"},{transform:"translateX(-0.5rem)"},{transform:"translateX(0.5rem)"},{transform:"translateX(0)"}],{duration:300,easing:"ease"}),isWC?(elem.focusIn(),"SM-INPUT"===elem.tagName&&""===elem.value.trim()&&elem.showError()):elem.focus();break}};reset=()=>{this.formElements.forEach((([elem,isWC])=>{if(isWC)elem.reset();else switch(elem.type){case"checkbox":case"radio":elem.checked=!1;break;default:elem.value=""}})),this._checkValidity()};elementsChanged=()=>{this.formElements=[...this.querySelectorAll(this.supportedElements)].map((elem=>[elem,elem.tagName.includes("-")])),this._requiredElements=this.formElements.filter((([elem])=>elem.hasAttribute("required"))),this.submitButton=this.querySelector('[variant="primary"], [type="submit"]'),this.resetButton=this.querySelector('[type="reset"]'),this.resetButton&&this.resetButton.addEventListener("click",this.reset),this._checkValidity()};checkIfSupported=elem=>1===elem.nodeType&&(elem.tagName.includes("-")||"input"===elem.tagName||elem.querySelector(this.supportedElements));connectedCallback(){const updateFormDecedents=this.debounce(this.elementsChanged,100);this.addEventListener("input",this.debounce(this._checkValidity,100)),this.addEventListener("keydown",this.debounce(this.handleKeydown,100)),this.shadowRoot.querySelector("slot").addEventListener("slotchange",updateFormDecedents),this.mutationObserver=new MutationObserver((mutations=>{mutations.forEach((mutation=>{("childList"===mutation.type&&[...mutation.addedNodes].some((node=>this.checkIfSupported(node)))||[...mutation.removedNodes].some((node=>this.checkIfSupported(node))))&&updateFormDecedents()}))})),this.mutationObserver.observe(this,{childList:!0,subtree:!0})}attributeChangedCallback(name,oldValue,newValue){"skip-submit"===name&&(this.skipSubmit=this.hasAttribute("skip-submit"))}disconnectedCallback(){this.removeEventListener("input",this.debounce(this._checkValidity,100)),this.removeEventListener("keydown",this.debounce(this.handleKeydown,100)),this.mutationObserver.disconnect()}});const smInput=document.createElement("template");smInput.innerHTML='
',customElements.define("sm-input",class extends HTMLElement{#validationState={validatedFor:void 0,isValid:!1,errorMessage:"Please fill out this field."};constructor(){super(),this.attachShadow({mode:"open"}).append(smInput.content.cloneNode(!0)),this.inputParent=this.shadowRoot.querySelector(".input"),this.input=this.shadowRoot.querySelector("input"),this.clearBtn=this.shadowRoot.querySelector(".clear"),this.label=this.shadowRoot.querySelector(".label"),this.feedbackText=this.shadowRoot.querySelector(".feedback-text"),this.outerContainer=this.shadowRoot.querySelector(".outer-container"),this.optionList=this.shadowRoot.querySelector(".datalist"),this._helperText="",this.isRequired=!1,this.datalist=[],this.validationFunction=void 0,this.reflectedAttributes=["value","required","disabled","type","inputmode","readonly","min","max","pattern","minlength","maxlength","step","list","autocomplete"]}static get observedAttributes(){return["value","placeholder","required","disabled","type","inputmode","readonly","min","max","pattern","minlength","maxlength","step","helper-text","error-text","list"]}get value(){return this.input.value}set value(val){val!==this.input.value&&(this.input.value=val,this._value=val,this.checkInput())}get placeholder(){return this.getAttribute("placeholder")}set placeholder(val){this.setAttribute("placeholder",val)}get type(){return this.getAttribute("type")}set type(val){this.setAttribute("type",val)}get validity(){return this.input.validity}get disabled(){return this.hasAttribute("disabled")}set disabled(value){value?(this.inputParent.classList.add("disabled"),this.setAttribute("disabled","")):(this.inputParent.classList.remove("disabled"),this.removeAttribute("disabled"))}get readOnly(){return this.hasAttribute("readonly")}set readOnly(value){value?this.setAttribute("readonly",""):this.removeAttribute("readonly")}set customValidation(val){val&&(this.validationFunction=val)}set errorText(val){this.#validationState.errorText=val}showError=(errorText=this.#validationState.errorText)=>{this.feedbackText.className="feedback-text error",this.feedbackText.innerHTML=` ${errorText}`};set helperText(val){this._helperText=val}get isValid(){if(this.#validationState.validatedFor===this.input.value)return this.#validationState.isValid;const _isValid=this.input.checkValidity();let _validity={isValid:!0,errorText:""};return this.validationFunction&&(_validity=this.validationFunction(this.input.value)),_isValid&&_validity.isValid?(this.feedbackText.className="feedback-text success",this.feedbackText.textContent=""):""!==this.value.trim()&&(_validity.errorText||this.#validationState.errorText)&&this.showError(_validity.errorText||this.#validationState.errorText),this.#validationState.validatedFor=this.input.value,this.#validationState.isValid=_isValid&&_validity.isValid,this.#validationState.errorText=_validity.errorText||this.#validationState.errorText,this.#validationState.isValid}reset=()=>{this.value=""};clear=()=>{this.value="",this.input.focus(),this.fireEvent()};focusIn=()=>{this.input.focus()};focusOut=()=>{this.input.blur()};fireEvent=()=>{let event=new Event("input",{bubbles:!0,cancelable:!0,composed:!0});this.dispatchEvent(event)};searchDatalist=searchKey=>{const filteredData=this.datalist.filter((item=>item.toLowerCase().includes(searchKey.toLowerCase())));if(filteredData.sort(((a,b)=>a.toLowerCase().indexOf(searchKey.toLowerCase())-b.toLowerCase().indexOf(searchKey.toLowerCase()))),filteredData.length){if(this.optionList.children.length>filteredData.length){const optionsToRemove=this.optionList.children.length-filteredData.length;for(let i=0;i{if(this.optionList.children[index])this.optionList.children[index].textContent=item;else{const option=document.createElement("li");option.textContent=item,option.classList.add("datalist-item"),option.setAttribute("tabindex","0"),this.optionList.appendChild(option)}})),this.optionList.classList.remove("hidden")}else this.optionList.classList.add("hidden")};checkInput=e=>{this.hasAttribute("readonly")||(""!==this.input.value?this.clearBtn.classList.remove("hidden"):this.clearBtn.classList.add("hidden")),this.hasAttribute("placeholder")&&""!==this.getAttribute("placeholder").trim()&&(""!==this.input.value?(this.shouldAnimateLabel&&this.inputParent.classList.add("animate-placeholder"),this.label.classList.toggle("hidden",!this.shouldAnimateLabel),this.datalist.length&&(this.searchTimeout&&clearTimeout(this.searchTimeout),this.searchTimeout=setTimeout((()=>{this.searchDatalist(this.input.value.trim())}),100))):(this.shouldAnimateLabel&&this.inputParent.classList.remove("animate-placeholder"),this.label.classList.remove("hidden"),this.feedbackText.textContent="",this.datalist.length&&(this.optionList.innerHTML="",this.optionList.classList.add("hidden"))))};allowOnlyNum=e=>{e.ctrlKey||1===e.key.length&&(("."!==e.key||!e.target.value.includes(".")&&0!==e.target.value.length)&&["0","1","2","3","4","5","6","7","8","9","."].includes(e.key)||e.preventDefault())};handleOptionClick=e=>{this.input.value=e.target.textContent,this.optionList.classList.add("hidden"),this.input.focus()};handleInputNavigation=e=>{"ArrowDown"===e.key?(e.preventDefault(),this.optionList.children.length&&this.optionList.children[0].focus()):"ArrowUp"===e.key&&(e.preventDefault(),this.optionList.children.length&&this.optionList.children[this.optionList.children.length-1].focus())};handleDatalistNavigation=e=>{"ArrowUp"===e.key?(e.preventDefault(),this.shadowRoot.activeElement.previousElementSibling?this.shadowRoot.activeElement.previousElementSibling.focus():this.input.focus()):"ArrowDown"===e.key?(e.preventDefault(),this.shadowRoot.activeElement.nextElementSibling?this.shadowRoot.activeElement.nextElementSibling.focus():this.input.focus()):"Enter"!==e.key&&" "!==e.key||(e.preventDefault(),this.input.value=e.target.textContent,this.optionList.classList.add("hidden"),this.input.focus())};handleFocus=e=>{this.datalist.length&&this.searchDatalist(this.input.value.trim())};handleBlur=e=>{this.datalist.length&&this.optionList.classList.add("hidden")};connectedCallback(){const uuid=crypto.randomUUID();if(this.input.id=uuid,this.label.htmlFor=uuid,this.shouldAnimateLabel=this.hasAttribute("animate"),this.shouldAnimateLabel&&""!==this.placeholder&&this.value&&(this.inputParent.classList.add("animate-placeholder"),this.label.classList.remove("hidden")),this.setAttribute("role","textbox"),"undefined"!=typeof smCompConfig&&smCompConfig["sm-input"]){const config=smCompConfig["sm-input"].find((config=>this.matches(config.selector)));this.customValidation=config?.customValidation}this.input.addEventListener("input",this.checkInput),this.clearBtn.addEventListener("click",this.clear),this.datalist.length&&(this.optionList.addEventListener("click",this.handleOptionClick),this.input.addEventListener("keydown",this.handleInputNavigation),this.optionList.addEventListener("keydown",this.handleDatalistNavigation)),this.input.addEventListener("focusin",this.handleFocus),this.addEventListener("focusout",this.handleBlur)}attributeChangedCallback(name,oldValue,newValue){if(oldValue!==newValue)switch(this.reflectedAttributes.includes(name)&&(this.hasAttribute(name)?this.input.setAttribute(name,this.getAttribute(name)?this.getAttribute(name):""):this.input.removeAttribute(name)),name){case"placeholder":this.label.textContent=newValue,this.setAttribute("aria-label",newValue);break;case"value":this.checkInput();break;case"type":this.hasAttribute("type")&&"number"===this.getAttribute("type")?(this.input.setAttribute("inputmode","decimal"),this.input.addEventListener("keydown",this.allowOnlyNum)):this.input.removeEventListener("keydown",this.allowOnlyNum);break;case"helper-text":this._helperText=newValue;break;case"error-text":this.#validationState.errorText=newValue;break;case"required":this.isRequired=this.hasAttribute("required"),this.isRequired?this.setAttribute("aria-required","true"):this.setAttribute("aria-required","false");break;case"readonly":this.hasAttribute("readonly")?this.inputParent.classList.add("readonly"):this.inputParent.classList.remove("readonly");break;case"disabled":this.hasAttribute("disabled")?this.inputParent.classList.add("disabled"):this.inputParent.classList.remove("disabled");break;case"list":this.hasAttribute("list")&&""!==this.getAttribute("list").trim()&&(this.datalist=this.getAttribute("list").split(","))}}disconnectedCallback(){this.input.removeEventListener("input",this.checkInput),this.clearBtn.removeEventListener("click",this.clear),this.input.removeEventListener("keydown",this.allowOnlyNum),this.optionList.removeEventListener("click",this.handleOptionClick),this.input.removeEventListener("keydown",this.handleInputNavigation),this.optionList.removeEventListener("keydown",this.handleDatalistNavigation),this.input.removeEventListener("focusin",this.handleFocus),this.removeEventListener("focusout",this.handleBlur)}});const smMenu=document.createElement("template");smMenu.innerHTML='
',customElements.define("sm-menu",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smMenu.content.cloneNode(!0)),this.isOpen=!1,this.availableOptions,this.containerDimensions,this.animOptions={duration:200,easing:"ease"},this.optionList=this.shadowRoot.querySelector(".options"),this.menu=this.shadowRoot.querySelector(".menu"),this.icon=this.shadowRoot.querySelector(".icon"),this.expand=this.expand.bind(this),this.collapse=this.collapse.bind(this),this.toggle=this.toggle.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleClick=this.handleClick.bind(this),this.handleClickOutside=this.handleClickOutside.bind(this)}static get observedAttributes(){return["value"]}get value(){return this.getAttribute("value")}set value(n){this.setAttribute("value",n)}expand(){this.isOpen||(this.optionList.classList.remove("hide"),this.optionList.animate([{transform:"translateY(-1rem)",opacity:"0"},{transform:"none",opacity:"1"}],this.animOptions).onfinish=()=>{this.isOpen=!0,this.icon.classList.add("focused"),document.addEventListener("mousedown",this.handleClickOutside);const n=this.optionList.firstElementChild.assignedElements().find((n=>"MENU-OPTION"===n.tagName));n&&n.focus()})}collapse(){this.isOpen&&(this.optionList.animate([{transform:"none",opacity:"1"},{transform:"translateY(-1rem)",opacity:"0"}],this.animOptions).onfinish=()=>{this.isOpen=!1,this.icon.classList.remove("focused"),this.optionList.classList.add("hide"),document.removeEventListener("mousedown",this.handleClickOutside)})}toggle(){this.isOpen?this.collapse():this.expand()}handleClick(n){n.target===this?this.toggle():this.collapse()}handleKeyDown(n){n.target===this?"ArrowDown"===n.key?(n.preventDefault(),this.availableOptions[0].focus()):" "===n.key&&(n.preventDefault(),this.toggle()):"ArrowUp"===n.key?(n.preventDefault(),document.activeElement.previousElementSibling?document.activeElement.previousElementSibling.focus():this.availableOptions[this.availableOptions.length-1].focus()):"ArrowDown"===n.key?(n.preventDefault(),document.activeElement.nextElementSibling?document.activeElement.nextElementSibling.focus():this.availableOptions[0].focus()):"Enter"!==n.key&&" "!==n.key||(n.preventDefault(),n.target.click(),this.collapse(),this.menu.focus())}handleClickOutside(n){this.contains(n.target)||2===n.button||this.collapse()}connectedCallback(){this.setAttribute("role","listbox"),this.setAttribute("aria-label","dropdown menu");this.shadowRoot.querySelector(".options slot").addEventListener("slotchange",(n=>{this.availableOptions=n.target.assignedElements(),this.containerDimensions=this.optionList.getBoundingClientRect()})),this.addEventListener("click",this.handleClick),this.addEventListener("keydown",this.handleKeyDown)}disconnectedCallback(){this.removeEventListener("click",this.handleClick),this.removeEventListener("keydown",this.handleKeyDown),document.removeEventListener("mousedown",this.handleClickOutside)}});const menuOption=document.createElement("template");menuOption.innerHTML='
',customElements.define("menu-option",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(menuOption.content.cloneNode(!0))}connectedCallback(){this.setAttribute("role","option"),this.setAttribute("tabindex","0"),this.addEventListener("keyup",(n=>{"Enter"!==n.key&&" "!==n.key||(n.preventDefault(),this.click())}))}});const smNotifications=document.createElement("template");smNotifications.innerHTML="
",customElements.define("sm-notifications",class extends HTMLElement{constructor(){super(),this.shadow=this.attachShadow({mode:"open"}).append(smNotifications.content.cloneNode(!0)),this.notificationPanel=this.shadowRoot.querySelector(".notification-panel"),this.animationOptions={duration:300,fill:"forwards",easing:"cubic-bezier(0.175, 0.885, 0.32, 1.275)"},this.push=this.push.bind(this),this.createNotification=this.createNotification.bind(this),this.removeNotification=this.removeNotification.bind(this),this.clearAll=this.clearAll.bind(this),this.remove=this.remove.bind(this),this.handleTouchMove=this.handleTouchMove.bind(this),this.startX=0,this.currentX=0,this.endX=0,this.swipeDistance=0,this.swipeDirection="",this.swipeThreshold=0,this.startTime=0,this.swipeTime=0,this.swipeTimeThreshold=200,this.currentTarget=null,this.notificationTimeout=5e3,this.mediaQuery=window.matchMedia("(min-width: 640px)"),this.handleOrientationChange=this.handleOrientationChange.bind(this),this.isBigViewport=!1}set timeout(value){isNaN(value)||(this.notificationTimeout=value)}randString(length){let result="";for(let i=0;i${icon}`:""} ${message} ${action?``:""} `,action&¬ification.querySelector(".action").addEventListener("click",action.callback),notification.querySelector(".close").addEventListener("click",(()=>{this.removeNotification(notification)})),pinned||setTimeout((()=>{this.removeNotification(notification,this.isBigViewport?"left":"top")}),timeout),notification}push(message,options={}){const notification=this.createNotification(message,options);return this.isBigViewport?this.notificationPanel.append(notification):this.notificationPanel.prepend(notification),notification.scrollIntoView({behavior:"smooth"}),this.notificationPanel.animate([{transform:`translateY(${this.isBigViewport?"":"-"}${notification.clientHeight}px)`},{transform:"none"}],this.animationOptions),notification.animate([{transform:"translateY(-1rem)",opacity:"0"},{transform:"none",opacity:"1"}],this.animationOptions).onfinish=e=>{e.target.commitStyles(),e.target.cancel()},notification.id}removeNotification(notification,direction="left"){if(!notification)return;const sign="left"===direction||"top"===direction?"-":"+";this.isBigViewport||"top"!==direction?notification.animate([{transform:this.currentX?`translateX(${this.currentX}px)`:"none",opacity:"1"},{transform:`translateX(calc(${sign}${Math.abs(this.currentX)}px ${sign} 1rem))`,opacity:"0"}],this.animationOptions).onfinish=()=>{notification.remove()}:notification.animate([{transform:this.currentX?`translateY(${this.currentX}px)`:"none",opacity:"1"},{transform:`translateY(calc(${sign}${Math.abs(this.currentX)}px ${sign} 1rem))`,opacity:"0"}],this.animationOptions).onfinish=()=>{notification.remove()}}remove(id){const notification=this.notificationPanel.querySelector(`#${id}`);notification&&this.removeNotification(notification)}clearAll(){Array.from(this.notificationPanel.children).forEach((child=>{this.removeNotification(child)}))}handleTouchMove(e){this.currentX=e.touches[0].clientX-this.startX,this.currentTarget.style.transform=`translateX(${this.currentX}px)`}handleOrientationChange(e){this.isBigViewport=e.matches,e.matches}connectedCallback(){this.handleOrientationChange(this.mediaQuery),this.mediaQuery.addEventListener("change",this.handleOrientationChange),this.notificationPanel.addEventListener("touchstart",(e=>{e.target.closest(".close")?this.removeNotification(e.target.closest(".notification")):e.target.closest(".notification")&&(this.swipeThreshold=e.target.closest(".notification").getBoundingClientRect().width/2,this.currentTarget=e.target.closest(".notification"),this.startTime=Date.now(),this.startX=e.touches[0].clientX,this.startY=e.touches[0].clientY,this.notificationPanel.addEventListener("touchmove",this.handleTouchMove,{passive:!0}))}),{passive:!0}),this.notificationPanel.addEventListener("touchend",(e=>{this.endX=e.changedTouches[0].clientX,this.endY=e.changedTouches[0].clientY,this.swipeDistance=Math.abs(this.endX-this.startX),this.swipeTime=Date.now()-this.startTime,this.endX>this.startX?this.swipeDirection="right":this.swipeDirection="left",this.swipeTime50&&this.removeNotification(this.currentTarget,this.swipeDirection):this.swipeDistance>this.swipeThreshold?this.removeNotification(this.currentTarget,this.swipeDirection):this.currentTarget.animate([{transform:`translateX(${this.currentX}px)`},{transform:"none"}],this.animationOptions).onfinish=e=>{e.target.commitStyles(),e.target.cancel()},this.notificationPanel.removeEventListener("touchmove",this.handleTouchMove),this.currentX=0}))}disconnectedCallback(){mediaQueryList.removeEventListener("change",handleOrientationChange)}});class Stack{constructor(){this.items=[]}push(t){this.items.push(t)}pop(){return 0==this.items.length?"Underflow":this.items.pop()}peek(){return this.items[this.items.length-1]}}const popupStack=new Stack,smPopup=document.createElement("template");smPopup.innerHTML=' ',customElements.define("sm-popup",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smPopup.content.cloneNode(!0)),this.allowClosing=!1,this.isOpen=!1,this.offset=0,this.touchStartY=0,this.touchEndY=0,this.touchStartTime=0,this.touchEndTime=0,this.touchEndAnimation=void 0,this.focusable,this.autoFocus,this.mutationObserver,this.popupContainer=this.shadowRoot.querySelector(".popup-container"),this.backdrop=this.shadowRoot.querySelector(".backdrop"),this.dialogBox=this.shadowRoot.querySelector(".popup"),this.popupBodySlot=this.shadowRoot.querySelector(".popup-body slot"),this.popupHeader=this.shadowRoot.querySelector(".popup-top"),this.resumeScrolling=this.resumeScrolling.bind(this),this.setStateOpen=this.setStateOpen.bind(this),this.show=this.show.bind(this),this.hide=this.hide.bind(this),this.handleTouchStart=this.handleTouchStart.bind(this),this.handleTouchMove=this.handleTouchMove.bind(this),this.handleTouchEnd=this.handleTouchEnd.bind(this),this.detectFocus=this.detectFocus.bind(this),this.handleSoftDismiss=this.handleSoftDismiss.bind(this),this.debounce=this.debounce.bind(this)}static get observedAttributes(){return["open"]}get open(){return this.isOpen}animateTo(t,e,i){let s=t.animate(e,{...i,fill:"both"});return s.finished.then((()=>{s.commitStyles(),s.cancel()})),s}resumeScrolling(){let t=document.body.style.top;window.scrollTo(0,-1*parseInt(t||"0")),document.body.style.overflow="",document.body.style.top="initial"}setStateOpen(){if(!this.isOpen||this.offset){let t=window.innerWidth>640?"scale(1.1)":`translateY(${this.offset?`${this.offset}px`:"100%"})`;this.animateTo(this.dialogBox,[{opacity:this.offset?1:0,transform:t},{opacity:1,transform:"none"}],{duration:300,easing:"ease"})}}show(t={}){let{pinned:e=!1,payload:i}=t;if(this.isOpen)return;let s={duration:300,easing:"ease"};return this.payload=i,popupStack.push({popup:this,permission:e}),popupStack.items.length>1&&this.animateTo(popupStack.items[popupStack.items.length-2].popup.shadowRoot.querySelector(".popup"),[{transform:"none"},{transform:window.innerWidth>640?"scale(0.95)":"translateY(-1.5rem)"}],s),this.popupContainer.classList.remove("hide"),this.offset||(this.backdrop.animate([{opacity:0},{opacity:1}],s).onfinish=()=>{this.resolveOpen(this.payload)},this.dispatchEvent(new CustomEvent("popupopened",{bubbles:!0,composed:!0,detail:{payload:this.payload}})),document.body.style.overflow="hidden",document.body.style.top=`-${window.scrollY}px`),this.setStateOpen(),this.pinned=e,this.isOpen=!0,setTimeout((()=>{let t=this.autoFocus||this.focusable?.[0]||this.dialogBox;t&&(t.tagName.includes("-")?t.focusIn():t.focus())}),0),this.hasAttribute("open")||(this.setAttribute("open",""),this.addEventListener("keydown",this.detectFocus),this.resizeObserver.observe(this),this.mutationObserver.observe(this,{attributes:!0,childList:!0,subtree:!0}),this.popupHeader.addEventListener("touchstart",this.handleTouchStart,{passive:!0}),this.backdrop.addEventListener("mousedown",this.handleSoftDismiss)),{opened:new Promise((t=>{this.resolveOpen=t})),closed:new Promise((t=>{this.resolveClose=t}))}}hide(t={}){let{payload:e}=t,i={duration:150,easing:"ease"};this.backdrop.animate([{opacity:1},{opacity:0}],i),this.animateTo(this.dialogBox,[{opacity:1,transform:window.innerWidth>640?"none":`translateY(${this.offset?`${this.offset}px`:"0"})`},{opacity:0,transform:window.innerWidth>640?"scale(1.1)":"translateY(100%)"}],i).finished.finally((()=>{this.popupContainer.classList.add("hide"),this.dialogBox.style="",this.removeAttribute("open"),this.forms.length&&this.forms.forEach((t=>t.reset())),this.dispatchEvent(new CustomEvent("popupclosed",{bubbles:!0,composed:!0,detail:{payload:e||this.payload}})),this.resolveClose(e||this.payload),this.isOpen=!1})),popupStack.pop(),popupStack.items.length?this.animateTo(popupStack.items[popupStack.items.length-1].popup.shadowRoot.querySelector(".popup"),[{transform:window.innerWidth>640?"scale(0.95)":"translateY(-1.5rem)"},{transform:"none"}],i):this.resumeScrolling(),this.resizeObserver.disconnect(),this.mutationObserver.disconnect(),this.removeEventListener("keydown",this.detectFocus),this.popupHeader.removeEventListener("touchstart",this.handleTouchStart,{passive:!0}),this.backdrop.removeEventListener("mousedown",this.handleSoftDismiss)}handleTouchStart(t){this.offset=0,this.popupHeader.addEventListener("touchmove",this.handleTouchMove,{passive:!0}),this.popupHeader.addEventListener("touchend",this.handleTouchEnd,{passive:!0}),this.touchStartY=t.changedTouches[0].clientY,this.touchStartTime=t.timeStamp}handleTouchMove(t){this.touchStartY{this.dialogBox.style.transform=`translateY(${this.offset}px)`})))}handleTouchEnd(t){if(this.touchEndTime=t.timeStamp,cancelAnimationFrame(this.touchEndAnimation),this.touchEndY=t.changedTouches[0].clientY,this.threshold=.3*this.dialogBox.getBoundingClientRect().height,this.touchEndTime-this.touchStartTime>200)if(this.touchEndY-this.touchStartY>this.threshold){if(this.pinned)return void this.setStateOpen();this.hide()}else this.setStateOpen();else if(this.touchEndY>this.touchStartY){if(this.pinned)return void this.setStateOpen();this.hide()}this.popupHeader.removeEventListener("touchmove",this.handleTouchMove,{passive:!0}),this.popupHeader.removeEventListener("touchend",this.handleTouchEnd,{passive:!0})}detectFocus(t){if("Tab"===t.key&&this.focusable.length){if(!this.firstFocusable)for(let e=0;e=0;i--)if(!this.focusable[i].disabled){this.lastFocusable=this.focusable[i];break}t.shiftKey&&document.activeElement===this.firstFocusable?(t.preventDefault(),this.lastFocusable.tagName.includes("SM-")?this.lastFocusable.focusIn():this.lastFocusable.focus()):t.shiftKey||document.activeElement!==this.lastFocusable||(t.preventDefault(),this.firstFocusable.tagName.includes("SM-")?this.firstFocusable.focusIn():this.firstFocusable.focus())}}updateFocusableList(){this.focusable=this.querySelectorAll('sm-button:not([disabled]), button:not([disabled]), [href], sm-input, input:not([readonly]), sm-select, select, sm-checkbox, sm-textarea, textarea, [tabindex]:not([tabindex="-1"])'),this.autoFocus=this.querySelector("[autofocus]"),this.firstFocusable=null,this.lastFocusable=null}handleSoftDismiss(){this.pinned?this.dialogBox.animate([{transform:"translateX(-1rem)"},{transform:"translateX(1rem)"},{transform:"translateX(-0.5rem)"},{transform:"translateX(0.5rem)"},{transform:"translateX(0)"}],{duration:300,easing:"ease"}):this.hide()}debounce(t,e){let i=null;return(...s)=>{window.clearTimeout(i),i=window.setTimeout((()=>{t.apply(null,s)}),e)}}connectedCallback(){this.popupBodySlot.addEventListener("slotchange",this.debounce((()=>{this.forms=this.querySelectorAll("sm-form"),this.updateFocusableList()}),0)),this.resizeObserver=new ResizeObserver((t=>{t.forEach((t=>{if(t.contentBoxSize){let e=Array.isArray(t.contentBoxSize)?t.contentBoxSize[0]:t.contentBoxSize;this.threshold=.3*e.blockSize.height}else this.threshold=.3*t.contentRect.height}))})),this.mutationObserver=new MutationObserver((t=>{this.updateFocusableList()}))}disconnectedCallback(){this.resizeObserver.disconnect(),this.mutationObserver.disconnect(),this.removeEventListener("keydown",this.detectFocus),this.popupHeader.removeEventListener("touchstart",this.handleTouchStart,{passive:!0}),this.backdrop.removeEventListener("mousedown",this.handleSoftDismiss)}attributeChangedCallback(t){"open"===t&&this.hasAttribute("open")&&this.show()}});const smSwitch=document.createElement("template");smSwitch.innerHTML='\t',customElements.define("sm-switch",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smSwitch.content.cloneNode(!0)),this.switch=this.shadowRoot.querySelector(".switch"),this.input=this.shadowRoot.querySelector("input"),this.isChecked=!1,this.isDisabled=!1,this.dispatch=this.dispatch.bind(this)}static get observedAttributes(){return["disabled","checked"]}get disabled(){return this.isDisabled}set disabled(e){e?this.setAttribute("disabled",""):this.removeAttribute("disabled")}get checked(){return this.isChecked}set checked(e){e?this.setAttribute("checked",""):this.removeAttribute("checked")}get value(){return this.isChecked}reset(){}dispatch(){this.dispatchEvent(new CustomEvent("change",{bubbles:!0,composed:!0,detail:{value:this.isChecked}}))}connectedCallback(){this.addEventListener("keydown",(e=>{" "!==e.key||this.isDisabled||(e.preventDefault(),this.input.click())})),this.input.addEventListener("click",(e=>{this.input.checked?this.checked=!0:this.checked=!1,this.dispatch()}))}attributeChangedCallback(e,t,n){t!==n&&("disabled"===e?this.hasAttribute("disabled")?this.disabled=!0:this.disabled=!1:"checked"===e&&(this.hasAttribute("checked")?(this.isChecked=!0,this.input.checked=!0):(this.isChecked=!1,this.input.checked=!1)))}});const smSelect=document.createElement("template");smSelect.innerHTML='
',customElements.define("sm-select",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smSelect.content.cloneNode(!0)),this.focusIn=this.focusIn.bind(this),this.reset=this.reset.bind(this),this.open=this.open.bind(this),this.collapse=this.collapse.bind(this),this.toggle=this.toggle.bind(this),this.handleOptionsNavigation=this.handleOptionsNavigation.bind(this),this.handleOptionSelection=this.handleOptionSelection.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.handleClickOutside=this.handleClickOutside.bind(this),this.selectOption=this.selectOption.bind(this),this.debounce=this.debounce.bind(this),this.elementsChanged=this.elementsChanged.bind(this),this.availableOptions=[],this.previousOption,this.isOpen=!1,this.label="",this.defaultSelected="",this.isUnderViewport=!1,this.animationOptions={duration:300,fill:"forwards",easing:"ease"},this.optionList=this.shadowRoot.querySelector(".options"),this.selection=this.shadowRoot.querySelector(".selection"),this.selectedOptionText=this.shadowRoot.querySelector(".selected-option-text")}static get observedAttributes(){return["disabled","label","readonly"]}get value(){return this.getAttribute("value")}set value(t){const e=this.availableOptions.find((e=>e.getAttribute("value")===t));e?(this.setAttribute("value",t),this.selectOption(e)):console.warn(`There is no option with ${t} as value`)}debounce(t,e){let n=null;return(...i)=>{window.clearTimeout(n),n=window.setTimeout((()=>{t.apply(null,i)}),e)}}reset(t=!0){if(this.availableOptions[0]&&this.previousOption!==this.availableOptions[0]){const e=this.availableOptions.find((t=>t.hasAttribute("selected")))||this.availableOptions[0];this.value=e.getAttribute("value"),t&&this.fireEvent()}}selectOption(t){this.previousOption!==t&&(this.querySelectorAll("[selected").forEach((t=>t.removeAttribute("selected"))),this.selectedOptionText.textContent=`${this.label}${t.textContent}`,t.setAttribute("selected",""),this.previousOption=t)}focusIn(){this.selection.focus()}open(){this.availableOptions.forEach((t=>t.setAttribute("tabindex",0))),this.optionList.classList.remove("hidden"),this.isUnderViewport=this.getBoundingClientRect().bottom+this.optionList.getBoundingClientRect().height>window.innerHeight,this.isUnderViewport?this.setAttribute("isUnder",""):this.removeAttribute("isUnder"),this.optionList.animate([{transform:`translateY(${this.isUnderViewport?"":"-"}0.5rem)`,opacity:0},{transform:"translateY(0)",opacity:1}],this.animationOptions),this.setAttribute("open",""),this.style.zIndex=1e3,(this.availableOptions.find((t=>t.hasAttribute("selected")))||this.availableOptions[0]).focus(),document.addEventListener("mousedown",this.handleClickOutside),this.isOpen=!0}collapse(){this.removeAttribute("open"),this.optionList.animate([{transform:"translateY(0)",opacity:1},{transform:`translateY(${this.isUnderViewport?"":"-"}0.5rem)`,opacity:0}],this.animationOptions).onfinish=()=>{this.availableOptions.forEach((t=>t.removeAttribute("tabindex"))),document.removeEventListener("mousedown",this.handleClickOutside),this.optionList.classList.add("hidden"),this.isOpen=!1,this.style.zIndex="auto"}}toggle(){this.isOpen||this.hasAttribute("disabled")?this.collapse():this.open()}fireEvent(){this.dispatchEvent(new CustomEvent("change",{bubbles:!0,composed:!0,detail:{value:this.value}}))}handleOptionsNavigation(t){"ArrowUp"===t.key?(t.preventDefault(),document.activeElement.previousElementSibling?document.activeElement.previousElementSibling.focus():this.availableOptions[this.availableOptions.length-1].focus()):"ArrowDown"===t.key&&(t.preventDefault(),document.activeElement.nextElementSibling?document.activeElement.nextElementSibling.focus():this.availableOptions[0].focus())}handleOptionSelection(t){this.previousOption!==document.activeElement&&(this.value=document.activeElement.getAttribute("value"),this.fireEvent())}handleClick(t){t.target===this?this.toggle():(this.handleOptionSelection(),this.collapse())}handleKeydown(t){t.target===this?this.isOpen&&"ArrowDown"===t.key?(t.preventDefault(),(this.availableOptions.find((t=>t.hasAttribute("selected")))||this.availableOptions[0]).focus(),this.handleOptionSelection(t)):" "===t.key&&(t.preventDefault(),this.toggle()):(this.handleOptionsNavigation(t),this.handleOptionSelection(t),["Enter"," ","Escape","Tab"].includes(t.key)&&(t.preventDefault(),this.collapse(),this.focusIn()))}handleClickOutside(t){this.isOpen&&!this.contains(t.target)&&this.collapse()}elementsChanged(){this.availableOptions=[...this.querySelectorAll("sm-option")],this.reset(!1),this.defaultSelected=this.value}connectedCallback(){this.setAttribute("role","listbox"),this.hasAttribute("disabled")||this.hasAttribute("readonly")||(this.selection.setAttribute("tabindex","0"),this.addEventListener("click",this.handleClick),this.addEventListener("keydown",this.handleKeydown));const t=this.debounce(this.elementsChanged,100);this.shadowRoot.querySelector("slot").addEventListener("slotchange",t),this.mutationObserver=new MutationObserver((e=>{let n=!1;if(e.forEach((e=>{switch(e.type){case"childList":t();break;case"attributes":n=!0}})),n){const t=this.availableOptions.find((t=>t.hasAttribute("selected")))||this.availableOptions[0];this.selectedOptionText.textContent=`${this.label}${t.textContent}`,this.setAttribute("value",t.getAttribute("value"))}})),this.mutationObserver.observe(this,{subtree:!0,childList:!0,attributeFilter:["selected"]}),new IntersectionObserver(((t,e)=>{t.forEach((t=>{if(t.isIntersecting){this.selection.getBoundingClientRect().left *{ padding: 0; margin: 0; -webkit-box-sizing: border-box; box-sizing: border-box;} :host{ display: -webkit-box; display: -ms-flexbox; display: flex; overflow: hidden; border-radius: var(--border-radius, 0.3rem);}.option{ position: relative; display: flex; -webkit-box-align: center; -ms-flex-align: center; align-items: center; width: 100%; gap: 0.5rem; padding: var(--padding, 0.6rem 1rem); cursor: pointer; outline: none; user-select: none;}.option::before{ position: absolute; content: \'\'; display: block; width: 0.2rem; height: 1em; left: 0; border-radius: 0 1em 1em 0; background: rgba(var(--text-color,(17,17,17)), 0.5); transition: all 0.2s ease-in-out; opacity: 0;}:host(:focus){ outline: none; background: rgba(var(--text-color,(17,17,17)), 0.1);}:host(:focus) .option::before{ opacity: 1}:host([selected]) .option::before{ opacity: 1; background: var(--accent-color, teal);}@media (hover: hover){ .option:hover{ background: rgba(var(--text-color,(17,17,17)), 0.1); } :host(:not([selected]):hover) .option::before{ opacity: 1 }}
',customElements.define("sm-option",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smOption.content.cloneNode(!0))}connectedCallback(){this.setAttribute("role","option")}});const spinner=document.createElement("template");spinner.innerHTML='';class SpinnerLoader extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(spinner.content.cloneNode(!0))}}window.customElements.define("sm-spinner",SpinnerLoader);const smTextarea=document.createElement("template");smTextarea.innerHTML=' ',customElements.define("sm-textarea",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smTextarea.content.cloneNode(!0)),this.textarea=this.shadowRoot.querySelector("textarea"),this.textareaBox=this.shadowRoot.querySelector(".textarea"),this.placeholder=this.shadowRoot.querySelector(".placeholder"),this.reflectedAttributes=["disabled","required","readonly","rows","minlength","maxlength"],this.reset=this.reset.bind(this),this.focusIn=this.focusIn.bind(this),this.fireEvent=this.fireEvent.bind(this),this.checkInput=this.checkInput.bind(this)}static get observedAttributes(){return["disabled","value","placeholder","required","readonly","rows","minlength","maxlength"]}get value(){return this.textarea.value}set value(e){this.setAttribute("value",e),this.fireEvent()}get disabled(){return this.hasAttribute("disabled")}set disabled(e){e?this.setAttribute("disabled",""):this.removeAttribute("disabled")}get isValid(){return this.textarea.checkValidity()}reset(){this.setAttribute("value","")}focusIn(){this.textarea.focus()}fireEvent(){let e=new Event("input",{bubbles:!0,cancelable:!0,composed:!0});this.dispatchEvent(e)}checkInput(){this.hasAttribute("placeholder")&&""!==this.getAttribute("placeholder")&&(""!==this.textarea.value?this.placeholder.classList.add("hide"):this.placeholder.classList.remove("hide"))}connectedCallback(){this.textarea.addEventListener("input",(e=>{this.textareaBox.dataset.value=this.textarea.value,this.checkInput()}))}attributeChangedCallback(e,t,n){this.reflectedAttributes.includes(e)?this.hasAttribute(e)?this.textarea.setAttribute(e,this.getAttribute(e)?this.getAttribute(e):""):this.textContent.removeAttribute(e):"placeholder"===e?this.placeholder.textContent=this.getAttribute("placeholder"):"value"===e&&(this.textarea.value=n,this.textareaBox.dataset.value=n,this.checkInput())}});const textField=document.createElement("template");textField.innerHTML='
',customElements.define("text-field",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(textField.content.cloneNode(!0)),this.textField=this.shadowRoot.querySelector(".text-field"),this.textContainer=this.textField.children[0],this.editButton=this.textField.querySelector(".edit-button"),this.isTextEditable=!1,this.isDisabled=!1}static get observedAttributes(){return["disabled"]}get value(){return this.text}set value(val){this.text=val,this.textContainer.textContent=val,this.setAttribute("value",val)}set disabled(val){this.isDisabled=val,this.isDisabled?this.setAttribute("disabled",""):this.removeAttribute("disabled")}setEditable=()=>{this.isTextEditable||(this.textContainer.contentEditable=!0,this.setAttribute("editable",""),this.textContainer.focus(),document.execCommand("selectAll",!1,null),this.editButton.textContent="Done",this.isTextEditable=!0)};setNonEditable=()=>{this.isTextEditable&&(this.textContainer.contentEditable=!1,this.removeAttribute("editable"),this.text!==this.textContainer.textContent.trim()&&(this.setAttribute("value",this.textContainer.textContent),this.text=this.textContainer.textContent.trim(),this.dispatchEvent(new CustomEvent("change",{bubbles:!0,cancelable:!0,composed:!0}))),this.editButton.textContent="Edit",this.isTextEditable=!1)};toggleEditable=()=>{this.isTextEditable?this.setNonEditable():this.setEditable()};revert=()=>{this.textContainer.isContentEditable&&(this.value=this.text,this.setNonEditable())};connectedCallback(){this.text,this.hasAttribute("value")&&(this.text=this.getAttribute("value"),this.textContainer.textContent=this.text),this.hasAttribute("disabled")?this.isDisabled=!0:this.isDisabled=!1,this.isDisabled||(this.textContainer.addEventListener("dblclick",this.setEditable),this.editButton.addEventListener("click",this.toggleEditable))}attributeChangedCallback(name){"disabled"===name&&(this.hasAttribute("disabled")?(this.editButton.classList.add("hide"),this.textContainer.removeEventListener("dblclick",this.setEditable),this.editButton.removeEventListener("click",this.toggleEditable),this.revert()):(this.editButton.classList.remove("hide"),this.textContainer.addEventListener("dblclick",this.setEditable),this.editButton.addEventListener("click",this.toggleEditable)))}disconnectedCallback(){this.textContainer.removeEventListener("dblclick",this.setEditable),this.editButton.removeEventListener("click",this.toggleEditable)}});const themeToggle=document.createElement("template");themeToggle.innerHTML=' ';class ThemeToggle extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(themeToggle.content.cloneNode(!0)),this.isChecked=!1,this.hasTheme="light",this.toggleState=this.toggleState.bind(this),this.fireEvent=this.fireEvent.bind(this),this.handleThemeChange=this.handleThemeChange.bind(this)}static get observedAttributes(){return["checked"]}daylight(){this.hasTheme="light",document.body.dataset.theme="light",this.setAttribute("aria-checked","false")}nightlight(){this.hasTheme="dark",document.body.dataset.theme="dark",this.setAttribute("aria-checked","true")}toggleState(){if(!document.startViewTransition)return this.toggleAttribute("checked"),void this.fireEvent();document.startViewTransition((()=>{this.toggleAttribute("checked"),this.fireEvent()}))}handleKeyDown(e){" "===e.key&&this.toggleState()}handleThemeChange(e){e.detail.theme!==this.hasTheme&&("dark"===e.detail.theme?this.setAttribute("checked",""):this.removeAttribute("checked"))}fireEvent(){this.dispatchEvent(new CustomEvent("themechange",{bubbles:!0,composed:!0,detail:{theme:this.hasTheme}}))}connectedCallback(){this.setAttribute("role","switch"),this.setAttribute("aria-label","theme toggle"),"dark"===localStorage.getItem(`${window.location.hostname}-theme`)?(this.nightlight(),this.setAttribute("checked","")):"light"===localStorage.getItem(`${window.location.hostname}-theme`)?(this.daylight(),this.removeAttribute("checked")):window.matchMedia("(prefers-color-scheme: dark)").matches?(this.nightlight(),this.setAttribute("checked","")):(this.daylight(),this.removeAttribute("checked")),this.addEventListener("click",this.toggleState),this.addEventListener("keydown",this.handleKeyDown),document.addEventListener("themechange",this.handleThemeChange)}disconnectedCallback(){this.removeEventListener("click",this.toggleState),this.removeEventListener("keydown",this.handleKeyDown),document.removeEventListener("themechange",this.handleThemeChange)}attributeChangedCallback(e,t,n){"checked"===e&&(this.hasAttribute("checked")?(this.nightlight(),localStorage.setItem(`${window.location.hostname}-theme`,"dark")):(this.daylight(),localStorage.setItem(`${window.location.hostname}-theme`,"light")))}}window.customElements.define("theme-toggle",ThemeToggle);const colorGrid=document.createElement("template");colorGrid.innerHTML='\n\n
\n
',customElements.define("color-grid",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(colorGrid.content.cloneNode(!0)),this.colorArray=[],this.container=this.shadowRoot.querySelector(".color-tile-container"),this.handleChange=this.handleChange.bind(this),this.setCheckMark=this.setCheckMark.bind(this)}set colors(arr){this.colorArray=arr,this.renderTiles()}set selectedColor(color){if(this.colorArray.includes(color)&&this.container.querySelector(`[data-color="${color}"]`)){const selectedTile=this.container.querySelector(`[data-color="${color}"]`);selectedTile&&(selectedTile.querySelector("input").checked=!0,this.setCheckMark(selectedTile))}}randString(length){let result="",characters="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";for(let i=0;i{const label=document.createElement("label");label.classList.add("color-tile"),label.setAttribute("data-color",color),color.includes("--")?label.setAttribute("style",`background-color: var(${color})`):label.setAttribute("style",`background-color: ${color}`),label.innerHTML=`\n \n `,frag.append(label)})),this.container.append(frag)}setCheckMark(target){target.parentNode.querySelectorAll(".checkmark").forEach((checkmark=>checkmark.remove()));const checkMark=document.createElement("div");checkMark.classList.add("checkmark"),checkMark.innerHTML='\n \n ',target.append(checkMark)}handleChange(e){const clickedTile=e.target.closest(".color-tile");this.setCheckMark(clickedTile);const clickedTileColor=clickedTile.dataset.color,tileSelected=new CustomEvent("colorselected",{bubbles:!0,composed:!0,detail:{value:clickedTileColor}});this.dispatchEvent(tileSelected)}connectedCallback(){this.container.addEventListener("change",this.handleChange)}disconnectedCallback(){this.container.removeEventListener("change",this.handleChange)}}),window.customElements.define("keys-generator",class extends HTMLElement{constructor(){super(),this.innerHTML='\n \n
\n
\n \n
\n \n

Keep your keys safe!

\n
\n

Don\'t share with anyone. Once lost private key can\'t be recovered.

\n
\n
\n
\n
\n
FLO address
\n \n
\n
\n
Private key
\n \n
\n
\n \n

You can use these FLO credentials with other RanchiMall apps too.

\n
\n '}get keys(){return{floID:this.querySelector("#generated_flo_address").value,privKey:this.querySelector("#generated_private_key").value}}generateKeys(){const{floID:floID,privKey:privKey}=floCrypto.generateNewID();this.querySelector("#generated_flo_address").value=floID,this.querySelector("#generated_private_key").value=privKey}clearKeys(){this.querySelector("#generated_flo_address").value="",this.querySelector("#generated_private_key").value=""}});const adBlockerWarning=document.createElement("template");adBlockerWarning.innerHTML='\n \n
\n ',window.customElements.define("adblocker-warning",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.shadowRoot.appendChild(adBlockerWarning.content.cloneNode(!0))}connectedCallback(){const isBrave=void 0!==navigator.brave;this.shadowRoot.querySelector("#adblocker_warning").innerHTML=`\n \n

Ad-Blocker Detected!

\n

\n Please disable your ad-blocker for optimal experience. Our app doesn't show ads or track activity. \n

\n ${isBrave?"If you have enabled Brave shield then disable it also.":""}\n `}});const IDBsupport=document.createElement("template");IDBsupport.innerHTML='\n \n \n ',window.customElements.define("idb-support",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.shadowRoot.appendChild(IDBsupport.content.cloneNode(!0))}connectedCallback(){const thisComponent=this;if("indexedDB"in window){const request=window.indexedDB.open("testDB",1);request.onerror=function(event){console.log("IndexedDB is not allowed or encountered an error."),thisComponent.shadowRoot.querySelector("#idb_support").style.display="flex"},request.onsuccess=function(event){thisComponent.remove()}}else console.log("IndexedDB is not supported.")}}); \ No newline at end of file diff --git a/messenger/scripts/floBlockchainAPI.js b/messenger/scripts/floBlockchainAPI.js new file mode 100644 index 0000000..9dedb90 --- /dev/null +++ b/messenger/scripts/floBlockchainAPI.js @@ -0,0 +1,1044 @@ +(function (EXPORTS) { //floBlockchainAPI v3.0.1b + /* FLO Blockchain Operator to send/receive data from blockchain using API calls via FLO Blockbook*/ + 'use strict'; + const floBlockchainAPI = EXPORTS; + + const DEFAULT = { + blockchain: floGlobals.blockchain, + apiURL: { + FLO: ['https://blockbook.ranchimall.net/'], + FLO_TEST: [] + }, + sendAmt: 0.0003, + fee: 0.0002, + minChangeAmt: 0.0002, + receiverID: floGlobals.adminID + }; + + const SATOSHI_IN_BTC = 1e8; + const isUndefined = val => typeof val === 'undefined'; + + const util = floBlockchainAPI.util = {}; + + util.Sat_to_FLO = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8)); + util.FLO_to_Sat = value => parseInt(value * SATOSHI_IN_BTC); + util.toFixed = value => parseFloat((value).toFixed(8)); + + Object.defineProperties(floBlockchainAPI, { + sendAmt: { + get: () => DEFAULT.sendAmt, + set: amt => !isNaN(amt) ? DEFAULT.sendAmt = amt : null + }, + fee: { + get: () => DEFAULT.fee, + set: fee => !isNaN(fee) ? DEFAULT.fee = fee : null + }, + defaultReceiver: { + get: () => DEFAULT.receiverID, + set: floID => DEFAULT.receiverID = floID + }, + blockchain: { + get: () => DEFAULT.blockchain + } + }); + + if (floGlobals.sendAmt) floBlockchainAPI.sendAmt = floGlobals.sendAmt; + if (floGlobals.fee) floBlockchainAPI.fee = floGlobals.fee; + + Object.defineProperties(floGlobals, { + sendAmt: { + get: () => DEFAULT.sendAmt, + set: amt => !isNaN(amt) ? DEFAULT.sendAmt = amt : null + }, + fee: { + get: () => DEFAULT.fee, + set: fee => !isNaN(fee) ? DEFAULT.fee = fee : null + } + }); + + const allServerList = new Set(floGlobals.apiURL && floGlobals.apiURL[DEFAULT.blockchain] ? floGlobals.apiURL[DEFAULT.blockchain] : DEFAULT.apiURL[DEFAULT.blockchain]); + + var serverList = Array.from(allServerList); + var curPos = floCrypto.randInt(0, serverList.length - 1); + + function fetch_retry(apicall, rm_node) { + return new Promise((resolve, reject) => { + let i = serverList.indexOf(rm_node) + if (i != -1) serverList.splice(i, 1); + curPos = floCrypto.randInt(0, serverList.length - 1); + fetch_api(apicall, false) + .then(result => resolve(result)) + .catch(error => reject(error)); + }) + } + + function fetch_api(apicall, ic = true) { + return new Promise((resolve, reject) => { + if (serverList.length === 0) { + if (ic) { + serverList = Array.from(allServerList); + curPos = floCrypto.randInt(0, serverList.length - 1); + fetch_api(apicall, false) + .then(result => resolve(result)) + .catch(error => reject(error)); + } else + reject("No FLO blockbook server working"); + } else { + let serverURL = serverList[curPos]; + fetch(serverURL + apicall).then(response => { + if (response.ok) + response.json().then(data => resolve(data)); + else { + fetch_retry(apicall, serverURL) + .then(result => resolve(result)) + .catch(error => reject(error)); + } + }).catch(error => { + fetch_retry(apicall, serverURL) + .then(result => resolve(result)) + .catch(error => reject(error)); + }) + } + }) + } + + Object.defineProperties(floBlockchainAPI, { + serverList: { + get: () => Array.from(serverList) + }, + current_server: { + get: () => serverList[curPos] + } + }); + + //Promised function to get data from API + const promisedAPI = floBlockchainAPI.promisedAPI = floBlockchainAPI.fetch = function (apicall, query_params = undefined) { + return new Promise((resolve, reject) => { + if (!isUndefined(query_params)) + apicall += '?' + new URLSearchParams(JSON.parse(JSON.stringify(query_params))).toString(); + //console.debug(apicall); + fetch_api(apicall) + .then(result => resolve(result)) + .catch(error => reject(error)); + }); + } + + //Get balance for the given Address + const getBalance = floBlockchainAPI.getBalance = function (addr) { + return new Promise((resolve, reject) => { + let api = `api/address/${addr}`; + promisedAPI(api, { details: "basic" }) + .then(result => resolve(result["balance"])) + .catch(error => reject(error)) + }); + } + + function getScriptPubKey(address) { + var tx = bitjs.transaction(); + tx.addoutput(address, 0); + let outputBuffer = tx.outputs.pop().script; + return Crypto.util.bytesToHex(outputBuffer) + } + + const getUTXOs = address => new Promise((resolve, reject) => { + promisedAPI(`api/utxo/${address}`, { confirmed: true }).then(utxos => { + let scriptPubKey = getScriptPubKey(address); + utxos.forEach(u => u.scriptPubKey = scriptPubKey); + resolve(utxos); + }).catch(error => reject(error)) + }) + + //create a transaction with single sender + const createTx = function (senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + else if (!floCrypto.validateFloID(senderAddr, true)) + return reject(`Invalid address : ${senderAddr}`); + else if (!floCrypto.validateFloID(receiverAddr)) + return reject(`Invalid address : ${receiverAddr}`); + else if (typeof sendAmt !== 'number' || sendAmt <= 0) + return reject(`Invalid sendAmt : ${sendAmt}`); + + getBalance(senderAddr).then(balance => { + var fee = DEFAULT.fee; + if (balance < sendAmt + fee) + return reject("Insufficient FLO balance!"); + getUTXOs(senderAddr).then(utxos => { + //form/construct the transaction data + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + for (var i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt + fee); i--) { + //use only utxos with confirmations (strict_utxo mode) + if (utxos[i].confirmations || !strict_utxo) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < sendAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + trx.addoutput(receiverAddr, sendAmt); + var change = utxoAmt - sendAmt - fee; + if (change > DEFAULT.minChangeAmt) + trx.addoutput(senderAddr, change); + trx.addflodata(floData.replace(/\n/g, ' ')); + resolve(trx); + } + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + floBlockchainAPI.createTx = function (senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + createTx(senderAddr, receiverAddr, sendAmt, floData, strict_utxo) + .then(trx => resolve(trx.serialize())) + .catch(error => reject(error)) + }) + } + + //Send Tx to blockchain + const sendTx = floBlockchainAPI.sendTx = function (senderAddr, receiverAddr, sendAmt, privKey, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + if (!floCrypto.validateFloID(senderAddr, true)) + return reject(`Invalid address : ${senderAddr}`); + else if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr)) + return reject("Invalid Private key!"); + createTx(senderAddr, receiverAddr, sendAmt, floData, strict_utxo).then(trx => { + var signedTxHash = trx.sign(privKey, 1); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }); + } + + //Write Data into blockchain + floBlockchainAPI.writeData = function (senderAddr, data, privKey, receiverAddr = DEFAULT.receiverID, options = {}) { + let strict_utxo = options.strict_utxo === false ? false : true, + sendAmt = isNaN(options.sendAmt) ? DEFAULT.sendAmt : options.sendAmt; + return new Promise((resolve, reject) => { + if (typeof data != "string") + data = JSON.stringify(data); + sendTx(senderAddr, receiverAddr, sendAmt, privKey, data, strict_utxo) + .then(txid => resolve(txid)) + .catch(error => reject(error)); + }); + } + + //merge all UTXOs of a given floID into a single UTXO + floBlockchainAPI.mergeUTXOs = function (floID, privKey, floData = '') { + return new Promise((resolve, reject) => { + if (!floCrypto.validateFloID(floID, true)) + return reject(`Invalid floID`); + if (!floCrypto.verifyPrivKey(privKey, floID)) + return reject("Invalid Private Key"); + if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + var fee = DEFAULT.fee; + getUTXOs(floID).then(utxos => { + for (var i = utxos.length - 1; i >= 0; i--) + if (utxos[i].confirmations) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + } + trx.addoutput(floID, utxoAmt - fee); + trx.addflodata(floData.replace(/\n/g, ' ')); + var signedTxHash = trx.sign(privKey, 1); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + //split sufficient UTXOs of a given floID for a parallel sending + floBlockchainAPI.splitUTXOs = function (floID, privKey, count, floData = '') { + return new Promise((resolve, reject) => { + if (!floCrypto.validateFloID(floID, true)) + return reject(`Invalid floID`); + if (!floCrypto.verifyPrivKey(privKey, floID)) + return reject("Invalid Private Key"); + if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + var fee = DEFAULT.fee; + var splitAmt = DEFAULT.sendAmt + fee; + var totalAmt = splitAmt * count; + getBalance(floID).then(balance => { + var fee = DEFAULT.fee; + if (balance < totalAmt + fee) + return reject("Insufficient FLO balance!"); + //get unconfirmed tx list + getUTXOs(floID).then(utxos => { + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < totalAmt + fee); i--) { + //use only utxos with confirmations (strict_utxo mode) + if (utxos[i].confirmations || !strict_utxo) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < totalAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + for (let i = 0; i < count; i++) + trx.addoutput(floID, splitAmt); + var change = utxoAmt - totalAmt - fee; + if (change > DEFAULT.minChangeAmt) + trx.addoutput(floID, change); + trx.addflodata(floData.replace(/\n/g, ' ')); + var signedTxHash = trx.sign(privKey, 1); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + /**Write data into blockchain from (and/or) to multiple floID + * @param {Array} senderPrivKeys List of sender private-keys + * @param {string} data FLO data of the txn + * @param {Array} receivers List of receivers + * @param {boolean} preserveRatio (optional) preserve ratio or equal contribution + * @return {Promise} + */ + floBlockchainAPI.writeDataMultiple = function (senderPrivKeys, data, receivers = [DEFAULT.receiverID], options = {}) { + return new Promise((resolve, reject) => { + if (!Array.isArray(senderPrivKeys)) + return reject("Invalid senderPrivKeys: SenderPrivKeys must be Array"); + if (options.preserveRatio === false) { + let tmp = {}; + let amount = (DEFAULT.sendAmt * receivers.length) / senderPrivKeys.length; + senderPrivKeys.forEach(key => tmp[key] = amount); + senderPrivKeys = tmp; + } + if (!Array.isArray(receivers)) + return reject("Invalid receivers: Receivers must be Array"); + else { + let tmp = {}; + let amount = options.sendAmt || DEFAULT.sendAmt; + receivers.forEach(floID => tmp[floID] = amount); + receivers = tmp + } + if (typeof data != "string") + data = JSON.stringify(data); + sendTxMultiple(senderPrivKeys, receivers, data) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }) + } + + /**Send Tx from (and/or) to multiple floID + * @param {Array or Object} senderPrivKeys List of sender private-key (optional: with coins to be sent) + * @param {Object} receivers List of receivers with respective amount to be sent + * @param {string} floData FLO data of the txn + * @return {Promise} + */ + const sendTxMultiple = floBlockchainAPI.sendTxMultiple = function (senderPrivKeys, receivers, floData = '') { + return new Promise((resolve, reject) => { + if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + let senders = {}, + preserveRatio; + //check for argument validations + try { + let invalids = { + InvalidSenderPrivKeys: [], + InvalidSenderAmountFor: [], + InvalidReceiverIDs: [], + InvalidReceiveAmountFor: [] + } + let inputVal = 0, + outputVal = 0; + //Validate sender privatekeys (and send amount if passed) + //conversion when only privateKeys are passed (preserveRatio mode) + if (Array.isArray(senderPrivKeys)) { + senderPrivKeys.forEach(key => { + try { + if (!key) + invalids.InvalidSenderPrivKeys.push(key); + else { + let floID = floCrypto.getFloID(key); + senders[floID] = { + wif: key + } + } + } catch (error) { + invalids.InvalidSenderPrivKeys.push(key) + } + }) + preserveRatio = true; + } + //conversion when privatekeys are passed with send amount + else { + for (let key in senderPrivKeys) { + try { + if (!key) + invalids.InvalidSenderPrivKeys.push(key); + else { + if (typeof senderPrivKeys[key] !== 'number' || senderPrivKeys[key] <= 0) + invalids.InvalidSenderAmountFor.push(key); + else + inputVal += senderPrivKeys[key]; + let floID = floCrypto.getFloID(key); + senders[floID] = { + wif: key, + coins: senderPrivKeys[key] + } + } + } catch (error) { + invalids.InvalidSenderPrivKeys.push(key) + } + } + preserveRatio = false; + } + //Validate the receiver IDs and receive amount + for (let floID in receivers) { + if (!floCrypto.validateFloID(floID)) + invalids.InvalidReceiverIDs.push(floID); + if (typeof receivers[floID] !== 'number' || receivers[floID] <= 0) + invalids.InvalidReceiveAmountFor.push(floID); + else + outputVal += receivers[floID]; + } + //Reject if any invalids are found + for (let i in invalids) + if (!invalids[i].length) + delete invalids[i]; + if (Object.keys(invalids).length) + return reject(invalids); + //Reject if given inputVal and outputVal are not equal + if (!preserveRatio && inputVal != outputVal) + return reject(`Input Amount (${inputVal}) not equal to Output Amount (${outputVal})`); + } catch (error) { + return reject(error) + } + //Get balance of senders + let promises = []; + for (let floID in senders) + promises.push(getBalance(floID)); + Promise.all(promises).then(results => { + let totalBalance = 0, + totalFee = DEFAULT.fee, + balance = {}; + //Divide fee among sender if not for preserveRatio + if (!preserveRatio) + var dividedFee = totalFee / Object.keys(senders).length; + //Check if balance of each sender is sufficient enough + let insufficient = []; + for (let floID in senders) { + balance[floID] = parseFloat(results.shift()); + if (isNaN(balance[floID]) || (preserveRatio && balance[floID] <= totalFee) || + (!preserveRatio && balance[floID] < senders[floID].coins + dividedFee)) + insufficient.push(floID); + totalBalance += balance[floID]; + } + if (insufficient.length) + return reject({ + InsufficientBalance: insufficient + }) + //Calculate totalSentAmount and check if totalBalance is sufficient + let totalSendAmt = totalFee; + for (let floID in receivers) + totalSendAmt += receivers[floID]; + if (totalBalance < totalSendAmt) + return reject("Insufficient total Balance"); + //Get the UTXOs of the senders + let promises = []; + for (let floID in senders) + promises.push(getUTXOs(floID)); + Promise.all(promises).then(results => { + var trx = bitjs.transaction(); + for (let floID in senders) { + let utxos = results.shift(); + let sendAmt; + if (preserveRatio) { + let ratio = (balance[floID] / totalBalance); + sendAmt = totalSendAmt * ratio; + } else + sendAmt = senders[floID].coins + dividedFee; + let utxoAmt = 0.0; + for (let i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt); i--) { + if (utxos[i].confirmations) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + } + } + if (utxoAmt < sendAmt) + return reject("Insufficient balance:" + floID); + let change = (utxoAmt - sendAmt); + if (change > 0) + trx.addoutput(floID, change); + } + for (let floID in receivers) + trx.addoutput(floID, receivers[floID]); + trx.addflodata(floData.replace(/\n/g, ' ')); + for (let floID in senders) + trx.sign(senders[floID].wif, 1); + var signedTxHash = trx.serialize(); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + //Create a multisig transaction + const createMultisigTx = function (redeemScript, receivers, amounts, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + var multisig = floCrypto.decodeRedeemScript(redeemScript); + + //validate multisig script and flodata + if (!multisig) + return reject(`Invalid redeemScript`); + var senderAddr = multisig.address; + if (!floCrypto.validateFloID(senderAddr)) + return reject(`Invalid multisig : ${senderAddr}`); + else if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + //validate receiver addresses + if (!Array.isArray(receivers)) + receivers = [receivers]; + for (let r of receivers) + if (!floCrypto.validateFloID(r)) + return reject(`Invalid address : ${r}`); + //validate amounts + if (!Array.isArray(amounts)) + amounts = [amounts]; + if (amounts.length != receivers.length) + return reject("Receivers and amounts have different length"); + var sendAmt = 0; + for (let a of amounts) { + if (typeof a !== 'number' || a <= 0) + return reject(`Invalid amount : ${a}`); + sendAmt += a; + } + + getBalance(senderAddr).then(balance => { + var fee = DEFAULT.fee; + if (balance < sendAmt + fee) + return reject("Insufficient FLO balance!"); + getUTXOs(senderAddr).then(utxos => { + //form/construct the transaction data + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + for (var i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt + fee); i--) { + //use only utxos with confirmations (strict_utxo mode) + if (utxos[i].confirmations || !strict_utxo) { + trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < sendAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + for (let i in receivers) + trx.addoutput(receivers[i], amounts[i]); + var change = utxoAmt - sendAmt - fee; + if (change > DEFAULT.minChangeAmt) + trx.addoutput(senderAddr, change); + trx.addflodata(floData.replace(/\n/g, ' ')); + resolve(trx); + } + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }); + } + + //Same as above, but explict call should return serialized tx-hex + floBlockchainAPI.createMultisigTx = function (redeemScript, receivers, amounts, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + createMultisigTx(redeemScript, receivers, amounts, floData, strict_utxo) + .then(trx => resolve(trx.serialize())) + .catch(error => reject(error)) + }) + } + + //Create and send multisig transaction + const sendMultisigTx = floBlockchainAPI.sendMultisigTx = function (redeemScript, privateKeys, receivers, amounts, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + var multisig = floCrypto.decodeRedeemScript(redeemScript); + if (!multisig) + return reject(`Invalid redeemScript`); + if (privateKeys.length < multisig.required) + return reject(`Insufficient privateKeys (required ${multisig.required})`); + for (let pk of privateKeys) { + var flag = false; + for (let pub of multisig.pubkeys) + if (floCrypto.verifyPrivKey(pk, pub, false)) + flag = true; + if (!flag) + return reject(`Invalid Private key`); + } + createMultisigTx(redeemScript, receivers, amounts, floData, strict_utxo).then(trx => { + for (let pk of privateKeys) + trx.sign(pk, 1); + var signedTxHash = trx.serialize(); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + floBlockchainAPI.writeMultisigData = function (redeemScript, data, privatekeys, receiverAddr = DEFAULT.receiverID, options = {}) { + let strict_utxo = options.strict_utxo === false ? false : true, + sendAmt = isNaN(options.sendAmt) ? DEFAULT.sendAmt : options.sendAmt; + return new Promise((resolve, reject) => { + if (!floCrypto.validateFloID(receiverAddr)) + return reject(`Invalid receiver: ${receiverAddr}`); + sendMultisigTx(redeemScript, privatekeys, receiverAddr, sendAmt, data, strict_utxo) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }) + } + + function deserializeTx(tx) { + if (typeof tx === 'string' || Array.isArray(tx)) { + try { + tx = bitjs.transaction(tx); + } catch { + throw "Invalid transaction hex"; + } + } else if (typeof tx !== 'object' || typeof tx.sign !== 'function') + throw "Invalid transaction object"; + return tx; + } + + floBlockchainAPI.signTx = function (tx, privateKey, sighashtype = 1) { + if (!floCrypto.getFloID(privateKey)) + throw "Invalid Private key"; + //deserialize if needed + tx = deserializeTx(tx); + var signedTxHex = tx.sign(privateKey, sighashtype); + return signedTxHex; + } + + const checkSigned = floBlockchainAPI.checkSigned = function (tx, bool = true) { + tx = deserializeTx(tx); + let n = []; + for (let i = 0; i < tx.inputs.length; i++) { + var s = tx.scriptDecode(i); + if (s['type'] === 'scriptpubkey') + n.push(s.signed); + else if (s['type'] === 'multisig') { + var rs = tx.decodeRedeemScript(s['rs']); + let x = { + s: 0, + r: rs['required'], + t: rs['pubkeys'].length + }; + //check input script for signatures + var script = Array.from(tx.inputs[i].script); + if (script[0] == 0) { //script with signatures + script = tx.parseScript(script); + for (var k = 0; k < script.length; k++) + if (Array.isArray(script[k]) && script[k][0] == 48) //0x30 DERSequence + x.s++; + } + //validate counts + if (x.r > x.t) + throw "signaturesRequired is more than publicKeys"; + else if (x.s < x.r) + n.push(x); + else + n.push(true); + } + } + return bool ? !(n.filter(x => x !== true).length) : n; + } + + floBlockchainAPI.checkIfSameTx = function (tx1, tx2) { + tx1 = deserializeTx(tx1); + tx2 = deserializeTx(tx2); + //compare input and output length + if (tx1.inputs.length !== tx2.inputs.length || tx1.outputs.length !== tx2.outputs.length) + return false; + //compare flodata + if (tx1.floData !== tx2.floData) + return false + //compare inputs + for (let i = 0; i < tx1.inputs.length; i++) + if (tx1.inputs[i].outpoint.hash !== tx2.inputs[i].outpoint.hash || tx1.inputs[i].outpoint.index !== tx2.inputs[i].outpoint.index) + return false; + //compare outputs + for (let i = 0; i < tx1.outputs.length; i++) + if (tx1.outputs[i].value !== tx2.outputs[i].value || Crypto.util.bytesToHex(tx1.outputs[i].script) !== Crypto.util.bytesToHex(tx2.outputs[i].script)) + return false; + return true; + } + + floBlockchainAPI.transactionID = function (tx) { + tx = deserializeTx(tx); + let clone = bitjs.clone(tx); + let raw_bytes = Crypto.util.hexToBytes(clone.serialize()); + let txid = Crypto.SHA256(Crypto.SHA256(raw_bytes, { asBytes: true }), { asBytes: true }).reverse(); + return Crypto.util.bytesToHex(txid); + } + + const getTxOutput = (txid, i) => new Promise((resolve, reject) => { + promisedAPI(`api/tx/${txid}`) + .then(result => resolve(result.vout[i])) + .catch(error => reject(error)) + }); + + function getOutputAddress(outscript) { + var bytes, version; + switch (outscript[0]) { + case 118: //legacy + bytes = outscript.slice(3, outscript.length - 2); + version = bitjs.pub; + break + case 169: //multisig + bytes = outscript.slice(2, outscript.length - 1); + version = bitjs.multisig; + break; + default: return; //unknown + } + bytes.unshift(version); + var hash = Crypto.SHA256(Crypto.SHA256(bytes, { asBytes: true }), { asBytes: true }); + var checksum = hash.slice(0, 4); + return bitjs.Base58.encode(bytes.concat(checksum)); + } + + floBlockchainAPI.parseTransaction = function (tx) { + return new Promise((resolve, reject) => { + tx = deserializeTx(tx); + let result = {}; + let promises = []; + //Parse Inputs + for (let i = 0; i < tx.inputs.length; i++) + promises.push(getTxOutput(tx.inputs[i].outpoint.hash, tx.inputs[i].outpoint.index)); + Promise.all(promises).then(inputs => { + result.inputs = inputs.map(inp => Object({ + address: inp.scriptPubKey.addresses[0], + value: parseFloat(inp.value) + })); + let signed = checkSigned(tx, false); + result.inputs.forEach((inp, i) => inp.signed = signed[i]); + //Parse Outputs + result.outputs = tx.outputs.map(out => Object({ + address: getOutputAddress(out.script), + value: util.Sat_to_FLO(out.value) + })) + //Parse Totals + result.total_input = parseFloat(result.inputs.reduce((a, inp) => a += inp.value, 0).toFixed(8)); + result.total_output = parseFloat(result.outputs.reduce((a, out) => a += out.value, 0).toFixed(8)); + result.fee = parseFloat((result.total_input - result.total_output).toFixed(8)); + result.floData = tx.floData; + resolve(result); + }).catch(error => reject(error)) + }) + } + + //Broadcast signed Tx in blockchain using API + const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) { + return new Promise((resolve, reject) => { + if (signedTxHash.length < 1) + return reject("Empty Transaction Data"); + + promisedAPI('/api/sendtx/' + signedTxHash) + .then(response => resolve(response["result"])) + .catch(error => reject(error)) + }) + } + + const getTx = floBlockchainAPI.getTx = function (txid) { + return new Promise((resolve, reject) => { + promisedAPI(`api/tx/${txid}`) + .then(response => resolve(response)) + .catch(error => reject(error)) + }) + } + + /**Wait for the given txid to get confirmation in blockchain + * @param {string} txid of the transaction to wait for + * @param {int} max_retry: maximum number of retries before exiting wait. negative number = Infinite retries (DEFAULT: -1 ie, infinite retries) + * @param {Array} retry_timeout: time (seconds) between retries (DEFAULT: 20 seconds) + * @return {Promise} resolves when tx gets confirmation + */ + const waitForConfirmation = floBlockchainAPI.waitForConfirmation = function (txid, max_retry = -1, retry_timeout = 20) { + return new Promise((resolve, reject) => { + setTimeout(function () { + getTx(txid).then(tx => { + if (!tx) + return reject("Transaction not found"); + if (tx.confirmations) + return resolve(tx); + else if (max_retry === 0) //no more retries + return reject("Waiting timeout: tx still not confirmed"); + else { + max_retry = max_retry < 0 ? -1 : max_retry - 1; //decrease retry count (unless infinite retries) + waitForConfirmation(txid, max_retry, retry_timeout) + .then(result => resolve(result)) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }, retry_timeout * 1000) + }) + } + + //Read Txs of Address + const readTxs = floBlockchainAPI.readTxs = function (addr, options = {}) { + return new Promise((resolve, reject) => { + //API options + let query_params = { details: 'txs' }; + //page options + if (!isUndefined(options.page) && Number.isInteger(options.page)) + query_params.page = options.page; + if (!isUndefined(options.pageSize) && Number.isInteger(options.pageSize)) + query_params.pageSize = options.pageSize; + //only confirmed tx + if (options.confirmed) //Default is false in server, so only add confirmed filter if confirmed has a true value + query_params.confirmed = true; + + promisedAPI(`api/address/${addr}`, query_params).then(response => { + if (!Array.isArray(response.txs)) //set empty array if address doesnt have any tx + response.txs = []; + resolve(response) + }).catch(error => reject(error)) + }); + } + + //backward support (floBlockchainAPI < v2.5.6) + function readAllTxs_oldSupport(addr, options, ignoreOld = 0, cacheTotal = 0) { + return new Promise((resolve, reject) => { + readTxs(addr, options).then(response => { + cacheTotal += response.txs.length; + let n_remaining = response.txApperances - cacheTotal + if (n_remaining < ignoreOld) { // must remove tx that would have been fetch during prev call + let n_remove = ignoreOld - n_remaining; + resolve(response.txs.slice(0, -n_remove)); + } else if (response.page == response.totalPages) //last page reached + resolve(response.txs); + else { + options.page = response.page + 1; + readAllTxs_oldSupport(addr, options, ignoreOld, cacheTotal) + .then(result => resolve(response.txs.concat(result))) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + } + + function readAllTxs_new(addr, options, lastItem) { + return new Promise((resolve, reject) => { + readTxs(addr, options).then(response => { + let i = response.txs.findIndex(t => t.txid === lastItem); + if (i != -1) //found lastItem + resolve(response.txs.slice(0, i)) + else if (response.page == response.totalPages) //last page reached + resolve(response.txs); + else { + options.page = response.page + 1; + readAllTxs_new(addr, options, lastItem) + .then(result => resolve(response.txs.concat(result))) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + } + + //Read All Txs of Address (newest first) + const readAllTxs = floBlockchainAPI.readAllTxs = function (addr, options = {}) { + return new Promise((resolve, reject) => { + if (Number.isInteger(options.ignoreOld)) //backward support: data from floBlockchainAPI < v2.5.6 + readAllTxs_oldSupport(addr, options, options.ignoreOld).then(txs => { + let last_tx = txs.find(t => t.confirmations > 0); + let new_lastItem = last_tx ? last_tx.txid : options.ignoreOld; + resolve({ + lastItem: new_lastItem, + items: txs + }) + + }).catch(error => reject(error)) + else //New format for floBlockchainAPI >= v2.5.6 + readAllTxs_new(addr, options, options.after).then(txs => { + let last_tx = txs.find(t => t.confirmations > 0); + let new_lastItem = last_tx ? last_tx.txid : options.after; + resolve({ + lastItem: new_lastItem, + items: txs + }) + }).catch(error => reject(error)) + }) + } + + /*Read flo Data from txs of given Address + options can be used to filter data + after : query after the given txid + confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_tx) + ignoreOld : ignore old txs (deprecated: support for backward compatibility only, cannot be used with 'after') + sentOnly : filters only sent data + receivedOnly: filters only received data + pattern : filters data that with JSON pattern + filter : custom filter funtion for floData (eg . filter: d => {return d[0] == '$'}) + tx : (boolean) resolve tx data or not (resolves an Array of Object with tx details) + sender : flo-id(s) of sender + receiver : flo-id(s) of receiver + */ + floBlockchainAPI.readData = function (addr, options = {}) { + return new Promise((resolve, reject) => { + + //fetch options + let query_options = {}; + query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: ignore unconfirmed tx + + if (!isUndefined(options.after)) + query_options.after = options.after; + else if (!isUndefined(options.ignoreOld)) + query_options.ignoreOld = options.ignoreOld; + + readAllTxs(addr, query_options).then(response => { + + if (typeof options.senders === "string") options.senders = [options.senders]; + if (typeof options.receivers === "string") options.receivers = [options.receivers]; + + //filter the txs based on options + const filteredData = response.items.filter(tx => { + + if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query + return false; + + if (options.sentOnly && !tx.vin.some(vin => vin.addresses[0] === addr)) + return false; + else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0]))) + return false; + + if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) + return false; + else if (Array.isArray(options.receivers) && !tx.vout.some(vout => options.receivers.includes(vout.scriptPubKey.addresses[0]))) + return false; + + if (options.pattern) { + try { + let jsonContent = JSON.parse(tx.floData); + if (!Object.keys(jsonContent).includes(options.pattern)) + return false; + } catch { + return false; + } + } + + if (options.filter && !options.filter(tx.floData)) + return false; + + return true; + }).map(tx => options.tx ? { + txid: tx.txid, + time: tx.time, + blockheight: tx.blockheight, + senders: new Set(tx.vin.map(v => v.addresses[0])), + receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])), + data: tx.floData + } : tx.floData); + + const result = { lastItem: response.lastItem }; + if (options.tx) + result.items = filteredData; + else + result.data = filteredData + resolve(result); + + }).catch(error => reject(error)) + }) + } + + /*Get the latest flo Data that match the caseFn from txs of given Address + caseFn: (function) flodata => return bool value + options can be used to filter data + after : query after the given txid + confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_tx) + sentOnly : filters only sent data + receivedOnly: filters only received data + tx : (boolean) resolve tx data or not (resolves an Array of Object with tx details) + sender : flo-id(s) of sender + receiver : flo-id(s) of receiver + */ + const getLatestData = floBlockchainAPI.getLatestData = function (addr, caseFn, options = {}) { + return new Promise((resolve, reject) => { + //fetch options + let query_options = {}; + query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: confirmed tx only + if (!isUndefined(options.page)) + query_options.page = options.page; + //if (!isUndefined(options.after)) query_options.after = options.after; + + let new_lastItem; + readTxs(addr, query_options).then(response => { + + //lastItem confirmed tx checked + if (!new_lastItem) { + let last_tx = response.items.find(t => t.confirmations > 0); + if (last_tx) + new_lastItem = last_tx.txid; + } + + if (typeof options.senders === "string") options.senders = [options.senders]; + if (typeof options.receivers === "string") options.receivers = [options.receivers]; + + //check if `after` txid is in the response + let i_after = response.txs.findIndex(t => t.txid === options.after); + if (i_after != -1) //found lastItem, hence remove it and all txs before that + response.items.splice(i_after); + + var item = response.items.find(tx => { + if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query + return false; + + if (options.sentOnly && !tx.vin.some(vin => vin.addresses[0] === addr)) + return false; + else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0]))) + return false; + + if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) + return false; + else if (Array.isArray(options.receivers) && !tx.vout.some(vout => options.receivers.includes(vout.scriptPubKey.addresses[0]))) + return false; + + return caseFn(tx.floData) ? true : false; //return only bool for find fn + }); + + //if item found, then resolve the result + if (!isUndefined(item)) { + const result = { lastItem: new_lastItem || item.txid }; + if (options.tx) { + result.item = { + txid: item.txid, + time: item.time, + blockheight: item.blockheight, + senders: new Set(item.vin.map(v => v.addresses[0])), + receivers: new Set(item.vout.map(v => v.scriptPubKey.addresses[0])), + data: item.floData + } + } else + result.data = item.floData; + return resolve(result); + } + + if (response.page == response.totalPages || i_after != -1) //reached last page to check + resolve({ lastItem: new_lastItem || options.after }); //no data match the caseFn, resolve just the lastItem + + //else if address needs chain query + else { + options.page = response.page + 1; + getLatestData(addr, caseFn, options) + .then(result => resolve(result)) + .catch(error => reject(error)) + } + + }).catch(error => reject(error)) + }) + } + +})('object' === typeof module ? module.exports : window.floBlockchainAPI = {}); \ No newline at end of file diff --git a/messenger/scripts/floCloudAPI.js b/messenger/scripts/floCloudAPI.js new file mode 100644 index 0000000..19ede97 --- /dev/null +++ b/messenger/scripts/floCloudAPI.js @@ -0,0 +1,1110 @@ +(function (EXPORTS) { //floCloudAPI v2.4.5 + /* FLO Cloud operations to send/request application data*/ + 'use strict'; + const floCloudAPI = EXPORTS; + + const DEFAULT = { + blockchainPrefix: 0x23, //Prefix version for FLO blockchain + SNStorageID: floGlobals.SNStorageID || "FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk", + adminID: floGlobals.adminID, + application: floGlobals.application, + SNStorageName: "SuperNodeStorage", + callback: (d, e) => console.debug(d, e) + }; + + var user_id, user_public, user_private, aes_key; + + function user(id, priv) { + if (!priv || !id) + return user.clear(); + let pub = floCrypto.getPubKeyHex(priv); + if (!pub || !floCrypto.verifyPubKey(pub, id)) + return user.clear(); + let n = floCrypto.randInt(12, 20); + aes_key = floCrypto.randString(n); + user_private = Crypto.AES.encrypt(priv, aes_key); + user_public = pub; + user_id = id; + return user_id; + } + + Object.defineProperties(user, { + id: { + get: () => { + if (!user_id) + throw "User not set"; + return user_id; + } + }, + public: { + get: () => { + if (!user_public) + throw "User not set"; + return user_public; + } + }, + sign: { + value: msg => { + if (!user_private) + throw "User not set"; + return floCrypto.signData(msg, Crypto.AES.decrypt(user_private, aes_key)); + } + }, + clear: { + value: () => user_id = user_public = user_private = aes_key = undefined + } + }) + + Object.defineProperties(floCloudAPI, { + SNStorageID: { + get: () => DEFAULT.SNStorageID + }, + SNStorageName: { + get: () => DEFAULT.SNStorageName + }, + adminID: { + get: () => DEFAULT.adminID + }, + application: { + get: () => DEFAULT.application + }, + user: { + get: () => user + } + }); + + var appObjects, generalData, lastVC; + Object.defineProperties(floGlobals, { + appObjects: { + get: () => appObjects, + set: obj => appObjects = obj + }, + generalData: { + get: () => generalData, + set: data => generalData = data + }, + generalDataset: { + value: (type, options = {}) => generalData[filterKey(type, options)] + }, + lastVC: { + get: () => lastVC, + set: vc => lastVC = vc + } + }); + + var supernodes = {}; //each supnernode must be stored as floID : {uri:,pubKey:} + Object.defineProperty(floCloudAPI, 'nodes', { + get: () => JSON.parse(JSON.stringify(supernodes)) + }); + + var kBucket; + const K_Bucket = floCloudAPI.K_Bucket = function (masterID, nodeList) { + + const decodeID = floID => { + let k = bitjs.Base58.decode(floID); + k.shift(); + k.splice(-4, 4); + let decodedId = Crypto.util.bytesToHex(k); + let nodeIdBigInt = new BigInteger(decodedId, 16); + let nodeIdBytes = nodeIdBigInt.toByteArrayUnsigned(); + let nodeIdNewInt8Array = new Uint8Array(nodeIdBytes); + return nodeIdNewInt8Array; + }; + + const _KB = new BuildKBucket({ + localNodeId: decodeID(masterID) + }); + nodeList.forEach(id => _KB.add({ + id: decodeID(id), + floID: id + })); + + const _CO = nodeList.map(id => [_KB.distance(_KB.localNodeId, decodeID(id)), id]) + .sort((a, b) => a[0] - b[0]) + .map(a => a[1]); + + const self = this; + Object.defineProperty(self, 'tree', { + get: () => _KB + }); + Object.defineProperty(self, 'list', { + get: () => Array.from(_CO) + }); + + self.isNode = floID => _CO.includes(floID); + self.innerNodes = function (id1, id2) { + if (!_CO.includes(id1) || !_CO.includes(id2)) + throw Error('Given nodes are not supernode'); + let iNodes = [] + for (let i = _CO.indexOf(id1) + 1; _CO[i] != id2; i++) { + if (i < _CO.length) + iNodes.push(_CO[i]) + else i = -1 + } + return iNodes + } + self.outterNodes = function (id1, id2) { + if (!_CO.includes(id1) || !_CO.includes(id2)) + throw Error('Given nodes are not supernode'); + let oNodes = [] + for (let i = _CO.indexOf(id2) + 1; _CO[i] != id1; i++) { + if (i < _CO.length) + oNodes.push(_CO[i]) + else i = -1 + } + return oNodes + } + self.prevNode = function (id, N = 1) { + let n = N || _CO.length; + if (!_CO.includes(id)) + throw Error('Given node is not supernode'); + let pNodes = [] + for (let i = 0, j = _CO.indexOf(id) - 1; i < n; j--) { + if (j == _CO.indexOf(id)) + break; + else if (j > -1) + pNodes[i++] = _CO[j] + else j = _CO.length + } + return (N == 1 ? pNodes[0] : pNodes) + } + self.nextNode = function (id, N = 1) { + let n = N || _CO.length; + if (!_CO.includes(id)) + throw Error('Given node is not supernode'); + if (!n) n = _CO.length; + let nNodes = [] + for (let i = 0, j = _CO.indexOf(id) + 1; i < n; j++) { + if (j == _CO.indexOf(id)) + break; + else if (j < _CO.length) + nNodes[i++] = _CO[j] + else j = -1 + } + return (N == 1 ? nNodes[0] : nNodes) + } + self.closestNode = function (id, N = 1) { + let decodedId = decodeID(id); + let n = N || _CO.length; + let cNodes = _KB.closest(decodedId, n) + .map(k => k.floID) + return (N == 1 ? cNodes[0] : cNodes) + } + } + + floCloudAPI.init = function startCloudProcess(nodes) { + return new Promise((resolve, reject) => { + try { + supernodes = nodes; + kBucket = new K_Bucket(DEFAULT.SNStorageID, Object.keys(supernodes)); + resolve('Cloud init successful'); + } catch (error) { + reject(error); + } + }) + } + + Object.defineProperty(floCloudAPI, 'kBucket', { + get: () => kBucket + }); + + const _inactive = new Set(); + + function ws_connect(snID) { + return new Promise((resolve, reject) => { + if (!(snID in supernodes)) + return reject(`${snID} is not a supernode`) + if (_inactive.has(snID)) + return reject(`${snID} is not active`) + var wsConn = new WebSocket("wss://" + supernodes[snID].uri + "/"); + wsConn.onopen = evt => resolve(wsConn); + wsConn.onerror = evt => { + _inactive.add(snID) + reject(`${snID} is unavailable`) + } + }) + } + + function ws_activeConnect(snID, reverse = false) { + return new Promise((resolve, reject) => { + if (_inactive.size === kBucket.list.length) + return reject('Cloud offline'); + if (!(snID in supernodes)) + snID = kBucket.closestNode(proxyID(snID)); + ws_connect(snID) + .then(node => resolve(node)) + .catch(error => { + if (reverse) + var nxtNode = kBucket.prevNode(snID); + else + var nxtNode = kBucket.nextNode(snID); + ws_activeConnect(nxtNode, reverse) + .then(node => resolve(node)) + .catch(error => reject(error)) + }) + }) + } + + function fetch_API(snID, data) { + return new Promise((resolve, reject) => { + if (_inactive.has(snID)) + return reject(`${snID} is not active`); + let fetcher, sn_url = "https://" + supernodes[snID].uri; + if (typeof data === "string") + fetcher = fetch(sn_url + "?" + data); + else if (typeof data === "object" && data.method === "POST") + fetcher = fetch(sn_url, data); + fetcher.then(response => { + if (response.ok || response.status === 400 || response.status === 500) + resolve(response); + else + reject(response); + }).catch(error => reject(error)) + }) + } + + function fetch_ActiveAPI(snID, data, reverse = false) { + return new Promise((resolve, reject) => { + if (_inactive.size === kBucket.list.length) + return reject('Cloud offline'); + if (!(snID in supernodes)) + snID = kBucket.closestNode(proxyID(snID)); + fetch_API(snID, data) + .then(result => resolve(result)) + .catch(error => { + _inactive.add(snID) + if (reverse) + var nxtNode = kBucket.prevNode(snID); + else + var nxtNode = kBucket.nextNode(snID); + fetch_ActiveAPI(nxtNode, data, reverse) + .then(result => resolve(result)) + .catch(error => reject(error)); + }) + }) + } + + function singleRequest(floID, data_obj, method = "POST") { + return new Promise((resolve, reject) => { + let data; + if (method === "POST") + data = { + method: "POST", + body: JSON.stringify(data_obj) + }; + else + data = new URLSearchParams(JSON.parse(JSON.stringify(data_obj))).toString(); + fetch_ActiveAPI(floID, data).then(response => { + if (response.ok) + response.json() + .then(result => resolve(result)) + .catch(error => reject(error)) + else response.text() + .then(result => reject(response.status + ": " + result)) //Error Message from Node + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + const _liveRequest = {}; + + function liveRequest(floID, request, callback) { + const filterData = typeof request.status !== 'undefined' ? + data => { + if (request.status) + return data; + else { + let filtered = {}; + for (let i in data) + if (request.trackList.includes(i)) + filtered[i] = data[i]; + return filtered; + } + } : + data => { + data = objectifier(data); + let filtered = {}, + proxy = proxyID(request.receiverID), + r = request; + for (let v in data) { + let d = data[v]; + if ((!r.atVectorClock || r.atVectorClock == v) && + (r.atVectorClock || !r.lowerVectorClock || r.lowerVectorClock <= v) && + (r.atVectorClock || !r.upperVectorClock || r.upperVectorClock >= v) && + (!r.afterTime || r.afterTime < d.log_time) && + r.application == d.application && + (proxy == d.receiverID || proxy == d.proxyID) && + (!r.comment || r.comment == d.comment) && + (!r.type || r.type == d.type) && + (!r.senderID || r.senderID.includes(d.senderID))) + filtered[v] = data[v]; + } + return filtered; + }; + + return new Promise((resolve, reject) => { + ws_activeConnect(floID).then(node => { + let randID = floCrypto.randString(5); + node.send(JSON.stringify(request)); + node.onmessage = (evt) => { + let d = null, + e = null; + try { + d = filterData(JSON.parse(evt.data)); + } catch (error) { + e = evt.data + } finally { + callback(d, e) + } + } + _liveRequest[randID] = node; + _liveRequest[randID].request = request; + resolve(randID); + }).catch(error => reject(error)); + }); + } + + Object.defineProperty(floCloudAPI, 'liveRequest', { + get: () => _liveRequest + }); + + Object.defineProperty(floCloudAPI, 'inactive', { + get: () => _inactive + }); + + const util = floCloudAPI.util = {}; + + const encodeMessage = util.encodeMessage = function (message) { + return btoa(unescape(encodeURIComponent(JSON.stringify(message)))) + } + + const decodeMessage = util.decodeMessage = function (message) { + return JSON.parse(decodeURIComponent(escape(atob(message)))) + } + + const filterKey = util.filterKey = function (type, options = {}) { + return type + (options.comment ? ':' + options.comment : '') + + '|' + (options.group || options.receiverID || DEFAULT.adminID) + + '|' + (options.application || DEFAULT.application); + } + + const proxyID = util.proxyID = function (address) { + if (!address) + return; + var bytes; + if (address.length == 33 || address.length == 34) { //legacy encoding + let decode = bitjs.Base58.decode(address); + bytes = decode.slice(0, decode.length - 4); + let checksum = decode.slice(decode.length - 4), + hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3] ? + bytes = undefined : bytes.shift(); + } else if (!address.startsWith("0x") && address.length == 42 || address.length == 62) { //bech encoding + if (typeof coinjs !== 'function') + throw "library missing (lib_btc.js)"; + let decode = coinjs.bech32_decode(address); + if (decode) { + bytes = decode.data; + bytes.shift(); + bytes = coinjs.bech32_convert(bytes, 5, 8, false); + if (address.length == 62) //for long bech, aggregate once more to get 160 bit + bytes = coinjs.bech32_convert(bytes, 5, 8, false); + } + } else if (address.length == 66) { //public key hex + bytes = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(address), { + asBytes: true + })); + } else if ((address.length == 42 && address.startsWith("0x")) || (address.length == 40 && !address.startsWith("0x"))) { //Ethereum Address + if (address.startsWith("0x")) { address = address.substring(2); } + bytes = Crypto.util.hexToBytes(address); + } + + if (!bytes) + throw "Invalid address: " + address; + else { + bytes.unshift(DEFAULT.blockchainPrefix); + let hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + return bitjs.Base58.encode(bytes.concat(hash.slice(0, 4))); + } + } + + const lastCommit = {}; + Object.defineProperty(lastCommit, 'get', { + value: objName => JSON.parse(lastCommit[objName]) + }); + Object.defineProperty(lastCommit, 'set', { + value: objName => lastCommit[objName] = JSON.stringify(appObjects[objName]) + }); + + function updateObject(objectName, dataSet) { + try { + console.log(dataSet) + let vcList = Object.keys(dataSet).sort(); + for (let vc of vcList) { + if (vc < lastVC[objectName] || dataSet[vc].type !== objectName) + continue; + switch (dataSet[vc].comment) { + case "RESET": + if (dataSet[vc].message.reset) + appObjects[objectName] = dataSet[vc].message.reset; + break; + case "UPDATE": + if (dataSet[vc].message.diff) + appObjects[objectName] = diff.merge(appObjects[objectName], dataSet[vc].message.diff); + } + lastVC[objectName] = vc; + } + lastCommit.set(objectName); + compactIDB.writeData("appObjects", appObjects[objectName], objectName); + compactIDB.writeData("lastVC", lastVC[objectName], objectName); + } catch (error) { + console.error(error) + } + } + + function storeGeneral(fk, dataSet) { + try { + console.log(dataSet) + if (typeof generalData[fk] !== "object") + generalData[fk] = {} + for (let vc in dataSet) { + generalData[fk][vc] = dataSet[vc]; + if (dataSet[vc].log_time > lastVC[fk]) + lastVC[fk] = dataSet[vc].log_time; + } + compactIDB.writeData("lastVC", lastVC[fk], fk) + compactIDB.writeData("generalData", generalData[fk], fk) + } catch (error) { + console.error(error) + } + } + + function objectifier(data) { + if (!Array.isArray(data)) + data = [data]; + return Object.fromEntries(data.map(d => { + d.message = decodeMessage(d.message); + return [d.vectorClock, d]; + })); + } + + //set status as online for user_id + floCloudAPI.setStatus = function (options = {}) { + return new Promise((resolve, reject) => { + let callback = options.callback instanceof Function ? options.callback : DEFAULT.callback; + var request = { + floID: user.id, + application: options.application || DEFAULT.application, + time: Date.now(), + status: true, + pubKey: user.public + } + let hashcontent = ["time", "application", "floID"].map(d => request[d]).join("|"); + request.sign = user.sign(hashcontent); + liveRequest(options.refID || DEFAULT.adminID, request, callback) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //request status of floID(s) in trackList + floCloudAPI.requestStatus = function (trackList, options = {}) { + return new Promise((resolve, reject) => { + if (!Array.isArray(trackList)) + trackList = [trackList]; + let callback = options.callback instanceof Function ? options.callback : DEFAULT.callback; + let request = { + status: false, + application: options.application || DEFAULT.application, + trackList: trackList + } + liveRequest(options.refID || DEFAULT.adminID, request, callback) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //send any message to supernode cloud storage + const sendApplicationData = floCloudAPI.sendApplicationData = function (message, type, options = {}) { + return new Promise((resolve, reject) => { + var data = { + senderID: user.id, + receiverID: options.receiverID || DEFAULT.adminID, + pubKey: user.public, + message: encodeMessage(message), + time: Date.now(), + application: options.application || DEFAULT.application, + type: type, + comment: options.comment || "" + } + let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"] + .map(d => data[d]).join("|") + data.sign = user.sign(hashcontent); + singleRequest(data.receiverID, data) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //request any data from supernode cloud + const requestApplicationData = floCloudAPI.requestApplicationData = function (type, options = {}) { + return new Promise((resolve, reject) => { + var request = { + receiverID: options.receiverID || DEFAULT.adminID, + senderID: options.senderID || undefined, + application: options.application || DEFAULT.application, + type: type, + comment: options.comment || undefined, + lowerVectorClock: options.lowerVectorClock || undefined, + upperVectorClock: options.upperVectorClock || undefined, + atVectorClock: options.atVectorClock || undefined, + afterTime: options.afterTime || undefined, + mostRecent: options.mostRecent || undefined, + } + + if (options.callback instanceof Function) { + liveRequest(request.receiverID, request, options.callback) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + if (options.method === "POST") + request = { + time: Date.now(), + request + }; + singleRequest(request.receiverID, request, options.method || "GET") + .then(data => resolve(data)).catch(error => reject(error)) + } + }) + } + + /*(NEEDS UPDATE) + //delete data from supernode cloud (received only) + floCloudAPI.deleteApplicationData = function(vectorClocks, options = {}) { + return new Promise((resolve, reject) => { + var delreq = { + requestorID: user.id, + pubKey: user.public, + time: Date.now(), + delete: (Array.isArray(vectorClocks) ? vectorClocks : [vectorClocks]), + application: options.application || DEFAULT.application + } + let hashcontent = ["time", "application", "delete"] + .map(d => delreq[d]).join("|") + delreq.sign = user.sign(hashcontent) + singleRequest(delreq.requestorID, delreq).then(result => { + let success = [], + failed = []; + result.forEach(r => r.status === 'fulfilled' ? + success.push(r.value) : failed.push(r.reason)); + resolve({ + success, + failed + }) + }).catch(error => reject(error)) + }) + } + */ + //edit comment of data in supernode cloud (sender only) + floCloudAPI.editApplicationData = function (vectorClock, comment_edit, options = {}) { + return new Promise((resolve, reject) => { + //request the data from cloud for resigning + let req_options = Object.assign({}, options); + req_options.atVectorClock = vectorClock; + requestApplicationData(undefined, req_options).then(result => { + if (!result.length) + return reject("Data not found"); + let data = result[0]; + if (data.senderID !== user.id) + return reject("Only sender can edit comment"); + data.comment = comment_edit; + let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"] + .map(d => data[d]).join("|"); + let re_sign = user.sign(hashcontent); + var request = { + receiverID: options.receiverID || DEFAULT.adminID, + requestorID: user.id, + pubKey: user.public, + time: Date.now(), + vectorClock: vectorClock, + edit: comment_edit, + re_sign: re_sign + } + let request_hash = ["time", "vectorClock", "edit", "re_sign"].map(d => request[d]).join("|"); + request.sign = user.sign(request_hash); + singleRequest(request.receiverID, request) + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + //tag data in supernode cloud (subAdmin access only) + floCloudAPI.tagApplicationData = function (vectorClock, tag, options = {}) { + return new Promise((resolve, reject) => { + if (!floGlobals.subAdmins.includes(user.id)) + return reject("Only subAdmins can tag data") + var request = { + receiverID: options.receiverID || DEFAULT.adminID, + requestorID: user.id, + pubKey: user.public, + time: Date.now(), + vectorClock: vectorClock, + tag: tag, + } + let hashcontent = ["time", "vectorClock", 'tag'].map(d => request[d]).join("|"); + request.sign = user.sign(hashcontent); + singleRequest(request.receiverID, request) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //note data in supernode cloud (receiver only or subAdmin allowed if receiver is adminID) + floCloudAPI.noteApplicationData = function (vectorClock, note, options = {}) { + return new Promise((resolve, reject) => { + var request = { + receiverID: options.receiverID || DEFAULT.adminID, + requestorID: user.id, + pubKey: user.public, + time: Date.now(), + vectorClock: vectorClock, + note: note, + } + let hashcontent = ["time", "vectorClock", 'note'].map(d => request[d]).join("|"); + request.sign = user.sign(hashcontent); + singleRequest(request.receiverID, request) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //send general data + floCloudAPI.sendGeneralData = function (message, type, options = {}) { + return new Promise((resolve, reject) => { + if (options.encrypt) { + let encryptionKey = options.encrypt === true ? + floGlobals.settings.encryptionKey : options.encrypt + message = floCrypto.encryptData(JSON.stringify(message), encryptionKey) + } + sendApplicationData(message, type, options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //request general data + floCloudAPI.requestGeneralData = function (type, options = {}) { + return new Promise((resolve, reject) => { + var fk = filterKey(type, options) + lastVC[fk] = parseInt(lastVC[fk]) || 0; + options.afterTime = options.afterTime || lastVC[fk]; + if (options.callback instanceof Function) { + let new_options = Object.create(options) + new_options.callback = (d, e) => { + storeGeneral(fk, d); + options.callback(d, e) + } + requestApplicationData(type, new_options) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + requestApplicationData(type, options).then(dataSet => { + storeGeneral(fk, objectifier(dataSet)) + resolve(dataSet) + }).catch(error => reject(error)) + } + }) + } + + //request an object data from supernode cloud + floCloudAPI.requestObjectData = function (objectName, options = {}) { + return new Promise((resolve, reject) => { + options.lowerVectorClock = options.lowerVectorClock || lastVC[objectName] + 1; + options.senderID = [false, null].includes(options.senderID) ? null : + options.senderID || floGlobals.subAdmins; + options.mostRecent = true; + options.comment = 'RESET'; + let callback = null; + if (options.callback instanceof Function) { + let old_callback = options.callback; + callback = (d, e) => { + updateObject(objectName, d); + old_callback(d, e); + } + delete options.callback; + } + requestApplicationData(objectName, options).then(dataSet => { + updateObject(objectName, objectifier(dataSet)); + delete options.comment; + options.lowerVectorClock = lastVC[objectName] + 1; + delete options.mostRecent; + if (callback) { + let new_options = Object.create(options); + new_options.callback = callback; + requestApplicationData(objectName, new_options) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + requestApplicationData(objectName, options).then(dataSet => { + updateObject(objectName, objectifier(dataSet)) + resolve(appObjects[objectName]) + }).catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + } + + floCloudAPI.closeRequest = function (requestID) { + return new Promise((resolve, reject) => { + let conn = _liveRequest[requestID] + if (!conn) + return reject('Request not found') + conn.onclose = evt => { + delete _liveRequest[requestID]; + resolve('Request connection closed') + } + conn.close() + }) + } + + //reset or initialize an object and send it to cloud + floCloudAPI.resetObjectData = function (objectName, options = {}) { + return new Promise((resolve, reject) => { + let message = { + reset: appObjects[objectName] + } + options.comment = 'RESET'; + sendApplicationData(message, objectName, options).then(result => { + lastCommit.set(objectName); + resolve(result) + }).catch(error => reject(error)) + }) + } + + //update the diff and send it to cloud + floCloudAPI.updateObjectData = function (objectName, options = {}) { + return new Promise((resolve, reject) => { + let message = { + diff: diff.find(lastCommit.get(objectName), appObjects[ + objectName]) + } + options.comment = 'UPDATE'; + sendApplicationData(message, objectName, options).then(result => { + lastCommit.set(objectName); + resolve(result) + }).catch(error => reject(error)) + }) + } + + //upload file + floCloudAPI.uploadFile = function (fileBlob, type, options = {}) { + return new Promise((resolve, reject) => { + if (!(fileBlob instanceof File) && !(fileBlob instanceof Blob)) + return reject("file must be instance of File/Blob"); + fileBlob.arrayBuffer().then(arraybuf => { + let file_data = { type: fileBlob.type, name: fileBlob.name }; + file_data.content = Crypto.util.bytesToBase64(new Uint8Array(arraybuf)); + if (options.encrypt) { + let encryptionKey = options.encrypt === true ? + floGlobals.settings.encryptionKey : options.encrypt + file_data = floCrypto.encryptData(JSON.stringify(file_data), encryptionKey) + } + sendApplicationData(file_data, type, options) + .then(({ vectorClock, receiverID, type, application }) => resolve({ vectorClock, receiverID, type, application })) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + //download file + floCloudAPI.downloadFile = function (vectorClock, options = {}) { + return new Promise((resolve, reject) => { + options.atVectorClock = vectorClock; + requestApplicationData(options.type, options).then(result => { + if (!result.length) + return reject("File not found"); + result = result[0]; + try { + let file_data = decodeMessage(result.message); + //file is encrypted: decryption required + if (file_data instanceof Object && "secret" in file_data) { + if (!options.decrypt) + return reject("Data is encrypted"); + let decryptionKey = (options.decrypt === true) ? Crypto.AES.decrypt(user_private, aes_key) : options.decrypt; + if (!Array.isArray(decryptionKey)) + decryptionKey = [decryptionKey]; + let flag = false; + for (let key of decryptionKey) { + try { + let tmp = floCrypto.decryptData(file_data, key); + file_data = JSON.parse(tmp); + flag = true; + break; + } catch (error) { } + } + if (!flag) + return reject("Unable to decrypt file: Invalid private key"); + } + //reconstruct the file + let arraybuf = new Uint8Array(Crypto.util.base64ToBytes(file_data.content)) + result.file = new File([arraybuf], file_data.name, { type: file_data.type }); + resolve(result) + } catch (error) { + console.error(error); + reject("Data is not a file"); + } + }).catch(error => reject(error)) + }) + } + + /* + Functions: + findDiff(original, updatedObj) returns an object with the added, deleted and updated differences + mergeDiff(original, allDiff) returns a new object from original object merged with all differences (allDiff is returned object of findDiff) + */ + var diff = (function () { + const isDate = d => d instanceof Date; + const isEmpty = o => Object.keys(o).length === 0; + const isObject = o => o != null && typeof o === 'object'; + const properObject = o => isObject(o) && !o.hasOwnProperty ? { + ...o + } : o; + const getLargerArray = (l, r) => l.length > r.length ? l : r; + + const preserve = (diff, left, right) => { + if (!isObject(diff)) return diff; + return Object.keys(diff).reduce((acc, key) => { + const leftArray = left[key]; + const rightArray = right[key]; + if (Array.isArray(leftArray) && Array.isArray(rightArray)) { + const array = [...getLargerArray(leftArray, rightArray)]; + return { + ...acc, + [key]: array.reduce((acc2, item, index) => { + if (diff[key].hasOwnProperty(index)) { + acc2[index] = preserve(diff[key][index], leftArray[index], rightArray[index]); // diff recurse and check for nested arrays + return acc2; + } + delete acc2[index]; // no diff aka empty + return acc2; + }, array) + }; + } + return { + ...acc, + [key]: diff[key] + }; + }, {}); + }; + + const updatedDiff = (lhs, rhs) => { + if (lhs === rhs) return {}; + if (!isObject(lhs) || !isObject(rhs)) return rhs; + const l = properObject(lhs); + const r = properObject(rhs); + if (isDate(l) || isDate(r)) { + if (l.valueOf() == r.valueOf()) return {}; + return r; + } + return Object.keys(r).reduce((acc, key) => { + if (l.hasOwnProperty(key)) { + const difference = updatedDiff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; + return { + ...acc, + [key]: difference + }; + } + return acc; + }, {}); + }; + + + const diff = (lhs, rhs) => { + if (lhs === rhs) return {}; // equal return no diff + if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs + const l = properObject(lhs); + const r = properObject(rhs); + const deletedValues = Object.keys(l).reduce((acc, key) => { + return r.hasOwnProperty(key) ? acc : { + ...acc, + [key]: null + }; + }, {}); + if (isDate(l) || isDate(r)) { + if (l.valueOf() == r.valueOf()) return {}; + return r; + } + return Object.keys(r).reduce((acc, key) => { + if (!l.hasOwnProperty(key)) return { + ...acc, + [key]: r[key] + }; // return added r key + const difference = diff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff + return { + ...acc, + [key]: difference + }; // return updated key + }, deletedValues); + }; + + const addedDiff = (lhs, rhs) => { + if (lhs === rhs || !isObject(lhs) || !isObject(rhs)) return {}; + const l = properObject(lhs); + const r = properObject(rhs); + return Object.keys(r).reduce((acc, key) => { + if (l.hasOwnProperty(key)) { + const difference = addedDiff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference)) return acc; + return { + ...acc, + [key]: difference + }; + } + return { + ...acc, + [key]: r[key] + }; + }, {}); + }; + + const arrayDiff = (lhs, rhs) => { + if (lhs === rhs) return {}; // equal return no diff + if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs + const l = properObject(lhs); + const r = properObject(rhs); + const deletedValues = Object.keys(l).reduce((acc, key) => { + return r.hasOwnProperty(key) ? acc : { + ...acc, + [key]: null + }; + }, {}); + if (isDate(l) || isDate(r)) { + if (l.valueOf() == r.valueOf()) return {}; + return r; + } + if (Array.isArray(r) && Array.isArray(l)) { + const deletedValues = l.reduce((acc, item, index) => { + return r.hasOwnProperty(index) ? acc.concat(item) : acc.concat(null); + }, []); + return r.reduce((acc, rightItem, index) => { + if (!deletedValues.hasOwnProperty(index)) { + return acc.concat(rightItem); + } + const leftItem = l[index]; + const difference = diff(rightItem, leftItem); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) { + delete acc[index]; + return acc; // return no diff + } + return acc.slice(0, index).concat(rightItem).concat(acc.slice(index + 1)); // return updated key + }, deletedValues); + } + + return Object.keys(r).reduce((acc, key) => { + if (!l.hasOwnProperty(key)) return { + ...acc, + [key]: r[key] + }; // return added r key + const difference = diff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff + return { + ...acc, + [key]: difference + }; // return updated key + }, deletedValues); + }; + + const deletedDiff = (lhs, rhs) => { + if (lhs === rhs || !isObject(lhs) || !isObject(rhs)) return {}; + const l = properObject(lhs); + const r = properObject(rhs); + return Object.keys(l).reduce((acc, key) => { + if (r.hasOwnProperty(key)) { + const difference = deletedDiff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference)) return acc; + return { + ...acc, + [key]: difference + }; + } + return { + ...acc, + [key]: null + }; + }, {}); + }; + + const mergeRecursive = (obj1, obj2, deleteMode = false) => { + for (var p in obj2) { + try { + if (obj2[p].constructor == Object) + obj1[p] = mergeRecursive(obj1[p], obj2[p], deleteMode); + // Property in destination object set; update its value. + else if (Array.isArray(obj2[p])) { + // obj1[p] = []; + if (obj2[p].length < 1) + obj1[p] = obj2[p]; + else + obj1[p] = mergeRecursive(obj1[p], obj2[p], deleteMode); + } else + obj1[p] = deleteMode && obj2[p] === null ? undefined : obj2[p]; + } catch (e) { + // Property in destination object not set; create it and set its value. + obj1[p] = deleteMode && obj2[p] === null ? undefined : obj2[p]; + } + } + return obj1; + } + + const cleanse = (obj) => { + Object.keys(obj).forEach(key => { + var value = obj[key]; + if (typeof value === "object" && value !== null) + obj[key] = cleanse(value); + else if (typeof value === 'undefined') + delete obj[key]; // undefined, remove it + }); + if (Array.isArray(obj)) + obj = obj.filter(v => typeof v !== 'undefined'); + return obj; + } + + + const findDiff = (lhs, rhs) => ({ + added: addedDiff(lhs, rhs), + deleted: deletedDiff(lhs, rhs), + updated: updatedDiff(lhs, rhs), + }); + + /*obj is original object or array, diff is the output of findDiff */ + const mergeDiff = (obj, diff) => { + if (Object.keys(diff.updated).length !== 0) + obj = mergeRecursive(obj, diff.updated) + if (Object.keys(diff.deleted).length !== 0) { + obj = mergeRecursive(obj, diff.deleted, true) + obj = cleanse(obj) + } + if (Object.keys(diff.added).length !== 0) + obj = mergeRecursive(obj, diff.added) + return obj + } + + return { + find: findDiff, + merge: mergeDiff + } + })(); + + +})('object' === typeof module ? module.exports : window.floCloudAPI = {}); diff --git a/messenger/scripts/floCloudAPI.min.js b/messenger/scripts/floCloudAPI.min.js new file mode 100644 index 0000000..6b37419 --- /dev/null +++ b/messenger/scripts/floCloudAPI.min.js @@ -0,0 +1 @@ +!function(EXPORTS){"use strict";const floCloudAPI="object"===typeof module?module.exports:window.floCloudAPI={},DEFAULT={blockchainPrefix:35,SNStorageID:floGlobals.SNStorageID||"FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk",adminID:floGlobals.adminID,application:floGlobals.application,SNStorageName:"SuperNodeStorage",callback:(d,e)=>console.debug(d,e)};var user_id,user_public,user_private,aes_key,appObjects,generalData,lastVC;function user(id,priv){if(!priv||!id)return user.clear();let pub=floCrypto.getPubKeyHex(priv);if(!pub||!floCrypto.verifyPubKey(pub,id))return user.clear();let n=floCrypto.randInt(12,20);return aes_key=floCrypto.randString(n),user_private=Crypto.AES.encrypt(priv,aes_key),user_public=pub,user_id=id}Object.defineProperties(user,{id:{get:()=>{if(!user_id)throw"User not set";return user_id}},public:{get:()=>{if(!user_public)throw"User not set";return user_public}},sign:{value:msg=>{if(!user_private)throw"User not set";return floCrypto.signData(msg,Crypto.AES.decrypt(user_private,aes_key))}},clear:{value:()=>user_id=user_public=user_private=aes_key=void 0}}),Object.defineProperties(floCloudAPI,{SNStorageID:{get:()=>DEFAULT.SNStorageID},SNStorageName:{get:()=>DEFAULT.SNStorageName},adminID:{get:()=>DEFAULT.adminID},application:{get:()=>DEFAULT.application},user:{get:()=>user}}),Object.defineProperties(floGlobals,{appObjects:{get:()=>appObjects,set:obj=>appObjects=obj},generalData:{get:()=>generalData,set:data=>generalData=data},generalDataset:{value:(type,options={})=>generalData[filterKey(type,options)]},lastVC:{get:()=>lastVC,set:vc=>lastVC=vc}});var kBucket,supernodes={};Object.defineProperty(floCloudAPI,"nodes",{get:()=>JSON.parse(JSON.stringify(supernodes))});const K_Bucket=floCloudAPI.K_Bucket=function(masterID,nodeList){const decodeID=floID=>{let k=bitjs.Base58.decode(floID);k.shift(),k.splice(-4,4);let decodedId=Crypto.util.bytesToHex(k),nodeIdBytes=new BigInteger(decodedId,16).toByteArrayUnsigned();return new Uint8Array(nodeIdBytes)},_KB=new BuildKBucket({localNodeId:decodeID(masterID)});nodeList.forEach((id=>_KB.add({id:decodeID(id),floID:id})));const _CO=nodeList.map((id=>[_KB.distance(_KB.localNodeId,decodeID(id)),id])).sort(((a,b)=>a[0]-b[0])).map((a=>a[1]));Object.defineProperty(this,"tree",{get:()=>_KB}),Object.defineProperty(this,"list",{get:()=>Array.from(_CO)}),this.isNode=floID=>_CO.includes(floID),this.innerNodes=function(id1,id2){if(!_CO.includes(id1)||!_CO.includes(id2))throw Error("Given nodes are not supernode");let iNodes=[];for(let i=_CO.indexOf(id1)+1;_CO[i]!=id2;i++)i<_CO.length?iNodes.push(_CO[i]):i=-1;return iNodes},this.outterNodes=function(id1,id2){if(!_CO.includes(id1)||!_CO.includes(id2))throw Error("Given nodes are not supernode");let oNodes=[];for(let i=_CO.indexOf(id2)+1;_CO[i]!=id1;i++)i<_CO.length?oNodes.push(_CO[i]):i=-1;return oNodes},this.prevNode=function(id,N=1){let n=N||_CO.length;if(!_CO.includes(id))throw Error("Given node is not supernode");let pNodes=[];for(let i=0,j=_CO.indexOf(id)-1;i-1?pNodes[i++]=_CO[j]:j=_CO.length;return 1==N?pNodes[0]:pNodes},this.nextNode=function(id,N=1){let n=N||_CO.length;if(!_CO.includes(id))throw Error("Given node is not supernode");n||(n=_CO.length);let nNodes=[];for(let i=0,j=_CO.indexOf(id)+1;ik.floID));return 1==N?cNodes[0]:cNodes}};floCloudAPI.init=function(nodes){return new Promise(((resolve,reject)=>{try{supernodes=nodes,kBucket=new K_Bucket(DEFAULT.SNStorageID,Object.keys(supernodes)),resolve("Cloud init successful")}catch(error){reject(error)}}))},Object.defineProperty(floCloudAPI,"kBucket",{get:()=>kBucket});const _inactive=new Set;function ws_activeConnect(snID,reverse=!1){return new Promise(((resolve,reject)=>{if(_inactive.size===kBucket.list.length)return reject("Cloud offline");snID in supernodes||(snID=kBucket.closestNode(proxyID(snID))),function(snID){return new Promise(((resolve,reject)=>{if(!(snID in supernodes))return reject(`${snID} is not a supernode`);if(_inactive.has(snID))return reject(`${snID} is not active`);var wsConn=new WebSocket("wss://"+supernodes[snID].uri+"/");wsConn.onopen=evt=>resolve(wsConn),wsConn.onerror=evt=>{_inactive.add(snID),reject(`${snID} is unavailable`)}}))}(snID).then((node=>resolve(node))).catch((error=>{if(reverse)var nxtNode=kBucket.prevNode(snID);else nxtNode=kBucket.nextNode(snID);ws_activeConnect(nxtNode,reverse).then((node=>resolve(node))).catch((error=>reject(error)))}))}))}function fetch_ActiveAPI(snID,data,reverse=!1){return new Promise(((resolve,reject)=>{if(_inactive.size===kBucket.list.length)return reject("Cloud offline");snID in supernodes||(snID=kBucket.closestNode(proxyID(snID))),function(snID,data){return new Promise(((resolve,reject)=>{if(_inactive.has(snID))return reject(`${snID} is not active`);let fetcher,sn_url="https://"+supernodes[snID].uri;"string"==typeof data?fetcher=fetch(sn_url+"?"+data):"object"==typeof data&&"POST"===data.method&&(fetcher=fetch(sn_url,data)),fetcher.then((response=>{response.ok||400===response.status||500===response.status?resolve(response):reject(response)})).catch((error=>reject(error)))}))}(snID,data).then((result=>resolve(result))).catch((error=>{if(_inactive.add(snID),reverse)var nxtNode=kBucket.prevNode(snID);else nxtNode=kBucket.nextNode(snID);fetch_ActiveAPI(nxtNode,data,reverse).then((result=>resolve(result))).catch((error=>reject(error)))}))}))}function singleRequest(floID,data_obj,method="POST"){return new Promise(((resolve,reject)=>{let data;data="POST"===method?{method:"POST",body:JSON.stringify(data_obj)}:new URLSearchParams(JSON.parse(JSON.stringify(data_obj))).toString(),fetch_ActiveAPI(floID,data).then((response=>{response.ok?response.json().then((result=>resolve(result))).catch((error=>reject(error))):response.text().then((result=>reject(response.status+": "+result))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))}const _liveRequest={};function liveRequest(floID,request,callback){const filterData=void 0!==request.status?data=>{if(request.status)return data;{let filtered={};for(let i in data)request.trackList.includes(i)&&(filtered[i]=data[i]);return filtered}}:data=>{data=objectifier(data);let filtered={},proxy=proxyID(request.receiverID),r=request;for(let v in data){let d=data[v];r.atVectorClock&&r.atVectorClock!=v||!(r.atVectorClock||!r.lowerVectorClock||r.lowerVectorClock<=v)||!(r.atVectorClock||!r.upperVectorClock||r.upperVectorClock>=v)||r.afterTime&&!(r.afterTime{ws_activeConnect(floID).then((node=>{let randID=floCrypto.randString(5);node.send(JSON.stringify(request)),node.onmessage=evt=>{let d=null,e=null;try{d=filterData(JSON.parse(evt.data))}catch(error){e=evt.data}finally{callback(d,e)}},_liveRequest[randID]=node,_liveRequest[randID].request=request,resolve(randID)})).catch((error=>reject(error)))}))}Object.defineProperty(floCloudAPI,"liveRequest",{get:()=>_liveRequest}),Object.defineProperty(floCloudAPI,"inactive",{get:()=>_inactive});const util=floCloudAPI.util={},encodeMessage=util.encodeMessage=function(message){return btoa(unescape(encodeURIComponent(JSON.stringify(message))))},decodeMessage=util.decodeMessage=function(message){return JSON.parse(decodeURIComponent(escape(atob(message))))},filterKey=util.filterKey=function(type,options={}){return type+(options.comment?":"+options.comment:"")+"|"+(options.group||options.receiverID||DEFAULT.adminID)+"|"+(options.application||DEFAULT.application)},proxyID=util.proxyID=function(address){if(address){var bytes;if(33==address.length||34==address.length){let decode=bitjs.Base58.decode(address);bytes=decode.slice(0,decode.length-4);let checksum=decode.slice(decode.length-4),hash=Crypto.SHA256(Crypto.SHA256(bytes,{asBytes:!0}),{asBytes:!0});hash[0]!=checksum[0]||hash[1]!=checksum[1]||hash[2]!=checksum[2]||hash[3]!=checksum[3]?bytes=void 0:bytes.shift()}else if(!address.startsWith("0x")&&42==address.length||62==address.length){if("function"!=typeof coinjs)throw"library missing (lib_btc.js)";let decode=coinjs.bech32_decode(address);decode&&((bytes=decode.data).shift(),bytes=coinjs.bech32_convert(bytes,5,8,!1),62==address.length&&(bytes=coinjs.bech32_convert(bytes,5,8,!1)))}else 66==address.length?bytes=ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(address),{asBytes:!0})):(42==address.length&&address.startsWith("0x")||40==address.length&&!address.startsWith("0x"))&&(address.startsWith("0x")&&(address=address.substring(2)),bytes=Crypto.util.hexToBytes(address));if(bytes){bytes.unshift(DEFAULT.blockchainPrefix);let hash=Crypto.SHA256(Crypto.SHA256(bytes,{asBytes:!0}),{asBytes:!0});return bitjs.Base58.encode(bytes.concat(hash.slice(0,4)))}throw"Invalid address: "+address}},lastCommit={};function updateObject(objectName,dataSet){try{console.log(dataSet);let vcList=Object.keys(dataSet).sort();for(let vc of vcList)if(!(vclastVC[fk]&&(lastVC[fk]=dataSet[vc].log_time);compactIDB.writeData("lastVC",lastVC[fk],fk),compactIDB.writeData("generalData",generalData[fk],fk)}catch(error){console.error(error)}}function objectifier(data){return Array.isArray(data)||(data=[data]),Object.fromEntries(data.map((d=>(d.message=decodeMessage(d.message),[d.vectorClock,d]))))}Object.defineProperty(lastCommit,"get",{value:objName=>JSON.parse(lastCommit[objName])}),Object.defineProperty(lastCommit,"set",{value:objName=>lastCommit[objName]=JSON.stringify(appObjects[objName])}),floCloudAPI.setStatus=function(options={}){return new Promise(((resolve,reject)=>{let callback=options.callback instanceof Function?options.callback:DEFAULT.callback;var request={floID:user.id,application:options.application||DEFAULT.application,time:Date.now(),status:!0,pubKey:user.public};let hashcontent=["time","application","floID"].map((d=>request[d])).join("|");request.sign=user.sign(hashcontent),liveRequest(options.refID||DEFAULT.adminID,request,callback).then((result=>resolve(result))).catch((error=>reject(error)))}))},floCloudAPI.requestStatus=function(trackList,options={}){return new Promise(((resolve,reject)=>{Array.isArray(trackList)||(trackList=[trackList]);let callback=options.callback instanceof Function?options.callback:DEFAULT.callback,request={status:!1,application:options.application||DEFAULT.application,trackList:trackList};liveRequest(options.refID||DEFAULT.adminID,request,callback).then((result=>resolve(result))).catch((error=>reject(error)))}))};const sendApplicationData=floCloudAPI.sendApplicationData=function(message,type,options={}){return new Promise(((resolve,reject)=>{var data={senderID:user.id,receiverID:options.receiverID||DEFAULT.adminID,pubKey:user.public,message:encodeMessage(message),time:Date.now(),application:options.application||DEFAULT.application,type:type,comment:options.comment||""};let hashcontent=["receiverID","time","application","type","message","comment"].map((d=>data[d])).join("|");data.sign=user.sign(hashcontent),singleRequest(data.receiverID,data).then((result=>resolve(result))).catch((error=>reject(error)))}))},requestApplicationData=floCloudAPI.requestApplicationData=function(type,options={}){return new Promise(((resolve,reject)=>{var request={receiverID:options.receiverID||DEFAULT.adminID,senderID:options.senderID||void 0,application:options.application||DEFAULT.application,type:type,comment:options.comment||void 0,lowerVectorClock:options.lowerVectorClock||void 0,upperVectorClock:options.upperVectorClock||void 0,atVectorClock:options.atVectorClock||void 0,afterTime:options.afterTime||void 0,mostRecent:options.mostRecent||void 0};options.callback instanceof Function?liveRequest(request.receiverID,request,options.callback).then((result=>resolve(result))).catch((error=>reject(error))):("POST"===options.method&&(request={time:Date.now(),request:request}),singleRequest(request.receiverID,request,options.method||"GET").then((data=>resolve(data))).catch((error=>reject(error))))}))};floCloudAPI.editApplicationData=function(vectorClock,comment_edit,options={}){return new Promise(((resolve,reject)=>{let req_options=Object.assign({},options);req_options.atVectorClock=vectorClock,requestApplicationData(void 0,req_options).then((result=>{if(!result.length)return reject("Data not found");let data=result[0];if(data.senderID!==user.id)return reject("Only sender can edit comment");data.comment=comment_edit;let hashcontent=["receiverID","time","application","type","message","comment"].map((d=>data[d])).join("|"),re_sign=user.sign(hashcontent);var request={receiverID:options.receiverID||DEFAULT.adminID,requestorID:user.id,pubKey:user.public,time:Date.now(),vectorClock:vectorClock,edit:comment_edit,re_sign:re_sign};let request_hash=["time","vectorClock","edit","re_sign"].map((d=>request[d])).join("|");request.sign=user.sign(request_hash),singleRequest(request.receiverID,request).then((result=>resolve(result))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},floCloudAPI.tagApplicationData=function(vectorClock,tag,options={}){return new Promise(((resolve,reject)=>{if(!floGlobals.subAdmins.includes(user.id))return reject("Only subAdmins can tag data");var request={receiverID:options.receiverID||DEFAULT.adminID,requestorID:user.id,pubKey:user.public,time:Date.now(),vectorClock:vectorClock,tag:tag};let hashcontent=["time","vectorClock","tag"].map((d=>request[d])).join("|");request.sign=user.sign(hashcontent),singleRequest(request.receiverID,request).then((result=>resolve(result))).catch((error=>reject(error)))}))},floCloudAPI.noteApplicationData=function(vectorClock,note,options={}){return new Promise(((resolve,reject)=>{var request={receiverID:options.receiverID||DEFAULT.adminID,requestorID:user.id,pubKey:user.public,time:Date.now(),vectorClock:vectorClock,note:note};let hashcontent=["time","vectorClock","note"].map((d=>request[d])).join("|");request.sign=user.sign(hashcontent),singleRequest(request.receiverID,request).then((result=>resolve(result))).catch((error=>reject(error)))}))},floCloudAPI.sendGeneralData=function(message,type,options={}){return new Promise(((resolve,reject)=>{if(options.encrypt){let encryptionKey=!0===options.encrypt?floGlobals.settings.encryptionKey:options.encrypt;message=floCrypto.encryptData(JSON.stringify(message),encryptionKey)}sendApplicationData(message,type,options).then((result=>resolve(result))).catch((error=>reject(error)))}))},floCloudAPI.requestGeneralData=function(type,options={}){return new Promise(((resolve,reject)=>{var fk=filterKey(type,options);if(lastVC[fk]=parseInt(lastVC[fk])||0,options.afterTime=options.afterTime||lastVC[fk],options.callback instanceof Function){let new_options=Object.create(options);new_options.callback=(d,e)=>{storeGeneral(fk,d),options.callback(d,e)},requestApplicationData(type,new_options).then((result=>resolve(result))).catch((error=>reject(error)))}else requestApplicationData(type,options).then((dataSet=>{storeGeneral(fk,objectifier(dataSet)),resolve(dataSet)})).catch((error=>reject(error)))}))},floCloudAPI.requestObjectData=function(objectName,options={}){return new Promise(((resolve,reject)=>{options.lowerVectorClock=options.lowerVectorClock||lastVC[objectName]+1,options.senderID=[!1,null].includes(options.senderID)?null:options.senderID||floGlobals.subAdmins,options.mostRecent=!0,options.comment="RESET";let callback=null;if(options.callback instanceof Function){let old_callback=options.callback;callback=(d,e)=>{updateObject(objectName,d),old_callback(d,e)},delete options.callback}requestApplicationData(objectName,options).then((dataSet=>{if(updateObject(objectName,objectifier(dataSet)),delete options.comment,options.lowerVectorClock=lastVC[objectName]+1,delete options.mostRecent,callback){let new_options=Object.create(options);new_options.callback=callback,requestApplicationData(objectName,new_options).then((result=>resolve(result))).catch((error=>reject(error)))}else requestApplicationData(objectName,options).then((dataSet=>{updateObject(objectName,objectifier(dataSet)),resolve(appObjects[objectName])})).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},floCloudAPI.closeRequest=function(requestID){return new Promise(((resolve,reject)=>{let conn=_liveRequest[requestID];if(!conn)return reject("Request not found");conn.onclose=evt=>{delete _liveRequest[requestID],resolve("Request connection closed")},conn.close()}))},floCloudAPI.resetObjectData=function(objectName,options={}){return new Promise(((resolve,reject)=>{let message={reset:appObjects[objectName]};options.comment="RESET",sendApplicationData(message,objectName,options).then((result=>{lastCommit.set(objectName),resolve(result)})).catch((error=>reject(error)))}))},floCloudAPI.updateObjectData=function(objectName,options={}){return new Promise(((resolve,reject)=>{let message={diff:diff.find(lastCommit.get(objectName),appObjects[objectName])};options.comment="UPDATE",sendApplicationData(message,objectName,options).then((result=>{lastCommit.set(objectName),resolve(result)})).catch((error=>reject(error)))}))},floCloudAPI.uploadFile=function(fileBlob,type,options={}){return new Promise(((resolve,reject)=>{if(!(fileBlob instanceof File||fileBlob instanceof Blob))return reject("file must be instance of File/Blob");fileBlob.arrayBuffer().then((arraybuf=>{let file_data={type:fileBlob.type,name:fileBlob.name};if(file_data.content=Crypto.util.bytesToBase64(new Uint8Array(arraybuf)),options.encrypt){let encryptionKey=!0===options.encrypt?floGlobals.settings.encryptionKey:options.encrypt;file_data=floCrypto.encryptData(JSON.stringify(file_data),encryptionKey)}sendApplicationData(file_data,type,options).then((({vectorClock:vectorClock,receiverID:receiverID,type:type,application:application})=>resolve({vectorClock:vectorClock,receiverID:receiverID,type:type,application:application}))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},floCloudAPI.downloadFile=function(vectorClock,options={}){return new Promise(((resolve,reject)=>{options.atVectorClock=vectorClock,requestApplicationData(options.type,options).then((result=>{if(!result.length)return reject("File not found");result=result[0];try{let file_data=decodeMessage(result.message);if(file_data instanceof Object&&"secret"in file_data){if(!options.decrypt)return reject("Data is encrypted");let decryptionKey=!0===options.decrypt?Crypto.AES.decrypt(user_private,aes_key):options.decrypt;Array.isArray(decryptionKey)||(decryptionKey=[decryptionKey]);let flag=!1;for(let key of decryptionKey)try{let tmp=floCrypto.decryptData(file_data,key);file_data=JSON.parse(tmp),flag=!0;break}catch(error){}if(!flag)return reject("Unable to decrypt file: Invalid private key")}let arraybuf=new Uint8Array(Crypto.util.base64ToBytes(file_data.content));result.file=new File([arraybuf],file_data.name,{type:file_data.type}),resolve(result)}catch(error){console.error(error),reject("Data is not a file")}})).catch((error=>reject(error)))}))};var diff=function(){const isDate=d=>d instanceof Date,isEmpty=o=>0===Object.keys(o).length,isObject=o=>null!=o&&"object"==typeof o,properObject=o=>isObject(o)&&!o.hasOwnProperty?{...o}:o,updatedDiff=(lhs,rhs)=>{if(lhs===rhs)return{};if(!isObject(lhs)||!isObject(rhs))return rhs;const l=properObject(lhs),r=properObject(rhs);return isDate(l)||isDate(r)?l.valueOf()==r.valueOf()?{}:r:Object.keys(r).reduce(((acc,key)=>{if(l.hasOwnProperty(key)){const difference=updatedDiff(l[key],r[key]);return isObject(difference)&&isEmpty(difference)&&!isDate(difference)?acc:{...acc,[key]:difference}}return acc}),{})},addedDiff=(lhs,rhs)=>{if(lhs===rhs||!isObject(lhs)||!isObject(rhs))return{};const l=properObject(lhs),r=properObject(rhs);return Object.keys(r).reduce(((acc,key)=>{if(l.hasOwnProperty(key)){const difference=addedDiff(l[key],r[key]);return isObject(difference)&&isEmpty(difference)?acc:{...acc,[key]:difference}}return{...acc,[key]:r[key]}}),{})},deletedDiff=(lhs,rhs)=>{if(lhs===rhs||!isObject(lhs)||!isObject(rhs))return{};const l=properObject(lhs),r=properObject(rhs);return Object.keys(l).reduce(((acc,key)=>{if(r.hasOwnProperty(key)){const difference=deletedDiff(l[key],r[key]);return isObject(difference)&&isEmpty(difference)?acc:{...acc,[key]:difference}}return{...acc,[key]:null}}),{})},mergeRecursive=(obj1,obj2,deleteMode=!1)=>{for(var p in obj2)try{obj2[p].constructor==Object?obj1[p]=mergeRecursive(obj1[p],obj2[p],deleteMode):Array.isArray(obj2[p])?obj2[p].length<1?obj1[p]=obj2[p]:obj1[p]=mergeRecursive(obj1[p],obj2[p],deleteMode):obj1[p]=deleteMode&&null===obj2[p]?void 0:obj2[p]}catch(e){obj1[p]=deleteMode&&null===obj2[p]?void 0:obj2[p]}return obj1},cleanse=obj=>(Object.keys(obj).forEach((key=>{var value=obj[key];"object"==typeof value&&null!==value?obj[key]=cleanse(value):void 0===value&&delete obj[key]})),Array.isArray(obj)&&(obj=obj.filter((v=>void 0!==v))),obj);return{find:(lhs,rhs)=>({added:addedDiff(lhs,rhs),deleted:deletedDiff(lhs,rhs),updated:updatedDiff(lhs,rhs)}),merge:(obj,diff)=>(0!==Object.keys(diff.updated).length&&(obj=mergeRecursive(obj,diff.updated)),0!==Object.keys(diff.deleted).length&&(obj=mergeRecursive(obj,diff.deleted,!0),obj=cleanse(obj)),0!==Object.keys(diff.added).length&&(obj=mergeRecursive(obj,diff.added)),obj)}}()}(); \ No newline at end of file diff --git a/messenger/scripts/floCrypto.js b/messenger/scripts/floCrypto.js new file mode 100644 index 0000000..db90758 --- /dev/null +++ b/messenger/scripts/floCrypto.js @@ -0,0 +1,541 @@ +(function (EXPORTS) { //floCrypto v2.3.6a + /* FLO Crypto Operators */ + 'use strict'; + const floCrypto = EXPORTS; + + const p = BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16); + const ecparams = EllipticCurve.getSECCurveByName("secp256k1"); + const ascii_alternatives = `‘ '\n’ '\n“ "\n” "\n– --\n— ---\n≥ >=\n≤ <=\n≠ !=\n× *\n÷ /\n← <-\n→ ->\n↔ <->\n⇒ =>\n⇐ <=\n⇔ <=>`; + const exponent1 = () => p.add(BigInteger.ONE).divide(BigInteger("4")); + coinjs.compressed = true; //defaulting coinjs compressed to true; + + function calculateY(x) { + let exp = exponent1(); + // x is x value of public key in BigInteger format without 02 or 03 or 04 prefix + return x.modPow(BigInteger("3"), p).add(BigInteger("7")).mod(p).modPow(exp, p) + } + + function getUncompressedPublicKey(compressedPublicKey) { + // Fetch x from compressedPublicKey + let pubKeyBytes = Crypto.util.hexToBytes(compressedPublicKey); + const prefix = pubKeyBytes.shift() // remove prefix + let prefix_modulus = prefix % 2; + pubKeyBytes.unshift(0) // add prefix 0 + let x = new BigInteger(pubKeyBytes) + let xDecimalValue = x.toString() + // Fetch y + let y = calculateY(x); + let yDecimalValue = y.toString(); + // verify y value + let resultBigInt = y.mod(BigInteger("2")); + let check = resultBigInt.toString() % 2; + if (prefix_modulus !== check) + yDecimalValue = y.negate().mod(p).toString(); + return { + x: xDecimalValue, + y: yDecimalValue + }; + } + + function getSenderPublicKeyString() { + let privateKey = ellipticCurveEncryption.senderRandom(); + var senderPublicKeyString = ellipticCurveEncryption.senderPublicString(privateKey); + return { + privateKey: privateKey, + senderPublicKeyString: senderPublicKeyString + } + } + + function deriveSharedKeySender(receiverPublicKeyHex, senderPrivateKey) { + let receiverPublicKeyString = getUncompressedPublicKey(receiverPublicKeyHex); + var senderDerivedKey = ellipticCurveEncryption.senderSharedKeyDerivation( + receiverPublicKeyString.x, receiverPublicKeyString.y, senderPrivateKey); + return senderDerivedKey; + } + + function deriveSharedKeyReceiver(senderPublicKeyString, receiverPrivateKey) { + return ellipticCurveEncryption.receiverSharedKeyDerivation( + senderPublicKeyString.XValuePublicString, senderPublicKeyString.YValuePublicString, receiverPrivateKey); + } + + function getReceiverPublicKeyString(privateKey) { + return ellipticCurveEncryption.receiverPublicString(privateKey); + } + + function wifToDecimal(pk_wif, isPubKeyCompressed = false) { + let pk = Bitcoin.Base58.decode(pk_wif) + pk.shift() + pk.splice(-4, 4) + //If the private key corresponded to a compressed public key, also drop the last byte (it should be 0x01). + if (isPubKeyCompressed == true) pk.pop() + pk.unshift(0) + let privateKeyDecimal = BigInteger(pk).toString() + let privateKeyHex = Crypto.util.bytesToHex(pk) + return { + privateKeyDecimal: privateKeyDecimal, + privateKeyHex: privateKeyHex + } + } + + //generate a random Interger within range + floCrypto.randInt = function (min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(securedMathRandom() * (max - min + 1)) + min; + } + + //generate a random String within length (options : alphaNumeric chars only) + floCrypto.randString = function (length, alphaNumeric = true) { + var result = ''; + var characters = alphaNumeric ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' : + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+-./*?@#&$<>=[]{}():'; + for (var i = 0; i < length; i++) + result += characters.charAt(Math.floor(securedMathRandom() * characters.length)); + return result; + } + + //Encrypt Data using public-key + floCrypto.encryptData = function (data, receiverPublicKeyHex) { + var senderECKeyData = getSenderPublicKeyString(); + var senderDerivedKey = deriveSharedKeySender(receiverPublicKeyHex, senderECKeyData.privateKey); + let senderKey = senderDerivedKey.XValue + senderDerivedKey.YValue; + let secret = Crypto.AES.encrypt(data, senderKey); + return { + secret: secret, + senderPublicKeyString: senderECKeyData.senderPublicKeyString + }; + } + + //Decrypt Data using private-key + floCrypto.decryptData = function (data, privateKeyHex) { + var receiverECKeyData = {}; + if (typeof privateKeyHex !== "string") throw new Error("No private key found."); + let privateKey = wifToDecimal(privateKeyHex, true); + if (typeof privateKey.privateKeyDecimal !== "string") throw new Error("Failed to detremine your private key."); + receiverECKeyData.privateKey = privateKey.privateKeyDecimal; + var receiverDerivedKey = deriveSharedKeyReceiver(data.senderPublicKeyString, receiverECKeyData.privateKey); + let receiverKey = receiverDerivedKey.XValue + receiverDerivedKey.YValue; + let decryptMsg = Crypto.AES.decrypt(data.secret, receiverKey); + return decryptMsg; + } + + //Sign data using private-key + floCrypto.signData = function (data, privateKeyHex) { + var key = new Bitcoin.ECKey(privateKeyHex); + var messageHash = Crypto.SHA256(data); + var messageSign = Bitcoin.ECDSA.sign(messageHash, key.priv); + var sighex = Crypto.util.bytesToHex(messageSign); + return sighex; + } + + //Verify signatue of the data using public-key + floCrypto.verifySign = function (data, signatureHex, publicKeyHex) { + var msgHash = Crypto.SHA256(data); + var sigBytes = Crypto.util.hexToBytes(signatureHex); + var publicKeyPoint = ecparams.getCurve().decodePointHex(publicKeyHex); + var verify = Bitcoin.ECDSA.verify(msgHash, sigBytes, publicKeyPoint); + return verify; + } + + //Generates a new flo ID and returns private-key, public-key and floID + const generateNewID = floCrypto.generateNewID = function () { + var key = new Bitcoin.ECKey(false); + key.setCompressed(true); + return { + floID: key.getBitcoinAddress(), + pubKey: key.getPubKeyHex(), + privKey: key.getBitcoinWalletImportFormat() + } + } + + Object.defineProperties(floCrypto, { + newID: { + get: () => generateNewID() + }, + hashID: { + value: (str) => { + let bytes = ripemd160(Crypto.SHA256(str, { asBytes: true }), { asBytes: true }); + bytes.unshift(bitjs.pub); + var hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + return bitjs.Base58.encode(bytes.concat(checksum)); + } + }, + tmpID: { + get: () => { + let bytes = Crypto.util.randomBytes(20); + bytes.unshift(bitjs.pub); + var hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + return bitjs.Base58.encode(bytes.concat(checksum)); + } + } + }); + + //Returns public-key from private-key + floCrypto.getPubKeyHex = function (privateKeyHex) { + if (!privateKeyHex) + return null; + var key = new Bitcoin.ECKey(privateKeyHex); + if (key.priv == null) + return null; + key.setCompressed(true); + return key.getPubKeyHex(); + } + + //Returns flo-ID from public-key or private-key + floCrypto.getFloID = function (keyHex) { + if (!keyHex) + return null; + try { + var key = new Bitcoin.ECKey(keyHex); + if (key.priv == null) + key.setPub(keyHex); + return key.getBitcoinAddress(); + } catch { + return null; + } + } + + floCrypto.getAddress = function (privateKeyHex, strict = false) { + if (!privateKeyHex) + return; + var key = new Bitcoin.ECKey(privateKeyHex); + if (key.priv == null) + return null; + key.setCompressed(true); + let pubKey = key.getPubKeyHex(), + version = bitjs.Base58.decode(privateKeyHex)[0]; + switch (version) { + case coinjs.priv: //BTC + return coinjs.bech32Address(pubKey).address; + case bitjs.priv: //FLO + return bitjs.pubkey2address(pubKey); + default: + return strict ? false : bitjs.pubkey2address(pubKey); //default to FLO address (if strict=false) + } + } + + //Verify the private-key for the given public-key or flo-ID + floCrypto.verifyPrivKey = function (privateKeyHex, pubKey_floID, isfloID = true) { + if (!privateKeyHex || !pubKey_floID) + return false; + try { + var key = new Bitcoin.ECKey(privateKeyHex); + if (key.priv == null) + return false; + key.setCompressed(true); + if (isfloID && pubKey_floID == key.getBitcoinAddress()) + return true; + else if (!isfloID && pubKey_floID.toUpperCase() == key.getPubKeyHex().toUpperCase()) + return true; + else + return false; + } catch { + return null; + } + } + + floCrypto.getMultisigAddress = function (publicKeyList, requiredSignatures) { + if (!Array.isArray(publicKeyList) || !publicKeyList.length) + return null; + if (!Number.isInteger(requiredSignatures) || requiredSignatures < 1 || requiredSignatures > publicKeyList.length) + return null; + try { + var multisig = bitjs.pubkeys2multisig(publicKeyList, requiredSignatures); + return multisig; + } catch { + return null; + } + } + + floCrypto.decodeRedeemScript = function (redeemScript) { + try { + var decoded = bitjs.transaction().decodeRedeemScript(redeemScript); + return decoded; + } catch { + return null; + } + } + + //Check if the given flo-id is valid or not + floCrypto.validateFloID = function (floID, regularOnly = false) { + if (!floID) + return false; + try { + let addr = new Bitcoin.Address(floID); + if (regularOnly && addr.version != Bitcoin.Address.standardVersion) + return false; + return true; + } catch { + return false; + } + } + + //Check if the given address (any blockchain) is valid or not + floCrypto.validateAddr = function (address, std = true, bech = true) { + let raw = decodeAddress(address); + if (!raw) + return false; + if (typeof raw.version !== 'undefined') { //legacy or segwit + if (std == false) + return false; + else if (std === true || (!Array.isArray(std) && std === raw.version) || (Array.isArray(std) && std.includes(raw.version))) + return true; + else + return false; + } else if (typeof raw.bech_version !== 'undefined') { //bech32 + if (bech === false) + return false; + else if (bech === true || (!Array.isArray(bech) && bech === raw.bech_version) || (Array.isArray(bech) && bech.includes(raw.bech_version))) + return true; + else + return false; + } else if (raw.type === 'ethereum') { + return true + } else //unknown + return false; + } + + //Check the public-key (or redeem-script) for the address (any blockchain) + floCrypto.verifyPubKey = function (pubKeyHex, address) { + let raw = decodeAddress(address); + if (!raw) + return; + let pub_hash = Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex), { asBytes: true }))); + if (typeof raw.bech_version !== 'undefined' && raw.bytes.length == 32) //bech32-multisig + raw.hex = Crypto.util.bytesToHex(ripemd160(raw.bytes, { asBytes: true })); + return pub_hash === raw.hex; + } + + //Convert the given address (any blockchain) to equivalent floID + floCrypto.toFloID = function (address, options = null) { + if (!address) + return; + let raw = decodeAddress(address); + if (!raw) + return; + else if (options) { //if (optional) version check is passed + if (typeof raw.version !== 'undefined' && (!options.std || !options.std.includes(raw.version))) + return; + if (typeof raw.bech_version !== 'undefined' && (!options.bech || !options.bech.includes(raw.bech_version))) + return; + } + raw.bytes.unshift(bitjs.pub); + let hash = Crypto.SHA256(Crypto.SHA256(raw.bytes, { + asBytes: true + }), { + asBytes: true + }); + return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4))); + } + + //Convert raw address bytes to floID + floCrypto.rawToFloID = function (raw_bytes) { + if (typeof raw_bytes === 'string') + raw_bytes = Crypto.util.hexToBytes(raw_bytes); + if (raw_bytes.length != 20) + return null; + raw_bytes.unshift(bitjs.pub); + let hash = Crypto.SHA256(Crypto.SHA256(raw_bytes, { + asBytes: true + }), { + asBytes: true + }); + return bitjs.Base58.encode(raw_bytes.concat(hash.slice(0, 4))); + } + + //Convert the given multisig address (any blockchain) to equivalent multisig floID + floCrypto.toMultisigFloID = function (address, options = null) { + if (!address) + return; + let raw = decodeAddress(address); + if (!raw) + return; + else if (options) { //if (optional) version check is passed + if (typeof raw.version !== 'undefined' && (!options.std || !options.std.includes(raw.version))) + return; + if (typeof raw.bech_version !== 'undefined' && (!options.bech || !options.bech.includes(raw.bech_version))) + return; + } + if (typeof raw.bech_version !== 'undefined') { + if (raw.bytes.length != 32) return; //multisig bech address have 32 bytes + //multisig-bech:hash=SHA256 whereas multisig:hash=r160(SHA265), thus ripemd160 the bytes from multisig-bech + raw.bytes = ripemd160(raw.bytes, { + asBytes: true + }); + } + raw.bytes.unshift(bitjs.multisig); + let hash = Crypto.SHA256(Crypto.SHA256(raw.bytes, { + asBytes: true + }), { + asBytes: true + }); + return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4))); + } + + //Checks if the given addresses (any blockchain) are same (w.r.t keys) + floCrypto.isSameAddr = function (addr1, addr2) { + if (!addr1 || !addr2) + return; + let raw1 = decodeAddress(addr1), + raw2 = decodeAddress(addr2); + if (!raw1 || !raw2) + return false; + else { + if (typeof raw1.bech_version !== 'undefined' && raw1.bytes.length == 32) //bech32-multisig + raw1.hex = Crypto.util.bytesToHex(ripemd160(raw1.bytes, { asBytes: true })); + if (typeof raw2.bech_version !== 'undefined' && raw2.bytes.length == 32) //bech32-multisig + raw2.hex = Crypto.util.bytesToHex(ripemd160(raw2.bytes, { asBytes: true })); + return raw1.hex === raw2.hex; + } + } + + const decodeAddress = floCrypto.decodeAddr = function (address) { + if (!address) + return; + else if (address.length == 33 || address.length == 34) { //legacy encoding + let decode = bitjs.Base58.decode(address); + let bytes = decode.slice(0, decode.length - 4); + let checksum = decode.slice(decode.length - 4), + hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + return (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3]) ? null : { + version: bytes.shift(), + hex: Crypto.util.bytesToHex(bytes), + bytes + } + } else if (!address.startsWith("0x") && address.length == 42 || address.length == 62) { //bech encoding + let decode = coinjs.bech32_decode(address); + if (decode) { + let bytes = decode.data; + let bech_version = bytes.shift(); + bytes = coinjs.bech32_convert(bytes, 5, 8, false); + return { + bech_version, + hrp: decode.hrp, + hex: Crypto.util.bytesToHex(bytes), + bytes + } + } else + return null; + } else if ((address.length == 42 && address.startsWith("0x")) || (address.length == 40 && !address.startsWith("0x"))) { //Ethereum Address + if (address.startsWith("0x")) { address = address.substring(2); } + let bytes = Crypto.util.hexToBytes(address); + return { + version: 1, + hex: address, + type: 'ethereum', + bytes + } + } + } + + //Split the str using shamir's Secret and Returns the shares + floCrypto.createShamirsSecretShares = function (str, total_shares, threshold_limit) { + try { + if (str.length > 0) { + var strHex = shamirSecretShare.str2hex(str); + var shares = shamirSecretShare.share(strHex, total_shares, threshold_limit); + return shares; + } + return false; + } catch { + return false + } + } + + //Returns the retrived secret by combining the shamirs shares + const retrieveShamirSecret = floCrypto.retrieveShamirSecret = function (sharesArray) { + try { + if (sharesArray.length > 0) { + var comb = shamirSecretShare.combine(sharesArray.slice(0, sharesArray.length)); + comb = shamirSecretShare.hex2str(comb); + return comb; + } + return false; + } catch { + return false; + } + } + + //Verifies the shares and str + floCrypto.verifyShamirsSecret = function (sharesArray, str) { + if (!str) + return null; + else if (retrieveShamirSecret(sharesArray) === str) + return true; + else + return false; + } + + const validateASCII = floCrypto.validateASCII = function (string, bool = true) { + if (typeof string !== "string") + return null; + if (bool) { + let x; + for (let i = 0; i < string.length; i++) { + x = string.charCodeAt(i); + if (x < 32 || x > 127) + return false; + } + return true; + } else { + let x, invalids = {}; + for (let i = 0; i < string.length; i++) { + x = string.charCodeAt(i); + if (x < 32 || x > 127) + if (x in invalids) + invalids[string[i]].push(i) + else + invalids[string[i]] = [i]; + } + if (Object.keys(invalids).length) + return invalids; + else + return true; + } + } + + floCrypto.convertToASCII = function (string, mode = 'soft-remove') { + let chars = validateASCII(string, false); + if (chars === true) + return string; + else if (chars === null) + return null; + let convertor, result = string, + refAlt = {}; + ascii_alternatives.split('\n').forEach(a => refAlt[a[0]] = a.slice(2)); + mode = mode.toLowerCase(); + if (mode === "hard-unicode") + convertor = (c) => `\\u${('000' + c.charCodeAt().toString(16)).slice(-4)}`; + else if (mode === "soft-unicode") + convertor = (c) => refAlt[c] || `\\u${('000' + c.charCodeAt().toString(16)).slice(-4)}`; + else if (mode === "hard-remove") + convertor = c => ""; + else if (mode === "soft-remove") + convertor = c => refAlt[c] || ""; + else + return null; + for (let c in chars) + result = result.replaceAll(c, convertor(c)); + return result; + } + + floCrypto.revertUnicode = function (string) { + return string.replace(/\\u[\dA-F]{4}/gi, + m => String.fromCharCode(parseInt(m.replace(/\\u/g, ''), 16))); + } + +})('object' === typeof module ? module.exports : window.floCrypto = {}); diff --git a/messenger/scripts/floCrypto.min.js b/messenger/scripts/floCrypto.min.js new file mode 100644 index 0000000..68f8c4c --- /dev/null +++ b/messenger/scripts/floCrypto.min.js @@ -0,0 +1 @@ +!function(EXPORTS){"use strict";const floCrypto="object"===typeof module?module.exports:window.floCrypto={},p=BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F",16),ecparams=EllipticCurve.getSECCurveByName("secp256k1"),ascii_alternatives="‘ '\n’ '\n“ \"\n” \"\n– --\n— ---\n≥ >=\n≤ <=\n≠ !=\n× *\n÷ /\n← <-\n→ ->\n↔ <->\n⇒ =>\n⇐ <=\n⇔ <=>",exponent1=()=>p.add(BigInteger.ONE).divide(BigInteger("4"));function getUncompressedPublicKey(compressedPublicKey){let pubKeyBytes=Crypto.util.hexToBytes(compressedPublicKey);let prefix_modulus=pubKeyBytes.shift()%2;pubKeyBytes.unshift(0);let x=new BigInteger(pubKeyBytes),xDecimalValue=x.toString(),y=function(x){let exp=exponent1();return x.modPow(BigInteger("3"),p).add(BigInteger("7")).mod(p).modPow(exp,p)}(x),yDecimalValue=y.toString();return prefix_modulus!==y.mod(BigInteger("2")).toString()%2&&(yDecimalValue=y.negate().mod(p).toString()),{x:xDecimalValue,y:yDecimalValue}}coinjs.compressed=!0,floCrypto.randInt=function(min,max){return min=Math.ceil(min),max=Math.floor(max),Math.floor(securedMathRandom()*(max-min+1))+min},floCrypto.randString=function(length,alphaNumeric=!0){for(var result="",characters=alphaNumeric?"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789":"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+-./*?@#&$<>=[]{}():",i=0;igenerateNewID()},hashID:{value:str=>{let bytes=ripemd160(Crypto.SHA256(str,{asBytes:!0}),{asBytes:!0});bytes.unshift(bitjs.pub);var checksum=Crypto.SHA256(Crypto.SHA256(bytes,{asBytes:!0}),{asBytes:!0}).slice(0,4);return bitjs.Base58.encode(bytes.concat(checksum))}},tmpID:{get:()=>{let bytes=Crypto.util.randomBytes(20);bytes.unshift(bitjs.pub);var checksum=Crypto.SHA256(Crypto.SHA256(bytes,{asBytes:!0}),{asBytes:!0}).slice(0,4);return bitjs.Base58.encode(bytes.concat(checksum))}}}),floCrypto.getPubKeyHex=function(privateKeyHex){if(!privateKeyHex)return null;var key=new Bitcoin.ECKey(privateKeyHex);return null==key.priv?null:(key.setCompressed(!0),key.getPubKeyHex())},floCrypto.getFloID=function(keyHex){if(!keyHex)return null;try{var key=new Bitcoin.ECKey(keyHex);return null==key.priv&&key.setPub(keyHex),key.getBitcoinAddress()}catch{return null}},floCrypto.getAddress=function(privateKeyHex,strict=!1){if(!privateKeyHex)return;var key=new Bitcoin.ECKey(privateKeyHex);if(null==key.priv)return null;key.setCompressed(!0);let pubKey=key.getPubKeyHex();switch(bitjs.Base58.decode(privateKeyHex)[0]){case coinjs.priv:return coinjs.bech32Address(pubKey).address;case bitjs.priv:return bitjs.pubkey2address(pubKey);default:return!strict&&bitjs.pubkey2address(pubKey)}},floCrypto.verifyPrivKey=function(privateKeyHex,pubKey_floID,isfloID=!0){if(!privateKeyHex||!pubKey_floID)return!1;try{var key=new Bitcoin.ECKey(privateKeyHex);return null!=key.priv&&(key.setCompressed(!0),!(!isfloID||pubKey_floID!=key.getBitcoinAddress())||!isfloID&&pubKey_floID.toUpperCase()==key.getPubKeyHex().toUpperCase())}catch{return null}},floCrypto.getMultisigAddress=function(publicKeyList,requiredSignatures){if(!Array.isArray(publicKeyList)||!publicKeyList.length)return null;if(!Number.isInteger(requiredSignatures)||requiredSignatures<1||requiredSignatures>publicKeyList.length)return null;try{return bitjs.pubkeys2multisig(publicKeyList,requiredSignatures)}catch{return null}},floCrypto.decodeRedeemScript=function(redeemScript){try{return bitjs.transaction().decodeRedeemScript(redeemScript)}catch{return null}},floCrypto.validateFloID=function(floID,regularOnly=!1){if(!floID)return!1;try{let addr=new Bitcoin.Address(floID);return!regularOnly||addr.version==Bitcoin.Address.standardVersion}catch{return!1}},floCrypto.validateAddr=function(address,std=!0,bech=!0){let raw=decodeAddress(address);return!!raw&&(void 0!==raw.version?0!=std&&!!(!0===std||!Array.isArray(std)&&std===raw.version||Array.isArray(std)&&std.includes(raw.version)):void 0!==raw.bech_version?!1!==bech&&!!(!0===bech||!Array.isArray(bech)&&bech===raw.bech_version||Array.isArray(bech)&&bech.includes(raw.bech_version)):"ethereum"===raw.type)},floCrypto.verifyPubKey=function(pubKeyHex,address){let raw=decodeAddress(address);if(!raw)return;let pub_hash=Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex),{asBytes:!0})));return void 0!==raw.bech_version&&32==raw.bytes.length&&(raw.hex=Crypto.util.bytesToHex(ripemd160(raw.bytes,{asBytes:!0}))),pub_hash===raw.hex},floCrypto.toFloID=function(address,options=null){if(!address)return;let raw=decodeAddress(address);if(!raw)return;if(options){if(!(void 0===raw.version||options.std&&options.std.includes(raw.version)))return;if(!(void 0===raw.bech_version||options.bech&&options.bech.includes(raw.bech_version)))return}raw.bytes.unshift(bitjs.pub);let hash=Crypto.SHA256(Crypto.SHA256(raw.bytes,{asBytes:!0}),{asBytes:!0});return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0,4)))},floCrypto.rawToFloID=function(raw_bytes){if("string"==typeof raw_bytes&&(raw_bytes=Crypto.util.hexToBytes(raw_bytes)),20!=raw_bytes.length)return null;raw_bytes.unshift(bitjs.pub);let hash=Crypto.SHA256(Crypto.SHA256(raw_bytes,{asBytes:!0}),{asBytes:!0});return bitjs.Base58.encode(raw_bytes.concat(hash.slice(0,4)))},floCrypto.toMultisigFloID=function(address,options=null){if(!address)return;let raw=decodeAddress(address);if(!raw)return;if(options){if(!(void 0===raw.version||options.std&&options.std.includes(raw.version)))return;if(!(void 0===raw.bech_version||options.bech&&options.bech.includes(raw.bech_version)))return}if(void 0!==raw.bech_version){if(32!=raw.bytes.length)return;raw.bytes=ripemd160(raw.bytes,{asBytes:!0})}raw.bytes.unshift(bitjs.multisig);let hash=Crypto.SHA256(Crypto.SHA256(raw.bytes,{asBytes:!0}),{asBytes:!0});return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0,4)))},floCrypto.isSameAddr=function(addr1,addr2){if(!addr1||!addr2)return;let raw1=decodeAddress(addr1),raw2=decodeAddress(addr2);return!(!raw1||!raw2)&&(void 0!==raw1.bech_version&&32==raw1.bytes.length&&(raw1.hex=Crypto.util.bytesToHex(ripemd160(raw1.bytes,{asBytes:!0}))),void 0!==raw2.bech_version&&32==raw2.bytes.length&&(raw2.hex=Crypto.util.bytesToHex(ripemd160(raw2.bytes,{asBytes:!0}))),raw1.hex===raw2.hex)};const decodeAddress=floCrypto.decodeAddr=function(address){if(address){if(33==address.length||34==address.length){let decode=bitjs.Base58.decode(address),bytes=decode.slice(0,decode.length-4),checksum=decode.slice(decode.length-4),hash=Crypto.SHA256(Crypto.SHA256(bytes,{asBytes:!0}),{asBytes:!0});return hash[0]!=checksum[0]||hash[1]!=checksum[1]||hash[2]!=checksum[2]||hash[3]!=checksum[3]?null:{version:bytes.shift(),hex:Crypto.util.bytesToHex(bytes),bytes:bytes}}if(!address.startsWith("0x")&&42==address.length||62==address.length){let decode=coinjs.bech32_decode(address);if(decode){let bytes=decode.data,bech_version=bytes.shift();return bytes=coinjs.bech32_convert(bytes,5,8,!1),{bech_version:bech_version,hrp:decode.hrp,hex:Crypto.util.bytesToHex(bytes),bytes:bytes}}return null}return 42==address.length&&address.startsWith("0x")||40==address.length&&!address.startsWith("0x")?{hex:address,type:"ethereum"}:void 0}};floCrypto.createShamirsSecretShares=function(str,total_shares,threshold_limit){try{if(str.length>0){var strHex=shamirSecretShare.str2hex(str);return shamirSecretShare.share(strHex,total_shares,threshold_limit)}return!1}catch{return!1}};const retrieveShamirSecret=floCrypto.retrieveShamirSecret=function(sharesArray){try{if(sharesArray.length>0){var comb=shamirSecretShare.combine(sharesArray.slice(0,sharesArray.length));return comb=shamirSecretShare.hex2str(comb)}return!1}catch{return!1}};floCrypto.verifyShamirsSecret=function(sharesArray,str){return str?retrieveShamirSecret(sharesArray)===str:null};const validateASCII=floCrypto.validateASCII=function(string,bool=!0){if("string"!=typeof string)return null;if(bool){let x;for(let i=0;i127)return!1;return!0}{let x,invalids={};for(let i=0;i127)&&(x in invalids?invalids[string[i]].push(i):invalids[string[i]]=[i]);return!Object.keys(invalids).length||invalids}};floCrypto.convertToASCII=function(string,mode="soft-remove"){let chars=validateASCII(string,!1);if(!0===chars)return string;if(null===chars)return null;let convertor,result=string,refAlt={};if(ascii_alternatives.split("\n").forEach((a=>refAlt[a[0]]=a.slice(2))),"hard-unicode"===(mode=mode.toLowerCase()))convertor=c=>`\\u${("000"+c.charCodeAt().toString(16)).slice(-4)}`;else if("soft-unicode"===mode)convertor=c=>refAlt[c]||`\\u${("000"+c.charCodeAt().toString(16)).slice(-4)}`;else if("hard-remove"===mode)convertor=c=>"";else{if("soft-remove"!==mode)return null;convertor=c=>refAlt[c]||""}for(let c in chars)result=result.replaceAll(c,convertor(c));return result},floCrypto.revertUnicode=function(string){return string.replace(/\\u[\dA-F]{4}/gi,(m=>String.fromCharCode(parseInt(m.replace(/\\u/g,""),16))))}}(); \ No newline at end of file diff --git a/messenger/scripts/floDapps.js b/messenger/scripts/floDapps.js new file mode 100644 index 0000000..480e4e4 --- /dev/null +++ b/messenger/scripts/floDapps.js @@ -0,0 +1,843 @@ +(function (EXPORTS) { //floDapps v2.4.1 + /* General functions for FLO Dapps*/ + 'use strict'; + const floDapps = EXPORTS; + + const DEFAULT = { + root: "floDapps", + application: floGlobals.application, + adminID: floGlobals.adminID + }; + + Object.defineProperties(floDapps, { + application: { + get: () => DEFAULT.application + }, + adminID: { + get: () => DEFAULT.adminID + }, + root: { + get: () => DEFAULT.root + } + }); + + var user_priv_raw, aes_key, user_priv_wrap; //private variable inside capsule + const raw_user = { + get private() { + if (!user_priv_raw) + throw "User not logged in"; + return Crypto.AES.decrypt(user_priv_raw, aes_key); + } + } + + var user_id, user_public, user_private; + const user = floDapps.user = { + get id() { + if (!user_id) + throw "User not logged in"; + return user_id; + }, + get public() { + if (!user_public) + throw "User not logged in"; + return user_public; + }, + get private() { + if (!user_private) + throw "User not logged in"; + else if (user_private instanceof Function) + return user_private(); + else + return Crypto.AES.decrypt(user_private, aes_key); + }, + sign(message) { + return floCrypto.signData(message, raw_user.private); + }, + decrypt(data) { + return floCrypto.decryptData(data, raw_user.private); + }, + encipher(message) { + return Crypto.AES.encrypt(message, raw_user.private); + }, + decipher(data) { + return Crypto.AES.decrypt(data, raw_user.private); + }, + get db_name() { + return "floDapps#" + floCrypto.toFloID(user.id); + }, + lock() { + user_private = user_priv_wrap; + }, + async unlock() { + if (await user.private === raw_user.private) + user_private = user_priv_raw; + }, + get_contact(id) { + if (!user.contacts) + throw "Contacts not available"; + else if (user.contacts[id]) + return user.contacts[id]; + else { + let id_raw = floCrypto.decodeAddr(id).hex; + for (let i in user.contacts) + if (floCrypto.decodeAddr(i).hex == id_raw) + return user.contacts[i]; + } + }, + get_pubKey(id) { + if (!user.pubKeys) + throw "Contacts not available"; + else if (user.pubKeys[id]) + return user.pubKeys[id]; + else { + let id_raw = floCrypto.decodeAddr(id).hex; + for (let i in user.pubKeys) + if (floCrypto.decodeAddr(i).hex == id_raw) + return user.pubKeys[i]; + } + }, + clear() { + user_id = user_public = user_private = undefined; + user_priv_raw = aes_key = undefined; + delete user.contacts; + delete user.pubKeys; + delete user.messages; + } + }; + + Object.defineProperties(window, { + myFloID: { + get: () => { + try { + return user.id; + } catch { + return; + } + } + }, + myUserID: { + get: () => { + try { + return user.id; + } catch { + return; + } + } + }, + myPubKey: { + get: () => { + try { + return user.public; + } catch { + return; + } + } + }, + myPrivKey: { + get: () => { + try { + return user.private; + } catch { + return; + } + } + } + }); + + var subAdmins, trustedIDs, settings; + Object.defineProperties(floGlobals, { + subAdmins: { + get: () => subAdmins + }, + trustedIDs: { + get: () => trustedIDs + }, + settings: { + get: () => settings + }, + contacts: { + get: () => user.contacts + }, + pubKeys: { + get: () => user.pubKeys + }, + messages: { + get: () => user.messages + } + }) + + function initIndexedDB() { + return new Promise((resolve, reject) => { + var obs_g = { + //general + lastTx: {}, + //supernode (cloud list) + supernodes: {} + } + var obs_a = { + //login credentials + credentials: {}, + //for Dapps + subAdmins: {}, + trustedIDs: {}, + settings: {}, + appObjects: {}, + generalData: {}, + lastVC: {} + } + //add other given objectStores + initIndexedDB.appObs = initIndexedDB.appObs || {} + for (let o in initIndexedDB.appObs) + if (!(o in obs_a)) + obs_a[o] = initIndexedDB.appObs[o] + Promise.all([ + compactIDB.initDB(DEFAULT.application, obs_a), + compactIDB.initDB(DEFAULT.root, obs_g) + ]).then(result => { + compactIDB.setDefaultDB(DEFAULT.application) + resolve("IndexedDB App Storage Initated Successfully") + }).catch(error => reject(error)); + }) + } + + function initUserDB() { + return new Promise((resolve, reject) => { + var obs = { + contacts: {}, + pubKeys: {}, + messages: {} + } + compactIDB.initDB(user.db_name, obs).then(result => { + resolve("UserDB Initated Successfully") + }).catch(error => reject('Init userDB failed')); + }) + } + + function loadUserDB() { + return new Promise((resolve, reject) => { + var loadData = ["contacts", "pubKeys", "messages"] + var promises = [] + for (var i = 0; i < loadData.length; i++) + promises[i] = compactIDB.readAllData(loadData[i], user.db_name) + Promise.all(promises).then(results => { + for (var i = 0; i < loadData.length; i++) + user[loadData[i]] = results[i] + resolve("Loaded Data from userDB") + }).catch(error => reject('Load userDB failed')) + }) + } + + const startUpOptions = { + cloud: true, + app_config: true, + } + + floDapps.startUpOptions = { + set app_config(val) { + if (val === true || val === false) + startUpOptions.app_config = val; + }, + get app_config() { return startUpOptions.app_config }, + + set cloud(val) { + if (val === true || val === false) + startUpOptions.cloud = val; + }, + get cloud() { return startUpOptions.cloud }, + } + + const startUpFunctions = []; + + startUpFunctions.push(function readSupernodeListFromAPI() { + return new Promise((resolve, reject) => { + if (!startUpOptions.cloud) + return resolve("No cloud for this app"); + const CLOUD_KEY = "floCloudAPI#" + floCloudAPI.SNStorageID; + compactIDB.readData("lastTx", CLOUD_KEY, DEFAULT.root).then(lastTx => { + var query_options = { sentOnly: true, pattern: floCloudAPI.SNStorageName }; + if (typeof lastTx == 'number') //lastTx is tx count (*backward support) + query_options.ignoreOld = lastTx; + else if (typeof lastTx == 'string') //lastTx is txid of last tx + query_options.after = lastTx; + //fetch data from flosight + floBlockchainAPI.readData(floCloudAPI.SNStorageID, query_options).then(result => { + compactIDB.readData("supernodes", CLOUD_KEY, DEFAULT.root).then(nodes => { + nodes = nodes || {}; + for (var i = result.data.length - 1; i >= 0; i--) { + var content = JSON.parse(result.data[i])[floCloudAPI.SNStorageName]; + for (let sn in content.removeNodes) + delete nodes[sn]; + for (let sn in content.newNodes) + nodes[sn] = content.newNodes[sn]; + for (let sn in content.updateNodes) + if (sn in nodes) //check if node is listed + nodes[sn].uri = content.updateNodes[sn]; + } + Promise.all([ + compactIDB.writeData("lastTx", result.lastItem, CLOUD_KEY, DEFAULT.root), + compactIDB.writeData("supernodes", nodes, CLOUD_KEY, DEFAULT.root) + ]).then(_ => { + floCloudAPI.init(nodes) + .then(result => resolve("Loaded Supernode list\n" + result)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + }).catch(error => reject(error)) + }) + }); + + startUpFunctions.push(function readAppConfigFromAPI() { + return new Promise((resolve, reject) => { + if (!startUpOptions.app_config) + return resolve("No configs for this app"); + compactIDB.readData("lastTx", `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root).then(lastTx => { + var query_options = { sentOnly: true, pattern: DEFAULT.application }; + if (typeof lastTx == 'number') //lastTx is tx count (*backward support) + query_options.ignoreOld = lastTx; + else if (typeof lastTx == 'string') //lastTx is txid of last tx + query_options.after = lastTx; + //fetch data from flosight + floBlockchainAPI.readData(DEFAULT.adminID, query_options).then(result => { + for (var i = result.data.length - 1; i >= 0; i--) { + var content = JSON.parse(result.data[i])[DEFAULT.application]; + if (!content || typeof content !== "object") + continue; + if (Array.isArray(content.removeSubAdmin)) + for (var j = 0; j < content.removeSubAdmin.length; j++) + compactIDB.removeData("subAdmins", content.removeSubAdmin[j]); + if (Array.isArray(content.addSubAdmin)) + for (var k = 0; k < content.addSubAdmin.length; k++) + compactIDB.writeData("subAdmins", true, content.addSubAdmin[k]); + if (Array.isArray(content.removeTrustedID)) + for (var j = 0; j < content.removeTrustedID.length; j++) + compactIDB.removeData("trustedIDs", content.removeTrustedID[j]); + if (Array.isArray(content.addTrustedID)) + for (var k = 0; k < content.addTrustedID.length; k++) + compactIDB.writeData("trustedIDs", true, content.addTrustedID[k]); + if (content.settings) + for (let l in content.settings) + compactIDB.writeData("settings", content.settings[l], l) + } + compactIDB.writeData("lastTx", result.lastItem, `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root); + compactIDB.readAllData("subAdmins").then(result => { + subAdmins = Object.keys(result); + compactIDB.readAllData("trustedIDs").then(result => { + trustedIDs = Object.keys(result); + compactIDB.readAllData("settings").then(result => { + settings = result; + resolve("Read app configuration from blockchain"); + }) + }) + }) + }) + }).catch(error => reject(error)) + }) + }); + + startUpFunctions.push(function loadDataFromAppIDB() { + return new Promise((resolve, reject) => { + if (!startUpOptions.cloud) + return resolve("No cloud for this app"); + var loadData = ["appObjects", "generalData", "lastVC"] + var promises = [] + for (var i = 0; i < loadData.length; i++) + promises[i] = compactIDB.readAllData(loadData[i]) + Promise.all(promises).then(results => { + for (var i = 0; i < loadData.length; i++) + floGlobals[loadData[i]] = results[i] + resolve("Loaded Data from app IDB") + }).catch(error => reject(error)) + }) + }); + + var keyInput = type => new Promise((resolve, reject) => { + let inputVal = prompt(`Enter ${type}: `) + if (inputVal === null) + reject(null) + else + resolve(inputVal) + }); + + function getCredentials() { + + const readSharesFromIDB = indexArr => new Promise((resolve, reject) => { + var promises = [] + for (var i = 0; i < indexArr.length; i++) + promises.push(compactIDB.readData('credentials', indexArr[i])) + Promise.all(promises).then(shares => { + var secret = floCrypto.retrieveShamirSecret(shares) + if (secret) + resolve(secret) + else + reject("Shares are insufficient or incorrect") + }).catch(error => { + clearCredentials(); + location.reload(); + }) + }); + + const writeSharesToIDB = (shares, i = 0, resultIndexes = []) => new Promise(resolve => { + if (i >= shares.length) + return resolve(resultIndexes) + var n = floCrypto.randInt(0, 100000) + compactIDB.addData("credentials", shares[i], n).then(res => { + resultIndexes.push(n) + writeSharesToIDB(shares, i + 1, resultIndexes) + .then(result => resolve(result)) + }).catch(error => { + writeSharesToIDB(shares, i, resultIndexes) + .then(result => resolve(result)) + }) + }); + + const getPrivateKeyCredentials = () => new Promise((resolve, reject) => { + var indexArr = localStorage.getItem(`${DEFAULT.application}#privKey`) + if (indexArr) { + readSharesFromIDB(JSON.parse(indexArr)) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + var privKey; + keyInput("PRIVATE_KEY").then(result => { + if (!result) + return reject("Empty Private Key") + var floID = floCrypto.getFloID(result) + if (!floID || !floCrypto.validateFloID(floID)) + return reject("Invalid Private Key") + privKey = result; + }).catch(error => { + console.log(error, "Generating Random Keys") + privKey = floCrypto.generateNewID().privKey + }).finally(_ => { + if (!privKey) + return; + var threshold = floCrypto.randInt(10, 20) + var shares = floCrypto.createShamirsSecretShares(privKey, threshold, threshold) + writeSharesToIDB(shares).then(resultIndexes => { + //store index keys in localStorage + localStorage.setItem(`${DEFAULT.application}#privKey`, JSON.stringify(resultIndexes)) + //also add a dummy privatekey to the IDB + var randomPrivKey = floCrypto.generateNewID().privKey + var randomThreshold = floCrypto.randInt(10, 20) + var randomShares = floCrypto.createShamirsSecretShares(randomPrivKey, randomThreshold, randomThreshold) + writeSharesToIDB(randomShares) + //resolve private Key + resolve(privKey) + }) + }) + } + }); + + const checkIfPinRequired = key => new Promise((resolve, reject) => { + if (key.length == 52) + resolve(key) + else { + keyInput("PIN/Password").then(pwd => { + try { + let privKey = Crypto.AES.decrypt(key, pwd); + resolve(privKey) + } catch (error) { + reject("Access Denied: Incorrect PIN/Password") + } + }).catch(error => reject("Access Denied: PIN/Password required")) + } + }); + + return new Promise((resolve, reject) => { + getPrivateKeyCredentials().then(key => { + checkIfPinRequired(key).then(privKey => { + try { + user_public = floCrypto.getPubKeyHex(privKey); + user_id = floCrypto.getAddress(privKey); + if (startUpOptions.cloud) + floCloudAPI.user(user_id, privKey); //Set user for floCloudAPI + user_priv_wrap = () => checkIfPinRequired(key); + let n = floCrypto.randInt(12, 20); + aes_key = floCrypto.randString(n); + user_priv_raw = Crypto.AES.encrypt(privKey, aes_key); + user_private = user_priv_wrap; + resolve('Login Credentials loaded successful') + } catch (error) { + console.log(error) + reject("Corrupted Private Key") + } + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + var startUpLog = (status, log) => status ? console.log(log) : console.error(log); + + const callStartUpFunction = i => new Promise((resolve, reject) => { + startUpFunctions[i]().then(result => { + callStartUpFunction.completed += 1; + startUpLog(true, `${result}\nCompleted ${callStartUpFunction.completed}/${callStartUpFunction.total} Startup functions`) + resolve(true) + }).catch(error => { + callStartUpFunction.failed += 1; + startUpLog(false, `${error}\nFailed ${callStartUpFunction.failed}/${callStartUpFunction.total} Startup functions`) + reject(false) + }) + }); + + var _midFunction; + const midStartUp = () => new Promise((res, rej) => { + if (_midFunction instanceof Function) { + _midFunction() + .then(r => res("Mid startup function completed")) + .catch(e => rej("Mid startup function failed")) + } else + res("No mid startup function") + }); + + const callAndLog = p => new Promise((res, rej) => { + p.then(r => { + startUpLog(true, r) + res(r) + }).catch(e => { + startUpLog(false, e) + rej(e) + }) + }); + + floDapps.launchStartUp = function () { + return new Promise((resolve, reject) => { + initIndexedDB().then(log => { + console.log(log) + callStartUpFunction.total = startUpFunctions.length; + callStartUpFunction.completed = 0; + callStartUpFunction.failed = 0; + let p1 = new Promise((res, rej) => { + Promise.all(startUpFunctions.map((f, i) => callStartUpFunction(i))).then(r => { + callAndLog(midStartUp()) + .then(r => res(true)) + .catch(e => rej(false)) + }) + }); + let p2 = new Promise((res, rej) => { + callAndLog(getCredentials()).then(r => { + callAndLog(initUserDB()).then(r => { + callAndLog(loadUserDB()) + .then(r => res(true)) + .catch(e => rej(false)) + }).catch(e => rej(false)) + }).catch(e => rej(false)) + }) + Promise.all([p1, p2]) + .then(r => resolve('App Startup finished successful')) + .catch(e => reject('App Startup failed')) + }).catch(error => { + startUpLog(false, error); + reject("App database initiation failed") + }) + }) + } + + floDapps.addStartUpFunction = fn => fn instanceof Function && !startUpFunctions.includes(fn) ? startUpFunctions.push(fn) : false; + + floDapps.setMidStartup = fn => fn instanceof Function ? _midFunction = fn : false; + + floDapps.setCustomStartupLogger = fn => fn instanceof Function ? startUpLog = fn : false; + + floDapps.setCustomPrivKeyInput = fn => fn instanceof Function ? keyInput = fn : false; + + floDapps.setAppObjectStores = appObs => initIndexedDB.appObs = appObs; + + floDapps.storeContact = function (floID, name) { + return new Promise((resolve, reject) => { + if (!floCrypto.validateAddr(floID)) + return reject("Invalid floID!") + compactIDB.writeData("contacts", name, floID, user.db_name).then(result => { + user.contacts[floID] = name; + resolve("Contact stored") + }).catch(error => reject(error)) + }); + } + + floDapps.storePubKey = function (floID, pubKey) { + return new Promise((resolve, reject) => { + if (floID in user.pubKeys) + return resolve("pubKey already stored") + if (!floCrypto.validateAddr(floID)) + return reject("Invalid floID!") + if (!floCrypto.verifyPubKey(pubKey, floID)) + return reject("Incorrect pubKey") + compactIDB.writeData("pubKeys", pubKey, floID, user.db_name).then(result => { + user.pubKeys[floID] = pubKey; + resolve("pubKey stored") + }).catch(error => reject(error)) + }); + } + + floDapps.sendMessage = function (floID, message) { + return new Promise((resolve, reject) => { + let options = { + receiverID: floID, + application: DEFAULT.root, + comment: DEFAULT.application + } + if (floID in user.pubKeys) + message = floCrypto.encryptData(JSON.stringify(message), user.pubKeys[floID]) + floCloudAPI.sendApplicationData(message, "Message", options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + floDapps.requestInbox = function (callback) { + return new Promise((resolve, reject) => { + let lastVC = Object.keys(user.messages).sort().pop() + let options = { + receiverID: user.id, + application: DEFAULT.root, + lowerVectorClock: lastVC + 1 + } + let privKey = raw_user.private; + options.callback = (d, e) => { + for (let v in d) { + try { + if (d[v].message instanceof Object && "secret" in d[v].message) + d[v].message = floCrypto.decryptData(d[v].message, privKey) + } catch (error) { } + compactIDB.writeData("messages", d[v], v, user.db_name) + user.messages[v] = d[v] + } + if (callback instanceof Function) + callback(d, e) + } + floCloudAPI.requestApplicationData("Message", options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + floDapps.manageAppConfig = function (adminPrivKey, addList, rmList, settings) { + return new Promise((resolve, reject) => { + if (!startUpOptions.app_config) + return reject("No configs for this app"); + if (!Array.isArray(addList) || !addList.length) addList = undefined; + if (!Array.isArray(rmList) || !rmList.length) rmList = undefined; + if (!settings || typeof settings !== "object" || !Object.keys(settings).length) settings = undefined; + if (!addList && !rmList && !settings) + return reject("No configuration change") + var floData = { + [DEFAULT.application]: { + addSubAdmin: addList, + removeSubAdmin: rmList, + settings: settings + } + } + var floID = floCrypto.getFloID(adminPrivKey) + if (floID != DEFAULT.adminID) + reject('Access Denied for Admin privilege') + else + floBlockchainAPI.writeData(floID, JSON.stringify(floData), adminPrivKey) + .then(result => resolve(['Updated App Configuration', result])) + .catch(error => reject(error)) + }) + } + + floDapps.manageAppTrustedIDs = function (adminPrivKey, addList, rmList) { + return new Promise((resolve, reject) => { + if (!startUpOptions.app_config) + return reject("No configs for this app"); + if (!Array.isArray(addList) || !addList.length) addList = undefined; + if (!Array.isArray(rmList) || !rmList.length) rmList = undefined; + if (!addList && !rmList) + return reject("No change in list") + var floData = { + [DEFAULT.application]: { + addTrustedID: addList, + removeTrustedID: rmList + } + } + var floID = floCrypto.getFloID(adminPrivKey) + if (floID != DEFAULT.adminID) + reject('Access Denied for Admin privilege') + else + floBlockchainAPI.writeData(floID, JSON.stringify(floData), adminPrivKey) + .then(result => resolve(['Updated App Configuration', result])) + .catch(error => reject(error)) + }) + } + + const clearCredentials = floDapps.clearCredentials = function () { + return new Promise((resolve, reject) => { + compactIDB.clearData('credentials', DEFAULT.application).then(result => { + localStorage.removeItem(`${DEFAULT.application}#privKey`); + user.clear(); + resolve("privKey credentials deleted!") + }).catch(error => reject(error)) + }) + } + + floDapps.deleteUserData = function (credentials = false) { + return new Promise((resolve, reject) => { + let p = [] + p.push(compactIDB.deleteDB(user.db_name)) + if (credentials) + p.push(clearCredentials()) + Promise.all(p) + .then(result => resolve('User database(local) deleted')) + .catch(error => reject(error)) + }) + } + + floDapps.deleteAppData = function () { + return new Promise((resolve, reject) => { + compactIDB.deleteDB(DEFAULT.application).then(result => { + localStorage.removeItem(`${DEFAULT.application}#privKey`) + user.clear(); + compactIDB.removeData('lastTx', `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root) + .then(result => resolve("App database(local) deleted")) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + floDapps.securePrivKey = function (pwd) { + return new Promise(async (resolve, reject) => { + let indexArr = localStorage.getItem(`${DEFAULT.application}#privKey`) + if (!indexArr) + return reject("PrivKey not found"); + indexArr = JSON.parse(indexArr) + let encryptedKey = Crypto.AES.encrypt(await user.private, pwd); + let threshold = indexArr.length; + let shares = floCrypto.createShamirsSecretShares(encryptedKey, threshold, threshold) + let promises = []; + let overwriteFn = (share, index) => + compactIDB.writeData("credentials", share, index, DEFAULT.application); + for (var i = 0; i < threshold; i++) + promises.push(overwriteFn(shares[i], indexArr[i])); + Promise.all(promises) + .then(results => resolve("Private Key Secured")) + .catch(error => reject(error)) + }) + } + + floDapps.verifyPin = function (pin = null) { + const readSharesFromIDB = function (indexArr) { + return new Promise((resolve, reject) => { + var promises = [] + for (var i = 0; i < indexArr.length; i++) + promises.push(compactIDB.readData('credentials', indexArr[i])) + Promise.all(promises).then(shares => { + var secret = floCrypto.retrieveShamirSecret(shares) + console.info(shares, secret) + if (secret) + resolve(secret) + else + reject("Shares are insufficient or incorrect") + }).catch(error => { + clearCredentials(); + location.reload(); + }) + }) + } + return new Promise((resolve, reject) => { + var indexArr = localStorage.getItem(`${DEFAULT.application}#privKey`) + console.info(indexArr) + if (!indexArr) + reject('No login credentials found') + readSharesFromIDB(JSON.parse(indexArr)).then(key => { + if (key.length == 52) { + if (pin === null) + resolve("Private key not secured") + else + reject("Private key not secured") + } else { + if (pin === null) + return reject("PIN/Password required") + try { + let privKey = Crypto.AES.decrypt(key, pin); + resolve("PIN/Password verified") + } catch (error) { + reject("Incorrect PIN/Password") + } + } + }).catch(error => reject(error)) + }) + } + + const getNextGeneralData = floDapps.getNextGeneralData = function (type, vectorClock = null, options = {}) { + var fk = floCloudAPI.util.filterKey(type, options) + vectorClock = vectorClock || getNextGeneralData[fk] || '0'; + var filteredResult = {} + if (floGlobals.generalData[fk]) { + for (let d in floGlobals.generalData[fk]) + if (d > vectorClock) + filteredResult[d] = JSON.parse(JSON.stringify(floGlobals.generalData[fk][d])) + } else if (options.comment) { + let comment = options.comment; + delete options.comment; + let fk = floCloudAPI.util.filterKey(type, options); + for (let d in floGlobals.generalData[fk]) + if (d > vectorClock && floGlobals.generalData[fk][d].comment == comment) + filteredResult[d] = JSON.parse(JSON.stringify(floGlobals.generalData[fk][d])) + } + if (options.decrypt) { + let decryptionKey = (options.decrypt === true) ? raw_user.private : options.decrypt; + if (!Array.isArray(decryptionKey)) + decryptionKey = [decryptionKey]; + for (let f in filteredResult) { + let data = filteredResult[f] + try { + if (data.message instanceof Object && "secret" in data.message) { + for (let key of decryptionKey) { + try { + let tmp = floCrypto.decryptData(data.message, key) + data.message = JSON.parse(tmp) + break; + } catch (error) { } + } + } + } catch (error) { } + } + } + getNextGeneralData[fk] = Object.keys(filteredResult).sort().pop(); + return filteredResult; + } + + const syncData = floDapps.syncData = {}; + + syncData.oldDevice = () => new Promise((resolve, reject) => { + let sync = { + contacts: user.contacts, + pubKeys: user.pubKeys, + messages: user.messages + } + let message = Crypto.AES.encrypt(JSON.stringify(sync), raw_user.private) + let options = { + receiverID: user.id, + application: DEFAULT.root + } + floCloudAPI.sendApplicationData(message, "syncData", options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }); + + syncData.newDevice = () => new Promise((resolve, reject) => { + var options = { + receiverID: user.id, + senderID: user.id, + application: DEFAULT.root, + mostRecent: true, + } + floCloudAPI.requestApplicationData("syncData", options).then(response => { + let vc = Object.keys(response).sort().pop() + let sync = JSON.parse(Crypto.AES.decrypt(response[vc].message, raw_user.private)) + let promises = [] + let store = (key, val, obs) => promises.push(compactIDB.writeData(obs, val, key, user.db_name)); + ["contacts", "pubKeys", "messages"].forEach(c => { + for (let i in sync[c]) { + store(i, sync[c][i], c) + user[c][i] = sync[c][i] + } + }) + Promise.all(promises) + .then(results => resolve("Sync data successful")) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }); +})('object' === typeof module ? module.exports : window.floDapps = {}); \ No newline at end of file diff --git a/messenger/scripts/floTokenAPI.js b/messenger/scripts/floTokenAPI.js new file mode 100644 index 0000000..2456b88 --- /dev/null +++ b/messenger/scripts/floTokenAPI.js @@ -0,0 +1,166 @@ +(function (EXPORTS) { //floTokenAPI v1.0.4a + /* Token Operator to send/receive tokens via blockchain using API calls*/ + 'use strict'; + const tokenAPI = EXPORTS; + + const DEFAULT = { + apiURL: floGlobals.tokenURL || "https://ranchimallflo.duckdns.org/", + currency: floGlobals.currency || "rupee" + } + + Object.defineProperties(tokenAPI, { + URL: { + get: () => DEFAULT.apiURL + }, + currency: { + get: () => DEFAULT.currency, + set: currency => DEFAULT.currency = currency + } + }); + + if (floGlobals.currency) tokenAPI.currency = floGlobals.currency; + + Object.defineProperties(floGlobals, { + currency: { + get: () => DEFAULT.currency, + set: currency => DEFAULT.currency = currency + } + }); + + const fetch_api = tokenAPI.fetch = function (apicall) { + return new Promise((resolve, reject) => { + console.debug(DEFAULT.apiURL + apicall); + fetch(DEFAULT.apiURL + apicall).then(response => { + if (response.ok) + response.json().then(data => resolve(data)); + else + reject(response) + }).catch(error => reject(error)) + }) + } + + const getBalance = tokenAPI.getBalance = function (floID, token = DEFAULT.currency) { + return new Promise((resolve, reject) => { + fetch_api(`api/v1.0/getFloAddressBalance?token=${token}&floAddress=${floID}`) + .then(result => resolve(result.balance || 0)) + .catch(error => reject(error)) + }) + } + + tokenAPI.getTx = function (txID) { + return new Promise((resolve, reject) => { + fetch_api(`api/v1.0/getTransactionDetails/${txID}`).then(res => { + if (res.result === "error") + reject(res.description); + else if (!res.parsedFloData) + reject("Data piece (parsedFloData) missing"); + else if (!res.transactionDetails) + reject("Data piece (transactionDetails) missing"); + else + resolve(res); + }).catch(error => reject(error)) + }) + } + + tokenAPI.sendToken = function (privKey, amount, receiverID, message = "", token = DEFAULT.currency, options = {}) { + return new Promise((resolve, reject) => { + let senderID = floCrypto.getFloID(privKey); + if (typeof amount !== "number" || isNaN(amount) || amount <= 0) + return reject("Invalid amount"); + getBalance(senderID, token).then(bal => { + if (amount > bal) + return reject(`Insufficient ${token}# balance`); + floBlockchainAPI.writeData(senderID, `send ${amount} ${token}# ${message}`, privKey, receiverID, options) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }); + } + + function sendTokens_raw(privKey, receiverID, token, amount, utxo, vout, scriptPubKey) { + return new Promise((resolve, reject) => { + var trx = bitjs.transaction(); + trx.addinput(utxo, vout, scriptPubKey) + trx.addoutput(receiverID, floBlockchainAPI.sendAmt); + trx.addflodata(`send ${amount} ${token}#`); + var signedTxHash = trx.sign(privKey, 1); + floBlockchainAPI.broadcastTx(signedTxHash) + .then(txid => resolve([receiverID, txid])) + .catch(error => reject([receiverID, error])) + }) + } + + //bulk transfer tokens + tokenAPI.bulkTransferTokens = function (sender, privKey, token, receivers) { + return new Promise((resolve, reject) => { + if (typeof receivers !== 'object') + return reject("receivers must be object in format {receiver1: amount1, receiver2:amount2...}") + + let receiver_list = Object.keys(receivers), amount_list = Object.values(receivers); + let invalidReceivers = receiver_list.filter(id => !floCrypto.validateFloID(id)); + let invalidAmount = amount_list.filter(val => typeof val !== 'number' || val <= 0); + if (invalidReceivers.length) + return reject(`Invalid receivers: ${invalidReceivers}`); + else if (invalidAmount.length) + return reject(`Invalid amounts: ${invalidAmount}`); + + if (receiver_list.length == 0) + return reject("Receivers cannot be empty"); + + if (receiver_list.length == 1) { + let receiver = receiver_list[0], amount = amount_list[0]; + floTokenAPI.sendToken(privKey, amount, receiver, "", token) + .then(txid => resolve({ success: { [receiver]: txid } })) + .catch(error => reject(error)) + } else { + //check for token balance + floTokenAPI.getBalance(sender, token).then(token_balance => { + let total_token_amout = amount_list.reduce((a, e) => a + e, 0); + if (total_token_amout > token_balance) + return reject(`Insufficient ${token}# balance`); + + //split utxos + floBlockchainAPI.splitUTXOs(sender, privKey, receiver_list.length).then(split_txid => { + //wait for the split utxo to get confirmation + floBlockchainAPI.waitForConfirmation(split_txid).then(split_tx => { + //send tokens using the split-utxo + var scriptPubKey = split_tx.vout[0].scriptPubKey.hex; + let promises = []; + for (let i in receiver_list) + promises.push(sendTokens_raw(privKey, receiver_list[i], token, amount_list[i], split_txid, i, scriptPubKey)); + Promise.allSettled(promises).then(results => { + let success = Object.fromEntries(results.filter(r => r.status == 'fulfilled').map(r => r.value)); + let failed = Object.fromEntries(results.filter(r => r.status == 'rejected').map(r => r.reason)); + resolve({ success, failed }); + }) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + } + + }) + } + + tokenAPI.getAllTxs = function (floID, token = DEFAULT.currency) { + return new Promise((resolve, reject) => { + fetch_api(`api/v1.0/getFloAddressTransactions?token=${token}&floAddress=${floID}`) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + const util = tokenAPI.util = {}; + + util.parseTxData = function (txData) { + let parsedData = {}; + for (let p in txData.parsedFloData) + parsedData[p] = txData.parsedFloData[p]; + parsedData.sender = txData.transactionDetails.vin[0].addr; + for (let vout of txData.transactionDetails.vout) + if (vout.scriptPubKey.addresses[0] !== parsedData.sender) + parsedData.receiver = vout.scriptPubKey.addresses[0]; + parsedData.time = txData.transactionDetails.time; + return parsedData; + } + +})('object' === typeof module ? module.exports : window.floTokenAPI = {}); \ No newline at end of file diff --git a/messenger/scripts/keccak.js b/messenger/scripts/keccak.js new file mode 100644 index 0000000..203bd38 --- /dev/null +++ b/messenger/scripts/keccak.js @@ -0,0 +1,673 @@ +(function () { + 'use strict'; + + var INPUT_ERROR = 'input is invalid type'; + var FINALIZE_ERROR = 'finalize already called'; + var WINDOW = typeof window === 'object'; + var root = WINDOW ? (window.keccak = window.keccak || {}) : {}; + if (root.JS_SHA3_NO_WINDOW) { + WINDOW = false; + } + var WEB_WORKER = !WINDOW && typeof self === 'object'; + var NODE_JS = !root.JS_SHA3_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; + if (NODE_JS) { + root = global; + } else if (WEB_WORKER) { + root = self; + } + var COMMON_JS = !root.JS_SHA3_NO_COMMON_JS && typeof module === 'object' && module.exports; + var AMD = typeof define === 'function' && define.amd; + var ARRAY_BUFFER = !root.JS_SHA3_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; + var HEX_CHARS = '0123456789abcdef'.split(''); + var SHAKE_PADDING = [31, 7936, 2031616, 520093696]; + var CSHAKE_PADDING = [4, 1024, 262144, 67108864]; + var KECCAK_PADDING = [1, 256, 65536, 16777216]; + var PADDING = [6, 1536, 393216, 100663296]; + var SHIFT = [0, 8, 16, 24]; + var RC = [1, 0, 32898, 0, 32906, 2147483648, 2147516416, 2147483648, 32907, 0, 2147483649, + 0, 2147516545, 2147483648, 32777, 2147483648, 138, 0, 136, 0, 2147516425, 0, + 2147483658, 0, 2147516555, 0, 139, 2147483648, 32905, 2147483648, 32771, + 2147483648, 32770, 2147483648, 128, 2147483648, 32778, 0, 2147483658, 2147483648, + 2147516545, 2147483648, 32896, 2147483648, 2147483649, 0, 2147516424, 2147483648]; + var BITS = [224, 256, 384, 512]; + var SHAKE_BITS = [128, 256]; + var OUTPUT_TYPES = ['hex', 'buffer', 'arrayBuffer', 'array', 'digest']; + var CSHAKE_BYTEPAD = { + '128': 168, + '256': 136 + }; + + + var isArray = root.JS_SHA3_NO_NODE_JS || !Array.isArray + ? function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + } + : Array.isArray; + + var isView = (ARRAY_BUFFER && (root.JS_SHA3_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) + ? function (obj) { + return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; + } + : ArrayBuffer.isView; + + // [message: string, isString: bool] + var formatMessage = function (message) { + var type = typeof message; + if (type === 'string') { + return [message, true]; + } + if (type !== 'object' || message === null) { + throw new Error(INPUT_ERROR); + } + if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { + return [new Uint8Array(message), false]; + } + if (!isArray(message) && !isView(message)) { + throw new Error(INPUT_ERROR); + } + return [message, false]; + } + + var empty = function (message) { + return formatMessage(message)[0].length === 0; + }; + + var createOutputMethod = function (bits, padding, outputType) { + return function (message) { + return new Keccak(bits, padding, bits).update(message)[outputType](); + }; + }; + + var createShakeOutputMethod = function (bits, padding, outputType) { + return function (message, outputBits) { + return new Keccak(bits, padding, outputBits).update(message)[outputType](); + }; + }; + + var createCshakeOutputMethod = function (bits, padding, outputType) { + return function (message, outputBits, n, s) { + return methods['cshake' + bits].update(message, outputBits, n, s)[outputType](); + }; + }; + + var createKmacOutputMethod = function (bits, padding, outputType) { + return function (key, message, outputBits, s) { + return methods['kmac' + bits].update(key, message, outputBits, s)[outputType](); + }; + }; + + var createOutputMethods = function (method, createMethod, bits, padding) { + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createMethod(bits, padding, type); + } + return method; + }; + + var createMethod = function (bits, padding) { + var method = createOutputMethod(bits, padding, 'hex'); + method.create = function () { + return new Keccak(bits, padding, bits); + }; + method.update = function (message) { + return method.create().update(message); + }; + return createOutputMethods(method, createOutputMethod, bits, padding); + }; + + var createShakeMethod = function (bits, padding) { + var method = createShakeOutputMethod(bits, padding, 'hex'); + method.create = function (outputBits) { + return new Keccak(bits, padding, outputBits); + }; + method.update = function (message, outputBits) { + return method.create(outputBits).update(message); + }; + return createOutputMethods(method, createShakeOutputMethod, bits, padding); + }; + + var createCshakeMethod = function (bits, padding) { + var w = CSHAKE_BYTEPAD[bits]; + var method = createCshakeOutputMethod(bits, padding, 'hex'); + method.create = function (outputBits, n, s) { + if (empty(n) && empty(s)) { + return methods['shake' + bits].create(outputBits); + } else { + return new Keccak(bits, padding, outputBits).bytepad([n, s], w); + } + }; + method.update = function (message, outputBits, n, s) { + return method.create(outputBits, n, s).update(message); + }; + return createOutputMethods(method, createCshakeOutputMethod, bits, padding); + }; + + var createKmacMethod = function (bits, padding) { + var w = CSHAKE_BYTEPAD[bits]; + var method = createKmacOutputMethod(bits, padding, 'hex'); + method.create = function (key, outputBits, s) { + return new Kmac(bits, padding, outputBits).bytepad(['KMAC', s], w).bytepad([key], w); + }; + method.update = function (key, message, outputBits, s) { + return method.create(key, outputBits, s).update(message); + }; + return createOutputMethods(method, createKmacOutputMethod, bits, padding); + }; + + var algorithms = [ + { name: 'keccak', padding: KECCAK_PADDING, bits: BITS, createMethod: createMethod }, + { name: 'sha3', padding: PADDING, bits: BITS, createMethod: createMethod }, + { name: 'shake', padding: SHAKE_PADDING, bits: SHAKE_BITS, createMethod: createShakeMethod }, + { name: 'cshake', padding: CSHAKE_PADDING, bits: SHAKE_BITS, createMethod: createCshakeMethod }, + { name: 'kmac', padding: CSHAKE_PADDING, bits: SHAKE_BITS, createMethod: createKmacMethod } + ]; + + var methods = {}, methodNames = []; + + for (var i = 0; i < algorithms.length; ++i) { + var algorithm = algorithms[i]; + var bits = algorithm.bits; + for (var j = 0; j < bits.length; ++j) { + var methodName = algorithm.name + '_' + bits[j]; + methodNames.push(methodName); + methods[methodName] = algorithm.createMethod(bits[j], algorithm.padding); + if (algorithm.name !== 'sha3') { + var newMethodName = algorithm.name + bits[j]; + methodNames.push(newMethodName); + methods[newMethodName] = methods[methodName]; + } + } + } + +methodNames.push("extractLast20Bytes"); +methods["extractLast20Bytes"] = extractLast20Bytes; + + + function Keccak(bits, padding, outputBits) { + this.blocks = []; + this.s = []; + this.padding = padding; + this.outputBits = outputBits; + this.reset = true; + this.finalized = false; + this.block = 0; + this.start = 0; + this.blockCount = (1600 - (bits << 1)) >> 5; + this.byteCount = this.blockCount << 2; + this.outputBlocks = outputBits >> 5; + this.extraBytes = (outputBits & 31) >> 3; + + for (var i = 0; i < 50; ++i) { + this.s[i] = 0; + } + } + + Keccak.prototype.update = function (message) { + if (this.finalized) { + throw new Error(FINALIZE_ERROR); + } + var result = formatMessage(message); + message = result[0]; + var isString = result[1]; + var blocks = this.blocks, byteCount = this.byteCount, length = message.length, + blockCount = this.blockCount, index = 0, s = this.s, i, code; + + while (index < length) { + if (this.reset) { + this.reset = false; + blocks[0] = this.block; + for (i = 1; i < blockCount + 1; ++i) { + blocks[i] = 0; + } + } + if (isString) { + for (i = this.start; index < length && i < byteCount; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } + } else { + for (i = this.start; index < length && i < byteCount; ++index) { + blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; + } + } + this.lastByteIndex = i; + if (i >= byteCount) { + this.start = i - byteCount; + this.block = blocks[blockCount]; + for (i = 0; i < blockCount; ++i) { + s[i] ^= blocks[i]; + } + f(s); + this.reset = true; + } else { + this.start = i; + } + } + return this; + }; + + Keccak.prototype.encode = function (x, right) { + var o = x & 255, n = 1; + var bytes = [o]; + x = x >> 8; + o = x & 255; + while (o > 0) { + bytes.unshift(o); + x = x >> 8; + o = x & 255; + ++n; + } + if (right) { + bytes.push(n); + } else { + bytes.unshift(n); + } + this.update(bytes); + return bytes.length; + }; + + Keccak.prototype.encodeString = function (str) { + var result = formatMessage(str); + str = result[0]; + var isString = result[1]; + var bytes = 0, length = str.length; + if (isString) { + for (var i = 0; i < str.length; ++i) { + var code = str.charCodeAt(i); + if (code < 0x80) { + bytes += 1; + } else if (code < 0x800) { + bytes += 2; + } else if (code < 0xd800 || code >= 0xe000) { + bytes += 3; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (str.charCodeAt(++i) & 0x3ff)); + bytes += 4; + } + } + } else { + bytes = length; + } + bytes += this.encode(bytes * 8); + this.update(str); + return bytes; + }; + + Keccak.prototype.bytepad = function (strs, w) { + var bytes = this.encode(w); + for (var i = 0; i < strs.length; ++i) { + bytes += this.encodeString(strs[i]); + } + var paddingBytes = (w - bytes % w) % w; + var zeros = []; + zeros.length = paddingBytes; + this.update(zeros); + return this; + }; + + Keccak.prototype.finalize = function () { + if (this.finalized) { + return; + } + this.finalized = true; + var blocks = this.blocks, i = this.lastByteIndex, blockCount = this.blockCount, s = this.s; + blocks[i >> 2] |= this.padding[i & 3]; + if (this.lastByteIndex === this.byteCount) { + blocks[0] = blocks[blockCount]; + for (i = 1; i < blockCount + 1; ++i) { + blocks[i] = 0; + } + } + blocks[blockCount - 1] |= 0x80000000; + for (i = 0; i < blockCount; ++i) { + s[i] ^= blocks[i]; + } + f(s); + }; + + Keccak.prototype.toString = Keccak.prototype.hex = function () { + this.finalize(); + + var blockCount = this.blockCount, s = this.s, outputBlocks = this.outputBlocks, + extraBytes = this.extraBytes, i = 0, j = 0; + var hex = '', block; + while (j < outputBlocks) { + for (i = 0; i < blockCount && j < outputBlocks; ++i, ++j) { + block = s[i]; + hex += HEX_CHARS[(block >> 4) & 0x0F] + HEX_CHARS[block & 0x0F] + + HEX_CHARS[(block >> 12) & 0x0F] + HEX_CHARS[(block >> 8) & 0x0F] + + HEX_CHARS[(block >> 20) & 0x0F] + HEX_CHARS[(block >> 16) & 0x0F] + + HEX_CHARS[(block >> 28) & 0x0F] + HEX_CHARS[(block >> 24) & 0x0F]; + } + if (j % blockCount === 0) { + f(s); + i = 0; + } + } + if (extraBytes) { + block = s[i]; + hex += HEX_CHARS[(block >> 4) & 0x0F] + HEX_CHARS[block & 0x0F]; + if (extraBytes > 1) { + hex += HEX_CHARS[(block >> 12) & 0x0F] + HEX_CHARS[(block >> 8) & 0x0F]; + } + if (extraBytes > 2) { + hex += HEX_CHARS[(block >> 20) & 0x0F] + HEX_CHARS[(block >> 16) & 0x0F]; + } + } + return hex; + }; + + Keccak.prototype.arrayBuffer = function () { + this.finalize(); + + var blockCount = this.blockCount, s = this.s, outputBlocks = this.outputBlocks, + extraBytes = this.extraBytes, i = 0, j = 0; + var bytes = this.outputBits >> 3; + var buffer; + if (extraBytes) { + buffer = new ArrayBuffer((outputBlocks + 1) << 2); + } else { + buffer = new ArrayBuffer(bytes); + } + var array = new Uint32Array(buffer); + while (j < outputBlocks) { + for (i = 0; i < blockCount && j < outputBlocks; ++i, ++j) { + array[j] = s[i]; + } + if (j % blockCount === 0) { + f(s); + } + } + if (extraBytes) { + array[i] = s[i]; + buffer = buffer.slice(0, bytes); + } + return buffer; + }; + + Keccak.prototype.buffer = Keccak.prototype.arrayBuffer; + + Keccak.prototype.digest = Keccak.prototype.array = function () { + this.finalize(); + + var blockCount = this.blockCount, s = this.s, outputBlocks = this.outputBlocks, + extraBytes = this.extraBytes, i = 0, j = 0; + var array = [], offset, block; + while (j < outputBlocks) { + for (i = 0; i < blockCount && j < outputBlocks; ++i, ++j) { + offset = j << 2; + block = s[i]; + array[offset] = block & 0xFF; + array[offset + 1] = (block >> 8) & 0xFF; + array[offset + 2] = (block >> 16) & 0xFF; + array[offset + 3] = (block >> 24) & 0xFF; + } + if (j % blockCount === 0) { + f(s); + } + } + if (extraBytes) { + offset = j << 2; + block = s[i]; + array[offset] = block & 0xFF; + if (extraBytes > 1) { + array[offset + 1] = (block >> 8) & 0xFF; + } + if (extraBytes > 2) { + array[offset + 2] = (block >> 16) & 0xFF; + } + } + return array; + }; + + function Kmac(bits, padding, outputBits) { + Keccak.call(this, bits, padding, outputBits); + } + + Kmac.prototype = new Keccak(); + + Kmac.prototype.finalize = function () { + this.encode(this.outputBits, true); + return Keccak.prototype.finalize.call(this); + }; + + var f = function (s) { + var h, l, n, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, + b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, + b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31, b32, b33, + b34, b35, b36, b37, b38, b39, b40, b41, b42, b43, b44, b45, b46, b47, b48, b49; + for (n = 0; n < 48; n += 2) { + c0 = s[0] ^ s[10] ^ s[20] ^ s[30] ^ s[40]; + c1 = s[1] ^ s[11] ^ s[21] ^ s[31] ^ s[41]; + c2 = s[2] ^ s[12] ^ s[22] ^ s[32] ^ s[42]; + c3 = s[3] ^ s[13] ^ s[23] ^ s[33] ^ s[43]; + c4 = s[4] ^ s[14] ^ s[24] ^ s[34] ^ s[44]; + c5 = s[5] ^ s[15] ^ s[25] ^ s[35] ^ s[45]; + c6 = s[6] ^ s[16] ^ s[26] ^ s[36] ^ s[46]; + c7 = s[7] ^ s[17] ^ s[27] ^ s[37] ^ s[47]; + c8 = s[8] ^ s[18] ^ s[28] ^ s[38] ^ s[48]; + c9 = s[9] ^ s[19] ^ s[29] ^ s[39] ^ s[49]; + + h = c8 ^ ((c2 << 1) | (c3 >>> 31)); + l = c9 ^ ((c3 << 1) | (c2 >>> 31)); + s[0] ^= h; + s[1] ^= l; + s[10] ^= h; + s[11] ^= l; + s[20] ^= h; + s[21] ^= l; + s[30] ^= h; + s[31] ^= l; + s[40] ^= h; + s[41] ^= l; + h = c0 ^ ((c4 << 1) | (c5 >>> 31)); + l = c1 ^ ((c5 << 1) | (c4 >>> 31)); + s[2] ^= h; + s[3] ^= l; + s[12] ^= h; + s[13] ^= l; + s[22] ^= h; + s[23] ^= l; + s[32] ^= h; + s[33] ^= l; + s[42] ^= h; + s[43] ^= l; + h = c2 ^ ((c6 << 1) | (c7 >>> 31)); + l = c3 ^ ((c7 << 1) | (c6 >>> 31)); + s[4] ^= h; + s[5] ^= l; + s[14] ^= h; + s[15] ^= l; + s[24] ^= h; + s[25] ^= l; + s[34] ^= h; + s[35] ^= l; + s[44] ^= h; + s[45] ^= l; + h = c4 ^ ((c8 << 1) | (c9 >>> 31)); + l = c5 ^ ((c9 << 1) | (c8 >>> 31)); + s[6] ^= h; + s[7] ^= l; + s[16] ^= h; + s[17] ^= l; + s[26] ^= h; + s[27] ^= l; + s[36] ^= h; + s[37] ^= l; + s[46] ^= h; + s[47] ^= l; + h = c6 ^ ((c0 << 1) | (c1 >>> 31)); + l = c7 ^ ((c1 << 1) | (c0 >>> 31)); + s[8] ^= h; + s[9] ^= l; + s[18] ^= h; + s[19] ^= l; + s[28] ^= h; + s[29] ^= l; + s[38] ^= h; + s[39] ^= l; + s[48] ^= h; + s[49] ^= l; + + b0 = s[0]; + b1 = s[1]; + b32 = (s[11] << 4) | (s[10] >>> 28); + b33 = (s[10] << 4) | (s[11] >>> 28); + b14 = (s[20] << 3) | (s[21] >>> 29); + b15 = (s[21] << 3) | (s[20] >>> 29); + b46 = (s[31] << 9) | (s[30] >>> 23); + b47 = (s[30] << 9) | (s[31] >>> 23); + b28 = (s[40] << 18) | (s[41] >>> 14); + b29 = (s[41] << 18) | (s[40] >>> 14); + b20 = (s[2] << 1) | (s[3] >>> 31); + b21 = (s[3] << 1) | (s[2] >>> 31); + b2 = (s[13] << 12) | (s[12] >>> 20); + b3 = (s[12] << 12) | (s[13] >>> 20); + b34 = (s[22] << 10) | (s[23] >>> 22); + b35 = (s[23] << 10) | (s[22] >>> 22); + b16 = (s[33] << 13) | (s[32] >>> 19); + b17 = (s[32] << 13) | (s[33] >>> 19); + b48 = (s[42] << 2) | (s[43] >>> 30); + b49 = (s[43] << 2) | (s[42] >>> 30); + b40 = (s[5] << 30) | (s[4] >>> 2); + b41 = (s[4] << 30) | (s[5] >>> 2); + b22 = (s[14] << 6) | (s[15] >>> 26); + b23 = (s[15] << 6) | (s[14] >>> 26); + b4 = (s[25] << 11) | (s[24] >>> 21); + b5 = (s[24] << 11) | (s[25] >>> 21); + b36 = (s[34] << 15) | (s[35] >>> 17); + b37 = (s[35] << 15) | (s[34] >>> 17); + b18 = (s[45] << 29) | (s[44] >>> 3); + b19 = (s[44] << 29) | (s[45] >>> 3); + b10 = (s[6] << 28) | (s[7] >>> 4); + b11 = (s[7] << 28) | (s[6] >>> 4); + b42 = (s[17] << 23) | (s[16] >>> 9); + b43 = (s[16] << 23) | (s[17] >>> 9); + b24 = (s[26] << 25) | (s[27] >>> 7); + b25 = (s[27] << 25) | (s[26] >>> 7); + b6 = (s[36] << 21) | (s[37] >>> 11); + b7 = (s[37] << 21) | (s[36] >>> 11); + b38 = (s[47] << 24) | (s[46] >>> 8); + b39 = (s[46] << 24) | (s[47] >>> 8); + b30 = (s[8] << 27) | (s[9] >>> 5); + b31 = (s[9] << 27) | (s[8] >>> 5); + b12 = (s[18] << 20) | (s[19] >>> 12); + b13 = (s[19] << 20) | (s[18] >>> 12); + b44 = (s[29] << 7) | (s[28] >>> 25); + b45 = (s[28] << 7) | (s[29] >>> 25); + b26 = (s[38] << 8) | (s[39] >>> 24); + b27 = (s[39] << 8) | (s[38] >>> 24); + b8 = (s[48] << 14) | (s[49] >>> 18); + b9 = (s[49] << 14) | (s[48] >>> 18); + + s[0] = b0 ^ (~b2 & b4); + s[1] = b1 ^ (~b3 & b5); + s[10] = b10 ^ (~b12 & b14); + s[11] = b11 ^ (~b13 & b15); + s[20] = b20 ^ (~b22 & b24); + s[21] = b21 ^ (~b23 & b25); + s[30] = b30 ^ (~b32 & b34); + s[31] = b31 ^ (~b33 & b35); + s[40] = b40 ^ (~b42 & b44); + s[41] = b41 ^ (~b43 & b45); + s[2] = b2 ^ (~b4 & b6); + s[3] = b3 ^ (~b5 & b7); + s[12] = b12 ^ (~b14 & b16); + s[13] = b13 ^ (~b15 & b17); + s[22] = b22 ^ (~b24 & b26); + s[23] = b23 ^ (~b25 & b27); + s[32] = b32 ^ (~b34 & b36); + s[33] = b33 ^ (~b35 & b37); + s[42] = b42 ^ (~b44 & b46); + s[43] = b43 ^ (~b45 & b47); + s[4] = b4 ^ (~b6 & b8); + s[5] = b5 ^ (~b7 & b9); + s[14] = b14 ^ (~b16 & b18); + s[15] = b15 ^ (~b17 & b19); + s[24] = b24 ^ (~b26 & b28); + s[25] = b25 ^ (~b27 & b29); + s[34] = b34 ^ (~b36 & b38); + s[35] = b35 ^ (~b37 & b39); + s[44] = b44 ^ (~b46 & b48); + s[45] = b45 ^ (~b47 & b49); + s[6] = b6 ^ (~b8 & b0); + s[7] = b7 ^ (~b9 & b1); + s[16] = b16 ^ (~b18 & b10); + s[17] = b17 ^ (~b19 & b11); + s[26] = b26 ^ (~b28 & b20); + s[27] = b27 ^ (~b29 & b21); + s[36] = b36 ^ (~b38 & b30); + s[37] = b37 ^ (~b39 & b31); + s[46] = b46 ^ (~b48 & b40); + s[47] = b47 ^ (~b49 & b41); + s[8] = b8 ^ (~b0 & b2); + s[9] = b9 ^ (~b1 & b3); + s[18] = b18 ^ (~b10 & b12); + s[19] = b19 ^ (~b11 & b13); + s[28] = b28 ^ (~b20 & b22); + s[29] = b29 ^ (~b21 & b23); + s[38] = b38 ^ (~b30 & b32); + s[39] = b39 ^ (~b31 & b33); + s[48] = b48 ^ (~b40 & b42); + s[49] = b49 ^ (~b41 & b43); + + s[0] ^= RC[n]; + s[1] ^= RC[n + 1]; + } + }; + +function extractLast20Bytes(hexString, addPrefix) { + // Ensure the input hexString has '0x' prefix + if (!hexString.startsWith('0x')) { + hexString = '0x' + hexString; + } + + // Remove '0x' prefix and parse the hex string to a BigInt + var bigIntValue = BigInt(hexString); + + // Extract the last 20 bytes (160 bits) from the BigInt + var last20Bytes = bigIntValue & BigInt('0x' + 'f'.repeat(40)); // 0xf is 4 bits in hexadecimal, repeated 40 times for 160 bits + + // Convert the result back to a hexadecimal string + var result = last20Bytes.toString(16).padStart(40, '0'); // 40 characters for 160 bits + + // Add '0x' prefix if addPrefix is truthy + if (addPrefix) { + result = '0x' + result; + } + + return result; +} + + if (typeof root.keccak === 'object') { + Object.assign(root.keccak, methods); + } + + if (COMMON_JS) { + module.exports = methods; + } else { + for (i = 0; i < methodNames.length; ++i) { + root[methodNames[i]] = methods[methodNames[i]]; + } + if (AMD) { + define(function () { + return methods; + }); + } + } +})(); diff --git a/messenger/scripts/lib.js b/messenger/scripts/lib.js new file mode 100644 index 0000000..e383403 --- /dev/null +++ b/messenger/scripts/lib.js @@ -0,0 +1,9975 @@ +(function (GLOBAL) { //lib v1.4.2b + 'use strict'; + /* Utility Libraries required for Standard operations + * All credits for these codes belong to their respective creators, moderators and owners. + * For more info (including license and terms of use), please visit respective source. + */ + GLOBAL.cryptocoin = (typeof floGlobals === 'undefined' ? null : floGlobals.blockchain) || 'FLO'; + + const getRandomBytes = (function () { + if (typeof require === 'function') { + const crypto = require('crypto'); + return function (buf) { + var bytes = crypto.randomBytes(buf.length); + buf.set(bytes); + return buf; + } + } else if (GLOBAL.crypto && GLOBAL.crypto.getRandomValues) { + return function (buf) { + return GLOBAL.crypto.getRandomValues(buf); + } + } else + throw Error('Unable to define getRandomBytes'); + })(); + + + GLOBAL.securedMathRandom = (function () { + if (typeof require === 'function') { + const crypto = require('crypto'); + return function () { + return crypto.randomBytes(4).readUInt32LE() / 0xffffffff; + } + } else if (GLOBAL.crypto && GLOBAL.crypto.getRandomValues) { + return function () { + return (GLOBAL.crypto.getRandomValues(new Uint32Array(1))[0] / 0xffffffff); + } + } else + throw Error('Unable to define securedMathRandom'); + })(); + + //Crypto.js + (function () { + // Global Crypto object + var Crypto = GLOBAL.Crypto = {}; + /*! + * Crypto-JS v2.5.4 Crypto.js + * http://code.google.com/p/crypto-js/ + * Copyright (c) 2009-2013, Jeff Mott. All rights reserved. + * http://code.google.com/p/crypto-js/wiki/License + */ + (function () { + + var base64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + // Crypto utilities + var util = Crypto.util = { + + // Bit-wise rotate left + rotl: function (n, b) { + return (n << b) | (n >>> (32 - b)); + }, + + // Bit-wise rotate right + rotr: function (n, b) { + return (n << (32 - b)) | (n >>> b); + }, + + // Swap big-endian to little-endian and vice versa + endian: function (n) { + + // If number given, swap endian + if (n.constructor == Number) { + return util.rotl(n, 8) & 0x00FF00FF | + util.rotl(n, 24) & 0xFF00FF00; + } + + // Else, assume array and swap all items + for (var i = 0; i < n.length; i++) + n[i] = util.endian(n[i]); + return n; + + }, + + // Generate an array of any length of random bytes + randomBytes: function (n) { + for (var bytes = []; n > 0; n--) + bytes.push(Math.floor(securedMathRandom() * 256)); + return bytes; + }, + + // Convert a byte array to big-endian 32-bit words + bytesToWords: function (bytes) { + for (var words = [], i = 0, b = 0; i < bytes.length; i++, b += 8) + words[b >>> 5] |= (bytes[i] & 0xFF) << (24 - b % 32); + return words; + }, + + // Convert big-endian 32-bit words to a byte array + wordsToBytes: function (words) { + for (var bytes = [], b = 0; b < words.length * 32; b += 8) + bytes.push((words[b >>> 5] >>> (24 - b % 32)) & 0xFF); + return bytes; + }, + + // Convert a byte array to a hex string + bytesToHex: function (bytes) { + for (var hex = [], i = 0; i < bytes.length; i++) { + hex.push((bytes[i] >>> 4).toString(16)); + hex.push((bytes[i] & 0xF).toString(16)); + } + return hex.join(""); + }, + + // Convert a hex string to a byte array + hexToBytes: function (hex) { + for (var bytes = [], c = 0; c < hex.length; c += 2) + bytes.push(parseInt(hex.substr(c, 2), 16)); + return bytes; + }, + + // Convert a byte array to a base-64 string + bytesToBase64: function (bytes) { + for (var base64 = [], i = 0; i < bytes.length; i += 3) { + var triplet = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; + for (var j = 0; j < 4; j++) { + if (i * 8 + j * 6 <= bytes.length * 8) + base64.push(base64map.charAt((triplet >>> 6 * (3 - j)) & 0x3F)); + else base64.push("="); + } + } + + return base64.join(""); + }, + + // Convert a base-64 string to a byte array + base64ToBytes: function (base64) { + // Remove non-base-64 characters + base64 = base64.replace(/[^A-Z0-9+\/]/ig, ""); + + for (var bytes = [], i = 0, imod4 = 0; i < base64.length; imod4 = ++i % 4) { + if (imod4 == 0) continue; + bytes.push(((base64map.indexOf(base64.charAt(i - 1)) & (Math.pow(2, -2 * imod4 + 8) - 1)) << (imod4 * 2)) | + (base64map.indexOf(base64.charAt(i)) >>> (6 - imod4 * 2))); + } + + return bytes; + } + + }; + + // Crypto character encodings + var charenc = Crypto.charenc = {}; + + // UTF-8 encoding + var UTF8 = charenc.UTF8 = { + + // Convert a string to a byte array + stringToBytes: function (str) { + return Binary.stringToBytes(unescape(encodeURIComponent(str))); + }, + + // Convert a byte array to a string + bytesToString: function (bytes) { + return decodeURIComponent(escape(Binary.bytesToString(bytes))); + } + + }; + + // Binary encoding + var Binary = charenc.Binary = { + + // Convert a string to a byte array + stringToBytes: function (str) { + for (var bytes = [], i = 0; i < str.length; i++) + bytes.push(str.charCodeAt(i) & 0xFF); + return bytes; + }, + + // Convert a byte array to a string + bytesToString: function (bytes) { + for (var str = [], i = 0; i < bytes.length; i++) + str.push(String.fromCharCode(bytes[i])); + return str.join(""); + } + + }; + + })(); + //Adding SHA1 to fix basic PKBDF2 + /* + * Crypto-JS v2.5.4 + * http://code.google.com/p/crypto-js/ + * (c) 2009-2012 by Jeff Mott. All rights reserved. + * http://code.google.com/p/crypto-js/wiki/License + */ + (function () { + + // Shortcuts + var C = Crypto, + util = C.util, + charenc = C.charenc, + UTF8 = charenc.UTF8, + Binary = charenc.Binary; + + // Public API + var SHA1 = C.SHA1 = function (message, options) { + var digestbytes = util.wordsToBytes(SHA1._sha1(message)); + return options && options.asBytes ? digestbytes : + options && options.asString ? Binary.bytesToString(digestbytes) : + util.bytesToHex(digestbytes); + }; + + // The core + SHA1._sha1 = function (message) { + + // Convert to byte array + if (message.constructor == String) message = UTF8.stringToBytes(message); + /* else, assume byte array already */ + + var m = util.bytesToWords(message), + l = message.length * 8, + w = [], + H0 = 1732584193, + H1 = -271733879, + H2 = -1732584194, + H3 = 271733878, + H4 = -1009589776; + + // Padding + m[l >> 5] |= 0x80 << (24 - l % 32); + m[((l + 64 >>> 9) << 4) + 15] = l; + + for (var i = 0; i < m.length; i += 16) { + + var a = H0, + b = H1, + c = H2, + d = H3, + e = H4; + + for (var j = 0; j < 80; j++) { + + if (j < 16) w[j] = m[i + j]; + else { + var n = w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16]; + w[j] = (n << 1) | (n >>> 31); + } + + var t = ((H0 << 5) | (H0 >>> 27)) + H4 + (w[j] >>> 0) + ( + j < 20 ? (H1 & H2 | ~H1 & H3) + 1518500249 : + j < 40 ? (H1 ^ H2 ^ H3) + 1859775393 : + j < 60 ? (H1 & H2 | H1 & H3 | H2 & H3) - 1894007588 : + (H1 ^ H2 ^ H3) - 899497514); + + H4 = H3; + H3 = H2; + H2 = (H1 << 30) | (H1 >>> 2); + H1 = H0; + H0 = t; + + } + + H0 += a; + H1 += b; + H2 += c; + H3 += d; + H4 += e; + + } + + return [H0, H1, H2, H3, H4]; + + }; + + // Package private blocksize + SHA1._blocksize = 16; + + SHA1._digestsize = 20; + + })(); + + //Added to make PKBDF2 work + /* + * Crypto-JS v2.5.4 + * http://code.google.com/p/crypto-js/ + * (c) 2009-2012 by Jeff Mott. All rights reserved. + * http://code.google.com/p/crypto-js/wiki/License + */ + (function () { + + // Shortcuts + var C = Crypto, + util = C.util, + charenc = C.charenc, + UTF8 = charenc.UTF8, + Binary = charenc.Binary; + + C.HMAC = function (hasher, message, key, options) { + + // Convert to byte arrays + if (message.constructor == String) message = UTF8.stringToBytes(message); + if (key.constructor == String) key = UTF8.stringToBytes(key); + /* else, assume byte arrays already */ + + // Allow arbitrary length keys + if (key.length > hasher._blocksize * 4) + key = hasher(key, { + asBytes: true + }); + + // XOR keys with pad constants + var okey = key.slice(0), + ikey = key.slice(0); + for (var i = 0; i < hasher._blocksize * 4; i++) { + okey[i] ^= 0x5C; + ikey[i] ^= 0x36; + } + + var hmacbytes = hasher(okey.concat(hasher(ikey.concat(message), { + asBytes: true + })), { + asBytes: true + }); + + return options && options.asBytes ? hmacbytes : + options && options.asString ? Binary.bytesToString(hmacbytes) : + util.bytesToHex(hmacbytes); + + }; + + })(); + + + //crypto-sha256-hmac.js + /* + * Crypto-JS v2.5.4 + * http://code.google.com/p/crypto-js/ + * (c) 2009-2012 by Jeff Mott. All rights reserved. + * http://code.google.com/p/crypto-js/wiki/License + */ + (function () { + var d = Crypto, + k = d.util, + g = d.charenc, + b = g.UTF8, + a = g.Binary, + c = [1116352408, 1899447441, 3049323471, 3921009573, 961987163, 1508970993, 2453635748, 2870763221, + 3624381080, 310598401, 607225278, 1426881987, 1925078388, 2162078206, 2614888103, 3248222580, + 3835390401, 4022224774, 264347078, 604807628, 770255983, 1249150122, 1555081692, 1996064986, + 2554220882, 2821834349, 2952996808, 3210313671, 3336571891, 3584528711, 113926993, 338241895, + 666307205, 773529912, 1294757372, 1396182291, 1695183700, 1986661051, 2177026350, 2456956037, + 2730485921, + 2820302411, 3259730800, 3345764771, 3516065817, 3600352804, 4094571909, 275423344, 430227734, + 506948616, 659060556, 883997877, 958139571, 1322822218, 1537002063, 1747873779, 1955562222, + 2024104815, 2227730452, 2361852424, 2428436474, 2756734187, 3204031479, 3329325298 + ], + e = d.SHA256 = function (b, c) { + var f = k.wordsToBytes(e._sha256(b)); + return c && c.asBytes ? f : c && c.asString ? a.bytesToString(f) : k.bytesToHex(f) + }; + e._sha256 = function (a) { + a.constructor == String && (a = b.stringToBytes(a)); + var e = k.bytesToWords(a), + f = a.length * 8, + a = [1779033703, 3144134277, + 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225 + ], + d = [], + g, m, r, i, n, o, s, t, h, l, j; + e[f >> 5] |= 128 << 24 - f % 32; + e[(f + 64 >> 9 << 4) + 15] = f; + for (t = 0; t < e.length; t += 16) { + f = a[0]; + g = a[1]; + m = a[2]; + r = a[3]; + i = a[4]; + n = a[5]; + o = a[6]; + s = a[7]; + for (h = 0; h < 64; h++) { + h < 16 ? d[h] = e[h + t] : (l = d[h - 15], j = d[h - 2], d[h] = ((l << 25 | l >>> 7) ^ + (l << 14 | l >>> 18) ^ l >>> 3) + (d[h - 7] >>> 0) + ((j << 15 | j >>> 17) ^ + (j << 13 | j >>> 19) ^ j >>> 10) + (d[h - 16] >>> 0)); + j = f & g ^ f & m ^ g & m; + var u = (f << 30 | f >>> 2) ^ (f << 19 | f >>> 13) ^ (f << 10 | f >>> 22); + l = (s >>> 0) + ((i << 26 | i >>> 6) ^ (i << 21 | i >>> 11) ^ (i << 7 | i >>> 25)) + + (i & n ^ ~i & o) + c[h] + (d[h] >>> 0); + j = u + j; + s = o; + o = n; + n = i; + i = r + l >>> 0; + r = m; + m = g; + g = f; + f = l + j >>> 0 + } + a[0] += f; + a[1] += g; + a[2] += m; + a[3] += r; + a[4] += i; + a[5] += n; + a[6] += o; + a[7] += s + } + return a + }; + e._blocksize = 16; + e._digestsize = 32 + })(); + (function () { + var d = Crypto, + k = d.util, + g = d.charenc, + b = g.UTF8, + a = g.Binary; + d.HMAC = function (c, e, d, g) { + e.constructor == String && (e = b.stringToBytes(e)); + d.constructor == String && (d = b.stringToBytes(d)); + d.length > c._blocksize * 4 && (d = c(d, { + asBytes: !0 + })); + for (var f = d.slice(0), d = d.slice(0), q = 0; q < c._blocksize * 4; q++) f[q] ^= 92, d[q] ^= + 54; + c = c(f.concat(c(d.concat(e), { + asBytes: !0 + })), { + asBytes: !0 + }); + return g && g.asBytes ? c : g && g.asString ? a.bytesToString(c) : k.bytesToHex(c) + } + })(); + })(); + + //SecureRandom.js + (function () { + + + /*! + * Random number generator with ArcFour PRNG + * + * NOTE: For best results, put code like + * + * in your main HTML document. + * + * Copyright Tom Wu, bitaddress.org BSD License. + * http://www-cs-students.stanford.edu/~tjw/jsbn/LICENSE + */ + + // Constructor function of Global SecureRandom object + var sr = GLOBAL.SecureRandom = function () { }; + + // Properties + sr.state; + sr.pool; + sr.pptr; + sr.poolCopyOnInit; + + // Pool size must be a multiple of 4 and greater than 32. + // An array of bytes the size of the pool will be passed to init() + sr.poolSize = 256; + + // --- object methods --- + + // public method + // ba: byte array + sr.prototype.nextBytes = function (ba) { + var i; + if (getRandomBytes && GLOBAL.Uint8Array) { + try { + var rvBytes = new Uint8Array(ba.length); + getRandomBytes(rvBytes); + for (i = 0; i < ba.length; ++i) + ba[i] = sr.getByte() ^ rvBytes[i]; + return; + } catch (e) { + alert(e); + } + } + for (i = 0; i < ba.length; ++i) ba[i] = sr.getByte(); + }; + + + // --- static methods --- + + // Mix in the current time (w/milliseconds) into the pool + // NOTE: this method should be called from body click/keypress event handlers to increase entropy + sr.seedTime = function () { + sr.seedInt(new Date().getTime()); + } + + sr.getByte = function () { + if (sr.state == null) { + sr.seedTime(); + sr.state = sr.ArcFour(); // Plug in your RNG constructor here + sr.state.init(sr.pool); + sr.poolCopyOnInit = []; + for (sr.pptr = 0; sr.pptr < sr.pool.length; ++sr.pptr) + sr.poolCopyOnInit[sr.pptr] = sr.pool[sr.pptr]; + sr.pptr = 0; + } + // TODO: allow reseeding after first request + return sr.state.next(); + } + + // Mix in a 32-bit integer into the pool + sr.seedInt = function (x) { + sr.seedInt8(x); + sr.seedInt8((x >> 8)); + sr.seedInt8((x >> 16)); + sr.seedInt8((x >> 24)); + } + + // Mix in a 16-bit integer into the pool + sr.seedInt16 = function (x) { + sr.seedInt8(x); + sr.seedInt8((x >> 8)); + } + + // Mix in a 8-bit integer into the pool + sr.seedInt8 = function (x) { + sr.pool[sr.pptr++] ^= x & 255; + if (sr.pptr >= sr.poolSize) sr.pptr -= sr.poolSize; + } + + // Arcfour is a PRNG + sr.ArcFour = function () { + function Arcfour() { + this.i = 0; + this.j = 0; + this.S = new Array(); + } + + // Initialize arcfour context from key, an array of ints, each from [0..255] + function ARC4init(key) { + var i, j, t; + for (i = 0; i < 256; ++i) + this.S[i] = i; + j = 0; + for (i = 0; i < 256; ++i) { + j = (j + this.S[i] + key[i % key.length]) & 255; + t = this.S[i]; + this.S[i] = this.S[j]; + this.S[j] = t; + } + this.i = 0; + this.j = 0; + } + + function ARC4next() { + var t; + this.i = (this.i + 1) & 255; + this.j = (this.j + this.S[this.i]) & 255; + t = this.S[this.i]; + this.S[this.i] = this.S[this.j]; + this.S[this.j] = t; + return this.S[(t + this.S[this.i]) & 255]; + } + + Arcfour.prototype.init = ARC4init; + Arcfour.prototype.next = ARC4next; + + return new Arcfour(); + }; + + + // Initialize the pool with junk if needed. + if (sr.pool == null) { + sr.pool = new Array(); + sr.pptr = 0; + var t; + if (getRandomBytes && GLOBAL.Uint8Array) { + try { + // Use webcrypto if available + var ua = new Uint8Array(sr.poolSize); + getRandomBytes(ua); + for (t = 0; t < sr.poolSize; ++t) + sr.pool[sr.pptr++] = ua[t]; + } catch (e) { + alert(e); + } + } + while (sr.pptr < sr.poolSize) { // extract some randomness from securedMathRandom() + t = Math.floor(65536 * securedMathRandom()); + sr.pool[sr.pptr++] = t >>> 8; + sr.pool[sr.pptr++] = t & 255; + } + sr.pptr = Math.floor(sr.poolSize * securedMathRandom()); + sr.seedTime(); + // entropy + var entropyStr = ""; + // screen size and color depth: ~4.8 to ~5.4 bits + entropyStr += (GLOBAL.screen.height * GLOBAL.screen.width * GLOBAL.screen.colorDepth); + entropyStr += (GLOBAL.screen.availHeight * GLOBAL.screen.availWidth * GLOBAL.screen.pixelDepth); + // time zone offset: ~4 bits + var dateObj = new Date(); + var timeZoneOffset = dateObj.getTimezoneOffset(); + entropyStr += timeZoneOffset; + // user agent: ~8.3 to ~11.6 bits + entropyStr += navigator.userAgent; + // browser plugin details: ~16.2 to ~21.8 bits + var pluginsStr = ""; + for (var i = 0; i < navigator.plugins.length; i++) { + pluginsStr += navigator.plugins[i].name + " " + navigator.plugins[i].filename + " " + navigator.plugins[i].description + " " + navigator.plugins[i].version + ", "; + } + var mimeTypesStr = ""; + for (var i = 0; i < navigator.mimeTypes.length; i++) { + mimeTypesStr += navigator.mimeTypes[i].description + " " + navigator.mimeTypes[i].type + " " + navigator.mimeTypes[i].suffixes + ", "; + } + entropyStr += pluginsStr + mimeTypesStr; + // cookies and storage: 1 bit + entropyStr += navigator.cookieEnabled + typeof (sessionStorage) + typeof (localStorage); + // language: ~7 bit + entropyStr += navigator.language; + // history: ~2 bit + entropyStr += GLOBAL.history.length; + // location + entropyStr += GLOBAL.location; + + var entropyBytes = Crypto.SHA256(entropyStr, { + asBytes: true + }); + for (var i = 0; i < entropyBytes.length; i++) { + sr.seedInt8(entropyBytes[i]); + } + } + })(); + + //ripemd160.js + (function () { + + /* + CryptoJS v3.1.2 + code.google.com/p/crypto-js + (c) 2009-2013 by Jeff Mott. All rights reserved. + code.google.com/p/crypto-js/wiki/License + */ + /** @preserve + (c) 2012 by Cédric Mesnil. All rights reserved. + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + // Constants table + var zl = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, + 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, + 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, + 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13 + ]; + var zr = [ + 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, + 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, + 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, + 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, + 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11 + ]; + var sl = [ + 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, + 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, + 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, + 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, + 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 + ]; + var sr = [ + 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, + 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, + 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, + 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, + 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 + ]; + + var hl = [0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E]; + var hr = [0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000]; + + var bytesToWords = function (bytes) { + var words = []; + for (var i = 0, b = 0; i < bytes.length; i++, b += 8) { + words[b >>> 5] |= bytes[i] << (24 - b % 32); + } + return words; + }; + + var wordsToBytes = function (words) { + var bytes = []; + for (var b = 0; b < words.length * 32; b += 8) { + bytes.push((words[b >>> 5] >>> (24 - b % 32)) & 0xFF); + } + return bytes; + }; + + var processBlock = function (H, M, offset) { + + // Swap endian + for (var i = 0; i < 16; i++) { + var offset_i = offset + i; + var M_offset_i = M[offset_i]; + + // Swap + M[offset_i] = ( + (((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) | + (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00) + ); + } + + // Working variables + var al, bl, cl, dl, el; + var ar, br, cr, dr, er; + + ar = al = H[0]; + br = bl = H[1]; + cr = cl = H[2]; + dr = dl = H[3]; + er = el = H[4]; + // Computation + var t; + for (var i = 0; i < 80; i += 1) { + t = (al + M[offset + zl[i]]) | 0; + if (i < 16) { + t += f1(bl, cl, dl) + hl[0]; + } else if (i < 32) { + t += f2(bl, cl, dl) + hl[1]; + } else if (i < 48) { + t += f3(bl, cl, dl) + hl[2]; + } else if (i < 64) { + t += f4(bl, cl, dl) + hl[3]; + } else { // if (i<80) { + t += f5(bl, cl, dl) + hl[4]; + } + t = t | 0; + t = rotl(t, sl[i]); + t = (t + el) | 0; + al = el; + el = dl; + dl = rotl(cl, 10); + cl = bl; + bl = t; + + t = (ar + M[offset + zr[i]]) | 0; + if (i < 16) { + t += f5(br, cr, dr) + hr[0]; + } else if (i < 32) { + t += f4(br, cr, dr) + hr[1]; + } else if (i < 48) { + t += f3(br, cr, dr) + hr[2]; + } else if (i < 64) { + t += f2(br, cr, dr) + hr[3]; + } else { // if (i<80) { + t += f1(br, cr, dr) + hr[4]; + } + t = t | 0; + t = rotl(t, sr[i]); + t = (t + er) | 0; + ar = er; + er = dr; + dr = rotl(cr, 10); + cr = br; + br = t; + } + // Intermediate hash value + t = (H[1] + cl + dr) | 0; + H[1] = (H[2] + dl + er) | 0; + H[2] = (H[3] + el + ar) | 0; + H[3] = (H[4] + al + br) | 0; + H[4] = (H[0] + bl + cr) | 0; + H[0] = t; + }; + + function f1(x, y, z) { + return ((x) ^ (y) ^ (z)); + } + + function f2(x, y, z) { + return (((x) & (y)) | ((~x) & (z))); + } + + function f3(x, y, z) { + return (((x) | (~(y))) ^ (z)); + } + + function f4(x, y, z) { + return (((x) & (z)) | ((y) & (~(z)))); + } + + function f5(x, y, z) { + return ((x) ^ ((y) | (~(z)))); + } + + function rotl(x, n) { + return (x << n) | (x >>> (32 - n)); + } + + GLOBAL.ripemd160 = function ripemd160(message) { + var H = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]; + + var m = bytesToWords(message); + + var nBitsLeft = message.length * 8; + var nBitsTotal = message.length * 8; + + // Add padding + m[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + m[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ( + (((nBitsTotal << 8) | (nBitsTotal >>> 24)) & 0x00ff00ff) | + (((nBitsTotal << 24) | (nBitsTotal >>> 8)) & 0xff00ff00) + ); + + for (var i = 0; i < m.length; i += 16) { + processBlock(H, m, i); + } + + // Swap endian + for (var i = 0; i < 5; i++) { + // Shortcut + var H_i = H[i]; + + // Swap + H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | + (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00); + } + + var digestbytes = wordsToBytes(H); + return digestbytes; + } + })(); + + //BigInteger.js + (function () { + // Upstream 'BigInteger' here: + // Original Author: http://www-cs-students.stanford.edu/~tjw/jsbn/ + // Follows 'jsbn' on Github: https://github.com/jasondavies/jsbn + // Review and Testing: https://github.com/cryptocoinjs/bigi/ + /*! + * Basic JavaScript BN library - subset useful for RSA encryption. v1.4 + * + * Copyright (c) 2005 Tom Wu + * All Rights Reserved. + * BSD License + * http://www-cs-students.stanford.edu/~tjw/jsbn/LICENSE + * + * Copyright Stephan Thomas + * Copyright pointbiz + */ + + // (public) Constructor function of Global BigInteger object + var BigInteger = GLOBAL.BigInteger = function BigInteger(a, b, c) { + if (!(this instanceof BigInteger)) + return new BigInteger(a, b, c); + + if (a != null) + if ("number" == typeof a) this.fromNumber(a, b, c); + else if (b == null && "string" != typeof a) this.fromString(a, 256); + else this.fromString(a, b); + }; + + // Bits per digit + var dbits; + + // JavaScript engine analysis + var canary = 0xdeadbeefcafe; + var j_lm = ((canary & 0xffffff) == 0xefcafe); + + // return new, unset BigInteger + function nbi() { + return new BigInteger(null); + } + + // am: Compute w_j += (x*this_i), propagate carries, + // c is initial carry, returns final carry. + // c < 3*dvalue, x < 2*dvalue, this_i < dvalue + // We need to select the fastest one that works in this environment. + + // am1: use a single mult and divide to get the high bits, + // max digit bits should be 26 because + // max internal value = 2*dvalue^2-2*dvalue (< 2^53) + function am1(i, x, w, j, c, n) { + while (--n >= 0) { + var v = x * this[i++] + w[j] + c; + c = Math.floor(v / 0x4000000); + w[j++] = v & 0x3ffffff; + } + return c; + } + // am2 avoids a big mult-and-extract completely. + // Max digit bits should be <= 30 because we do bitwise ops + // on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) + function am2(i, x, w, j, c, n) { + var xl = x & 0x7fff, + xh = x >> 15; + while (--n >= 0) { + var l = this[i] & 0x7fff; + var h = this[i++] >> 15; + var m = xh * l + h * xl; + l = xl * l + ((m & 0x7fff) << 15) + w[j] + (c & 0x3fffffff); + c = (l >>> 30) + (m >>> 15) + xh * h + (c >>> 30); + w[j++] = l & 0x3fffffff; + } + return c; + } + // Alternately, set max digit bits to 28 since some + // browsers slow down when dealing with 32-bit numbers. + function am3(i, x, w, j, c, n) { + var xl = x & 0x3fff, + xh = x >> 14; + while (--n >= 0) { + var l = this[i] & 0x3fff; + var h = this[i++] >> 14; + var m = xh * l + h * xl; + l = xl * l + ((m & 0x3fff) << 14) + w[j] + c; + c = (l >> 28) + (m >> 14) + xh * h; + w[j++] = l & 0xfffffff; + } + return c; + } + if (j_lm && (navigator.appName == "Microsoft Internet Explorer")) { + BigInteger.prototype.am = am2; + dbits = 30; + } else if (j_lm && (navigator.appName != "Netscape")) { + BigInteger.prototype.am = am1; + dbits = 26; + } else { // Mozilla/Netscape seems to prefer am3 + BigInteger.prototype.am = am3; + dbits = 28; + } + + BigInteger.prototype.DB = dbits; + BigInteger.prototype.DM = ((1 << dbits) - 1); + BigInteger.prototype.DV = (1 << dbits); + + var BI_FP = 52; + BigInteger.prototype.FV = Math.pow(2, BI_FP); + BigInteger.prototype.F1 = BI_FP - dbits; + BigInteger.prototype.F2 = 2 * dbits - BI_FP; + + // Digit conversions + var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz"; + var BI_RC = new Array(); + var rr, vv; + rr = "0".charCodeAt(0); + for (vv = 0; vv <= 9; ++vv) BI_RC[rr++] = vv; + rr = "a".charCodeAt(0); + for (vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv; + rr = "A".charCodeAt(0); + for (vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv; + + function int2char(n) { + return BI_RM.charAt(n); + } + + function intAt(s, i) { + var c = BI_RC[s.charCodeAt(i)]; + return (c == null) ? -1 : c; + } + + + + // return bigint initialized to value + function nbv(i) { + var r = nbi(); + r.fromInt(i); + return r; + } + + + // returns bit length of the integer x + function nbits(x) { + var r = 1, + t; + if ((t = x >>> 16) != 0) { + x = t; + r += 16; + } + if ((t = x >> 8) != 0) { + x = t; + r += 8; + } + if ((t = x >> 4) != 0) { + x = t; + r += 4; + } + if ((t = x >> 2) != 0) { + x = t; + r += 2; + } + if ((t = x >> 1) != 0) { + x = t; + r += 1; + } + return r; + } + + + + + + + + // (protected) copy this to r + BigInteger.prototype.copyTo = function (r) { + for (var i = this.t - 1; i >= 0; --i) r[i] = this[i]; + r.t = this.t; + r.s = this.s; + }; + + + // (protected) set from integer value x, -DV <= x < DV + BigInteger.prototype.fromInt = function (x) { + this.t = 1; + this.s = (x < 0) ? -1 : 0; + if (x > 0) this[0] = x; + else if (x < -1) this[0] = x + this.DV; + else this.t = 0; + }; + + // (protected) set from string and radix + BigInteger.prototype.fromString = function (s, b) { + var k; + if (b == 16) k = 4; + else if (b == 8) k = 3; + else if (b == 256) k = 8; // byte array + else if (b == 2) k = 1; + else if (b == 32) k = 5; + else if (b == 4) k = 2; + else { + this.fromRadix(s, b); + return; + } + this.t = 0; + this.s = 0; + var i = s.length, + mi = false, + sh = 0; + while (--i >= 0) { + var x = (k == 8) ? s[i] & 0xff : intAt(s, i); + if (x < 0) { + if (s.charAt(i) == "-") mi = true; + continue; + } + mi = false; + if (sh == 0) + this[this.t++] = x; + else if (sh + k > this.DB) { + this[this.t - 1] |= (x & ((1 << (this.DB - sh)) - 1)) << sh; + this[this.t++] = (x >> (this.DB - sh)); + } else + this[this.t - 1] |= x << sh; + sh += k; + if (sh >= this.DB) sh -= this.DB; + } + if (k == 8 && (s[0] & 0x80) != 0) { + this.s = -1; + if (sh > 0) this[this.t - 1] |= ((1 << (this.DB - sh)) - 1) << sh; + } + this.clamp(); + if (mi) BigInteger.ZERO.subTo(this, this); + }; + + + // (protected) clamp off excess high words + BigInteger.prototype.clamp = function () { + var c = this.s & this.DM; + while (this.t > 0 && this[this.t - 1] == c) --this.t; + }; + + // (protected) r = this << n*DB + BigInteger.prototype.dlShiftTo = function (n, r) { + var i; + for (i = this.t - 1; i >= 0; --i) r[i + n] = this[i]; + for (i = n - 1; i >= 0; --i) r[i] = 0; + r.t = this.t + n; + r.s = this.s; + }; + + // (protected) r = this >> n*DB + BigInteger.prototype.drShiftTo = function (n, r) { + for (var i = n; i < this.t; ++i) r[i - n] = this[i]; + r.t = Math.max(this.t - n, 0); + r.s = this.s; + }; + + + // (protected) r = this << n + BigInteger.prototype.lShiftTo = function (n, r) { + var bs = n % this.DB; + var cbs = this.DB - bs; + var bm = (1 << cbs) - 1; + var ds = Math.floor(n / this.DB), + c = (this.s << bs) & this.DM, + i; + for (i = this.t - 1; i >= 0; --i) { + r[i + ds + 1] = (this[i] >> cbs) | c; + c = (this[i] & bm) << bs; + } + for (i = ds - 1; i >= 0; --i) r[i] = 0; + r[ds] = c; + r.t = this.t + ds + 1; + r.s = this.s; + r.clamp(); + }; + + + // (protected) r = this >> n + BigInteger.prototype.rShiftTo = function (n, r) { + r.s = this.s; + var ds = Math.floor(n / this.DB); + if (ds >= this.t) { + r.t = 0; + return; + } + var bs = n % this.DB; + var cbs = this.DB - bs; + var bm = (1 << bs) - 1; + r[0] = this[ds] >> bs; + for (var i = ds + 1; i < this.t; ++i) { + r[i - ds - 1] |= (this[i] & bm) << cbs; + r[i - ds] = this[i] >> bs; + } + if (bs > 0) r[this.t - ds - 1] |= (this.s & bm) << cbs; + r.t = this.t - ds; + r.clamp(); + }; + + + // (protected) r = this - a + BigInteger.prototype.subTo = function (a, r) { + var i = 0, + c = 0, + m = Math.min(a.t, this.t); + while (i < m) { + c += this[i] - a[i]; + r[i++] = c & this.DM; + c >>= this.DB; + } + if (a.t < this.t) { + c -= a.s; + while (i < this.t) { + c += this[i]; + r[i++] = c & this.DM; + c >>= this.DB; + } + c += this.s; + } else { + c += this.s; + while (i < a.t) { + c -= a[i]; + r[i++] = c & this.DM; + c >>= this.DB; + } + c -= a.s; + } + r.s = (c < 0) ? -1 : 0; + if (c < -1) r[i++] = this.DV + c; + else if (c > 0) r[i++] = c; + r.t = i; + r.clamp(); + }; + + + // (protected) r = this * a, r != this,a (HAC 14.12) + // "this" should be the larger one if appropriate. + BigInteger.prototype.multiplyTo = function (a, r) { + var x = this.abs(), + y = a.abs(); + var i = x.t; + r.t = i + y.t; + while (--i >= 0) r[i] = 0; + for (i = 0; i < y.t; ++i) r[i + x.t] = x.am(0, y[i], r, i, 0, x.t); + r.s = 0; + r.clamp(); + if (this.s != a.s) BigInteger.ZERO.subTo(r, r); + }; + + + // (protected) r = this^2, r != this (HAC 14.16) + BigInteger.prototype.squareTo = function (r) { + var x = this.abs(); + var i = r.t = 2 * x.t; + while (--i >= 0) r[i] = 0; + for (i = 0; i < x.t - 1; ++i) { + var c = x.am(i, x[i], r, 2 * i, 0, 1); + if ((r[i + x.t] += x.am(i + 1, 2 * x[i], r, 2 * i + 1, c, x.t - i - 1)) >= x.DV) { + r[i + x.t] -= x.DV; + r[i + x.t + 1] = 1; + } + } + if (r.t > 0) r[r.t - 1] += x.am(i, x[i], r, 2 * i, 0, 1); + r.s = 0; + r.clamp(); + }; + + + + // (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) + // r != q, this != m. q or r may be null. + BigInteger.prototype.divRemTo = function (m, q, r) { + var pm = m.abs(); + if (pm.t <= 0) return; + var pt = this.abs(); + if (pt.t < pm.t) { + if (q != null) q.fromInt(0); + if (r != null) this.copyTo(r); + return; + } + if (r == null) r = nbi(); + var y = nbi(), + ts = this.s, + ms = m.s; + var nsh = this.DB - nbits(pm[pm.t - 1]); // normalize modulus + if (nsh > 0) { + pm.lShiftTo(nsh, y); + pt.lShiftTo(nsh, r); + } else { + pm.copyTo(y); + pt.copyTo(r); + } + var ys = y.t; + var y0 = y[ys - 1]; + if (y0 == 0) return; + var yt = y0 * (1 << this.F1) + ((ys > 1) ? y[ys - 2] >> this.F2 : 0); + var d1 = this.FV / yt, + d2 = (1 << this.F1) / yt, + e = 1 << this.F2; + var i = r.t, + j = i - ys, + t = (q == null) ? nbi() : q; + y.dlShiftTo(j, t); + if (r.compareTo(t) >= 0) { + r[r.t++] = 1; + r.subTo(t, r); + } + BigInteger.ONE.dlShiftTo(ys, t); + t.subTo(y, y); // "negative" y so we can replace sub with am later + while (y.t < ys) y[y.t++] = 0; + while (--j >= 0) { + // Estimate quotient digit + var qd = (r[--i] == y0) ? this.DM : Math.floor(r[i] * d1 + (r[i - 1] + e) * d2); + if ((r[i] += y.am(0, qd, r, j, 0, ys)) < qd) { // Try it out + y.dlShiftTo(j, t); + r.subTo(t, r); + while (r[i] < --qd) r.subTo(t, r); + } + } + if (q != null) { + r.drShiftTo(ys, q); + if (ts != ms) BigInteger.ZERO.subTo(q, q); + } + r.t = ys; + r.clamp(); + if (nsh > 0) r.rShiftTo(nsh, r); // Denormalize remainder + if (ts < 0) BigInteger.ZERO.subTo(r, r); + }; + + + // (protected) return "-1/this % 2^DB"; useful for Mont. reduction + // justification: + // xy == 1 (mod m) + // xy = 1+km + // xy(2-xy) = (1+km)(1-km) + // x[y(2-xy)] = 1-k^2m^2 + // x[y(2-xy)] == 1 (mod m^2) + // if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 + // should reduce x and y(2-xy) by m^2 at each step to keep size bounded. + // JS multiply "overflows" differently from C/C++, so care is needed here. + BigInteger.prototype.invDigit = function () { + if (this.t < 1) return 0; + var x = this[0]; + if ((x & 1) == 0) return 0; + var y = x & 3; // y == 1/x mod 2^2 + y = (y * (2 - (x & 0xf) * y)) & 0xf; // y == 1/x mod 2^4 + y = (y * (2 - (x & 0xff) * y)) & 0xff; // y == 1/x mod 2^8 + y = (y * (2 - (((x & 0xffff) * y) & 0xffff))) & 0xffff; // y == 1/x mod 2^16 + // last step - calculate inverse mod DV directly; + // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints + y = (y * (2 - x * y % this.DV)) % this.DV; // y == 1/x mod 2^dbits + // we really want the negative inverse, and -DV < y < DV + return (y > 0) ? this.DV - y : -y; + }; + + + // (protected) true iff this is even + BigInteger.prototype.isEven = function () { + return ((this.t > 0) ? (this[0] & 1) : this.s) == 0; + }; + + + // (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) + BigInteger.prototype.exp = function (e, z) { + if (e > 0xffffffff || e < 1) return BigInteger.ONE; + var r = nbi(), + r2 = nbi(), + g = z.convert(this), + i = nbits(e) - 1; + g.copyTo(r); + while (--i >= 0) { + z.sqrTo(r, r2); + if ((e & (1 << i)) > 0) z.mulTo(r2, g, r); + else { + var t = r; + r = r2; + r2 = t; + } + } + return z.revert(r); + }; + + + // (public) return string representation in given radix + BigInteger.prototype.toString = function (b) { + if (this.s < 0) return "-" + this.negate().toString(b); + var k; + if (b == 16) k = 4; + else if (b == 8) k = 3; + else if (b == 2) k = 1; + else if (b == 32) k = 5; + else if (b == 4) k = 2; + else return this.toRadix(b); + var km = (1 << k) - 1, + d, m = false, + r = "", + i = this.t; + var p = this.DB - (i * this.DB) % k; + if (i-- > 0) { + if (p < this.DB && (d = this[i] >> p) > 0) { + m = true; + r = int2char(d); + } + while (i >= 0) { + if (p < k) { + d = (this[i] & ((1 << p) - 1)) << (k - p); + d |= this[--i] >> (p += this.DB - k); + } else { + d = (this[i] >> (p -= k)) & km; + if (p <= 0) { + p += this.DB; + --i; + } + } + if (d > 0) m = true; + if (m) r += int2char(d); + } + } + return m ? r : "0"; + }; + + + // (public) -this + BigInteger.prototype.negate = function () { + var r = nbi(); + BigInteger.ZERO.subTo(this, r); + return r; + }; + + // (public) |this| + BigInteger.prototype.abs = function () { + return (this.s < 0) ? this.negate() : this; + }; + + // (public) return + if this > a, - if this < a, 0 if equal + BigInteger.prototype.compareTo = function (a) { + var r = this.s - a.s; + if (r != 0) return r; + var i = this.t; + r = i - a.t; + if (r != 0) return (this.s < 0) ? -r : r; + while (--i >= 0) + if ((r = this[i] - a[i]) != 0) return r; + return 0; + } + + // (public) return the number of bits in "this" + BigInteger.prototype.bitLength = function () { + if (this.t <= 0) return 0; + return this.DB * (this.t - 1) + nbits(this[this.t - 1] ^ (this.s & this.DM)); + }; + + // (public) this mod a + BigInteger.prototype.mod = function (a) { + var r = nbi(); + this.abs().divRemTo(a, null, r); + if (this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r, r); + return r; + } + + // (public) this^e % m, 0 <= e < 2^32 + BigInteger.prototype.modPowInt = function (e, m) { + var z; + if (e < 256 || m.isEven()) z = new Classic(m); + else z = new Montgomery(m); + return this.exp(e, z); + }; + + // "constants" + BigInteger.ZERO = nbv(0); + BigInteger.ONE = nbv(1); + + + + + + + + // Copyright (c) 2005-2009 Tom Wu + // All Rights Reserved. + // See "LICENSE" for details. + // Extended JavaScript BN functions, required for RSA private ops. + // Version 1.1: new BigInteger("0", 10) returns "proper" zero + // Version 1.2: square() API, isProbablePrime fix + + + // return index of lowest 1-bit in x, x < 2^31 + function lbit(x) { + if (x == 0) return -1; + var r = 0; + if ((x & 0xffff) == 0) { + x >>= 16; + r += 16; + } + if ((x & 0xff) == 0) { + x >>= 8; + r += 8; + } + if ((x & 0xf) == 0) { + x >>= 4; + r += 4; + } + if ((x & 3) == 0) { + x >>= 2; + r += 2; + } + if ((x & 1) == 0) ++r; + return r; + } + + // return number of 1 bits in x + function cbit(x) { + var r = 0; + while (x != 0) { + x &= x - 1; + ++r; + } + return r; + } + + var lowprimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, + 89, + 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, + 193, + 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, + 311, + 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, + 433, + 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, + 569, + 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, + 683, + 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, + 827, + 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, + 971, + 977, 983, 991, 997 + ]; + var lplim = (1 << 26) / lowprimes[lowprimes.length - 1]; + + + + // (protected) return x s.t. r^x < DV + BigInteger.prototype.chunkSize = function (r) { + return Math.floor(Math.LN2 * this.DB / Math.log(r)); + }; + + // (protected) convert to radix string + BigInteger.prototype.toRadix = function (b) { + if (b == null) b = 10; + if (this.signum() == 0 || b < 2 || b > 36) return "0"; + var cs = this.chunkSize(b); + var a = Math.pow(b, cs); + var d = nbv(a), + y = nbi(), + z = nbi(), + r = ""; + this.divRemTo(d, y, z); + while (y.signum() > 0) { + r = (a + z.intValue()).toString(b).substr(1) + r; + y.divRemTo(d, y, z); + } + return z.intValue().toString(b) + r; + }; + + // (protected) convert from radix string + BigInteger.prototype.fromRadix = function (s, b) { + this.fromInt(0); + if (b == null) b = 10; + var cs = this.chunkSize(b); + var d = Math.pow(b, cs), + mi = false, + j = 0, + w = 0; + for (var i = 0; i < s.length; ++i) { + var x = intAt(s, i); + if (x < 0) { + if (s.charAt(i) == "-" && this.signum() == 0) mi = true; + continue; + } + w = b * w + x; + if (++j >= cs) { + this.dMultiply(d); + this.dAddOffset(w, 0); + j = 0; + w = 0; + } + } + if (j > 0) { + this.dMultiply(Math.pow(b, j)); + this.dAddOffset(w, 0); + } + if (mi) BigInteger.ZERO.subTo(this, this); + }; + + // (protected) alternate constructor + BigInteger.prototype.fromNumber = function (a, b, c) { + if ("number" == typeof b) { + // new BigInteger(int,int,RNG) + if (a < 2) this.fromInt(1); + else { + this.fromNumber(a, c); + if (!this.testBit(a - 1)) // force MSB set + this.bitwiseTo(BigInteger.ONE.shiftLeft(a - 1), op_or, this); + if (this.isEven()) this.dAddOffset(1, 0); // force odd + while (!this.isProbablePrime(b)) { + this.dAddOffset(2, 0); + if (this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a - 1), this); + } + } + } else { + // new BigInteger(int,RNG) + var x = new Array(), + t = a & 7; + x.length = (a >> 3) + 1; + b.nextBytes(x); + if (t > 0) x[0] &= ((1 << t) - 1); + else x[0] = 0; + this.fromString(x, 256); + } + }; + + // (protected) r = this op a (bitwise) + BigInteger.prototype.bitwiseTo = function (a, op, r) { + var i, f, m = Math.min(a.t, this.t); + for (i = 0; i < m; ++i) r[i] = op(this[i], a[i]); + if (a.t < this.t) { + f = a.s & this.DM; + for (i = m; i < this.t; ++i) r[i] = op(this[i], f); + r.t = this.t; + } else { + f = this.s & this.DM; + for (i = m; i < a.t; ++i) r[i] = op(f, a[i]); + r.t = a.t; + } + r.s = op(this.s, a.s); + r.clamp(); + }; + + // (protected) this op (1<>= this.DB; + } + if (a.t < this.t) { + c += a.s; + while (i < this.t) { + c += this[i]; + r[i++] = c & this.DM; + c >>= this.DB; + } + c += this.s; + } else { + c += this.s; + while (i < a.t) { + c += a[i]; + r[i++] = c & this.DM; + c >>= this.DB; + } + c += a.s; + } + r.s = (c < 0) ? -1 : 0; + if (c > 0) r[i++] = c; + else if (c < -1) r[i++] = this.DV + c; + r.t = i; + r.clamp(); + }; + + // (protected) this *= n, this >= 0, 1 < n < DV + BigInteger.prototype.dMultiply = function (n) { + this[this.t] = this.am(0, n - 1, this, 0, 0, this.t); + ++this.t; + this.clamp(); + }; + + // (protected) this += n << w words, this >= 0 + BigInteger.prototype.dAddOffset = function (n, w) { + if (n == 0) return; + while (this.t <= w) this[this.t++] = 0; + this[w] += n; + while (this[w] >= this.DV) { + this[w] -= this.DV; + if (++w >= this.t) this[this.t++] = 0; + ++this[w]; + } + }; + + // (protected) r = lower n words of "this * a", a.t <= n + // "this" should be the larger one if appropriate. + BigInteger.prototype.multiplyLowerTo = function (a, n, r) { + var i = Math.min(this.t + a.t, n); + r.s = 0; // assumes a,this >= 0 + r.t = i; + while (i > 0) r[--i] = 0; + var j; + for (j = r.t - this.t; i < j; ++i) r[i + this.t] = this.am(0, a[i], r, i, 0, this.t); + for (j = Math.min(a.t, n); i < j; ++i) this.am(0, a[i], r, i, 0, n - i); + r.clamp(); + }; + + + // (protected) r = "this * a" without lower n words, n > 0 + // "this" should be the larger one if appropriate. + BigInteger.prototype.multiplyUpperTo = function (a, n, r) { + --n; + var i = r.t = this.t + a.t - n; + r.s = 0; // assumes a,this >= 0 + while (--i >= 0) r[i] = 0; + for (i = Math.max(n - this.t, 0); i < a.t; ++i) + r[this.t + i - n] = this.am(n - i, a[i], r, 0, 0, this.t + i - n); + r.clamp(); + r.drShiftTo(1, r); + }; + + // (protected) this % n, n < 2^26 + BigInteger.prototype.modInt = function (n) { + if (n <= 0) return 0; + var d = this.DV % n, + r = (this.s < 0) ? n - 1 : 0; + if (this.t > 0) + if (d == 0) r = this[0] % n; + else + for (var i = this.t - 1; i >= 0; --i) r = (d * r + this[i]) % n; + return r; + }; + + + // (protected) true if probably prime (HAC 4.24, Miller-Rabin) + BigInteger.prototype.millerRabin = function (t) { + var n1 = this.subtract(BigInteger.ONE); + var k = n1.getLowestSetBit(); + if (k <= 0) return false; + var r = n1.shiftRight(k); + t = (t + 1) >> 1; + if (t > lowprimes.length) t = lowprimes.length; + var a = nbi(); + for (var i = 0; i < t; ++i) { + //Pick bases at random, instead of starting at 2 + a.fromInt(lowprimes[Math.floor(securedMathRandom() * lowprimes.length)]); + var y = a.modPow(r, this); + if (y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) { + var j = 1; + while (j++ < k && y.compareTo(n1) != 0) { + y = y.modPowInt(2, this); + if (y.compareTo(BigInteger.ONE) == 0) return false; + } + if (y.compareTo(n1) != 0) return false; + } + } + return true; + }; + + + + // (public) + BigInteger.prototype.clone = function () { + var r = nbi(); + this.copyTo(r); + return r; + }; + + // (public) return value as integer + BigInteger.prototype.intValue = function () { + if (this.s < 0) { + if (this.t == 1) return this[0] - this.DV; + else if (this.t == 0) return -1; + } else if (this.t == 1) return this[0]; + else if (this.t == 0) return 0; + // assumes 16 < DB < 32 + return ((this[1] & ((1 << (32 - this.DB)) - 1)) << this.DB) | this[0]; + }; + + + // (public) return value as byte + BigInteger.prototype.byteValue = function () { + return (this.t == 0) ? this.s : (this[0] << 24) >> 24; + }; + + // (public) return value as short (assumes DB>=16) + BigInteger.prototype.shortValue = function () { + return (this.t == 0) ? this.s : (this[0] << 16) >> 16; + }; + + // (public) 0 if this == 0, 1 if this > 0 + BigInteger.prototype.signum = function () { + if (this.s < 0) return -1; + else if (this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0; + else return 1; + }; + + + // (public) convert to bigendian byte array + BigInteger.prototype.toByteArray = function () { + var i = this.t, + r = new Array(); + r[0] = this.s; + var p = this.DB - (i * this.DB) % 8, + d, k = 0; + if (i-- > 0) { + if (p < this.DB && (d = this[i] >> p) != (this.s & this.DM) >> p) + r[k++] = d | (this.s << (this.DB - p)); + while (i >= 0) { + if (p < 8) { + d = (this[i] & ((1 << p) - 1)) << (8 - p); + d |= this[--i] >> (p += this.DB - 8); + } else { + d = (this[i] >> (p -= 8)) & 0xff; + if (p <= 0) { + p += this.DB; + --i; + } + } + if ((d & 0x80) != 0) d |= -256; + if (k == 0 && (this.s & 0x80) != (d & 0x80)) ++k; + if (k > 0 || d != this.s) r[k++] = d; + } + } + return r; + }; + + BigInteger.prototype.equals = function (a) { + return (this.compareTo(a) == 0); + }; + BigInteger.prototype.min = function (a) { + return (this.compareTo(a) < 0) ? this : a; + }; + BigInteger.prototype.max = function (a) { + return (this.compareTo(a) > 0) ? this : a; + }; + + // (public) this & a + function op_and(x, y) { + return x & y; + } + BigInteger.prototype.and = function (a) { + var r = nbi(); + this.bitwiseTo(a, op_and, r); + return r; + }; + + // (public) this | a + function op_or(x, y) { + return x | y; + } + BigInteger.prototype.or = function (a) { + var r = nbi(); + this.bitwiseTo(a, op_or, r); + return r; + }; + + // (public) this ^ a + function op_xor(x, y) { + return x ^ y; + } + BigInteger.prototype.xor = function (a) { + var r = nbi(); + this.bitwiseTo(a, op_xor, r); + return r; + }; + + // (public) this & ~a + function op_andnot(x, y) { + return x & ~y; + } + BigInteger.prototype.andNot = function (a) { + var r = nbi(); + this.bitwiseTo(a, op_andnot, r); + return r; + }; + + // (public) ~this + BigInteger.prototype.not = function () { + var r = nbi(); + for (var i = 0; i < this.t; ++i) r[i] = this.DM & ~this[i]; + r.t = this.t; + r.s = ~this.s; + return r; + }; + + // (public) this << n + BigInteger.prototype.shiftLeft = function (n) { + var r = nbi(); + if (n < 0) this.rShiftTo(-n, r); + else this.lShiftTo(n, r); + return r; + }; + + // (public) this >> n + BigInteger.prototype.shiftRight = function (n) { + var r = nbi(); + if (n < 0) this.lShiftTo(-n, r); + else this.rShiftTo(n, r); + return r; + }; + + // (public) returns index of lowest 1-bit (or -1 if none) + BigInteger.prototype.getLowestSetBit = function () { + for (var i = 0; i < this.t; ++i) + if (this[i] != 0) return i * this.DB + lbit(this[i]); + if (this.s < 0) return this.t * this.DB; + return -1; + }; + + // (public) return number of set bits + BigInteger.prototype.bitCount = function () { + var r = 0, + x = this.s & this.DM; + for (var i = 0; i < this.t; ++i) r += cbit(this[i] ^ x); + return r; + }; + + // (public) true iff nth bit is set + BigInteger.prototype.testBit = function (n) { + var j = Math.floor(n / this.DB); + if (j >= this.t) return (this.s != 0); + return ((this[j] & (1 << (n % this.DB))) != 0); + }; + + // (public) this | (1< 1) { + var g2 = nbi(); + z.sqrTo(g[1], g2); + while (n <= km) { + g[n] = nbi(); + z.mulTo(g2, g[n - 2], g[n]); + n += 2; + } + } + + var j = e.t - 1, + w, is1 = true, + r2 = nbi(), + t; + i = nbits(e[j]) - 1; + while (j >= 0) { + if (i >= k1) w = (e[j] >> (i - k1)) & km; + else { + w = (e[j] & ((1 << (i + 1)) - 1)) << (k1 - i); + if (j > 0) w |= e[j - 1] >> (this.DB + i - k1); + } + + n = k; + while ((w & 1) == 0) { + w >>= 1; + --n; + } + if ((i -= n) < 0) { + i += this.DB; + --j; + } + if (is1) { // ret == 1, don't bother squaring or multiplying it + g[w].copyTo(r); + is1 = false; + } else { + while (n > 1) { + z.sqrTo(r, r2); + z.sqrTo(r2, r); + n -= 2; + } + if (n > 0) z.sqrTo(r, r2); + else { + t = r; + r = r2; + r2 = t; + } + z.mulTo(r2, g[w], r); + } + + while (j >= 0 && (e[j] & (1 << i)) == 0) { + z.sqrTo(r, r2); + t = r; + r = r2; + r2 = t; + if (--i < 0) { + i = this.DB - 1; + --j; + } + } + } + return z.revert(r); + }; + + // (public) 1/this % m (HAC 14.61) + BigInteger.prototype.modInverse = function (m) { + var ac = m.isEven(); + if (this.signum() === 0) throw new Error('division by zero'); + if ((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO; + var u = m.clone(), + v = this.clone(); + var a = nbv(1), + b = nbv(0), + c = nbv(0), + d = nbv(1); + while (u.signum() != 0) { + while (u.isEven()) { + u.rShiftTo(1, u); + if (ac) { + if (!a.isEven() || !b.isEven()) { + a.addTo(this, a); + b.subTo(m, b); + } + a.rShiftTo(1, a); + } else if (!b.isEven()) b.subTo(m, b); + b.rShiftTo(1, b); + } + while (v.isEven()) { + v.rShiftTo(1, v); + if (ac) { + if (!c.isEven() || !d.isEven()) { + c.addTo(this, c); + d.subTo(m, d); + } + c.rShiftTo(1, c); + } else if (!d.isEven()) d.subTo(m, d); + d.rShiftTo(1, d); + } + if (u.compareTo(v) >= 0) { + u.subTo(v, u); + if (ac) a.subTo(c, a); + b.subTo(d, b); + } else { + v.subTo(u, v); + if (ac) c.subTo(a, c); + d.subTo(b, d); + } + } + if (v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; + while (d.compareTo(m) >= 0) d.subTo(m, d); + while (d.signum() < 0) d.addTo(m, d); + return d; + }; + + + // (public) this^e + BigInteger.prototype.pow = function (e) { + return this.exp(e, new NullExp()); + }; + + // (public) gcd(this,a) (HAC 14.54) + BigInteger.prototype.gcd = function (a) { + var x = (this.s < 0) ? this.negate() : this.clone(); + var y = (a.s < 0) ? a.negate() : a.clone(); + if (x.compareTo(y) < 0) { + var t = x; + x = y; + y = t; + } + var i = x.getLowestSetBit(), + g = y.getLowestSetBit(); + if (g < 0) return x; + if (i < g) g = i; + if (g > 0) { + x.rShiftTo(g, x); + y.rShiftTo(g, y); + } + while (x.signum() > 0) { + if ((i = x.getLowestSetBit()) > 0) x.rShiftTo(i, x); + if ((i = y.getLowestSetBit()) > 0) y.rShiftTo(i, y); + if (x.compareTo(y) >= 0) { + x.subTo(y, x); + x.rShiftTo(1, x); + } else { + y.subTo(x, y); + y.rShiftTo(1, y); + } + } + if (g > 0) y.lShiftTo(g, y); + return y; + }; + + // (public) test primality with certainty >= 1-.5^t + BigInteger.prototype.isProbablePrime = function (t) { + var i, x = this.abs(); + if (x.t == 1 && x[0] <= lowprimes[lowprimes.length - 1]) { + for (i = 0; i < lowprimes.length; ++i) + if (x[0] == lowprimes[i]) return true; + return false; + } + if (x.isEven()) return false; + i = 1; + while (i < lowprimes.length) { + var m = lowprimes[i], + j = i + 1; + while (j < lowprimes.length && m < lplim) m *= lowprimes[j++]; + m = x.modInt(m); + while (i < j) + if (m % lowprimes[i++] == 0) return false; + } + return x.millerRabin(t); + }; + + + // JSBN-specific extension + + // (public) this^2 + BigInteger.prototype.square = function () { + var r = nbi(); + this.squareTo(r); + return r; + }; + + + // NOTE: BigInteger interfaces not implemented in jsbn: + // BigInteger(int signum, byte[] magnitude) + // double doubleValue() + // float floatValue() + // int hashCode() + // long longValue() + // static BigInteger valueOf(long val) + + + + // Copyright Stephan Thomas (start) --- // + // https://raw.github.com/bitcoinjs/bitcoinjs-lib/07f9d55ccb6abd962efb6befdd37671f85ea4ff9/src/util.js + // BigInteger monkey patching + BigInteger.valueOf = nbv; + + /** + * Returns a byte array representation of the big integer. + * + * This returns the absolute of the contained value in big endian + * form. A value of zero results in an empty array. + */ + BigInteger.prototype.toByteArrayUnsigned = function () { + var ba = this.abs().toByteArray(); + if (ba.length) { + if (ba[0] == 0) { + ba = ba.slice(1); + } + return ba.map(function (v) { + return (v < 0) ? v + 256 : v; + }); + } else { + // Empty array, nothing to do + return ba; + } + }; + + /** + * Turns a byte array into a big integer. + * + * This function will interpret a byte array as a big integer in big + * endian notation and ignore leading zeros. + */ + BigInteger.fromByteArrayUnsigned = function (ba) { + if (!ba.length) { + return ba.valueOf(0); + } else if (ba[0] & 0x80) { + // Prepend a zero so the BigInteger class doesn't mistake this + // for a negative integer. + return new BigInteger([0].concat(ba)); + } else { + return new BigInteger(ba); + } + }; + + /** + * Converts big integer to signed byte representation. + * + * The format for this value uses a the most significant bit as a sign + * bit. If the most significant bit is already occupied by the + * absolute value, an extra byte is prepended and the sign bit is set + * there. + * + * Examples: + * + * 0 => 0x00 + * 1 => 0x01 + * -1 => 0x81 + * 127 => 0x7f + * -127 => 0xff + * 128 => 0x0080 + * -128 => 0x8080 + * 255 => 0x00ff + * -255 => 0x80ff + * 16300 => 0x3fac + * -16300 => 0xbfac + * 62300 => 0x00f35c + * -62300 => 0x80f35c + */ + BigInteger.prototype.toByteArraySigned = function () { + var val = this.abs().toByteArrayUnsigned(); + var neg = this.compareTo(BigInteger.ZERO) < 0; + + if (neg) { + if (val[0] & 0x80) { + val.unshift(0x80); + } else { + val[0] |= 0x80; + } + } else { + if (val[0] & 0x80) { + val.unshift(0x00); + } + } + + return val; + }; + + /** + * Parse a signed big integer byte representation. + * + * For details on the format please see BigInteger.toByteArraySigned. + */ + BigInteger.fromByteArraySigned = function (ba) { + // Check for negative value + if (ba[0] & 0x80) { + // Remove sign bit + ba[0] &= 0x7f; + + return BigInteger.fromByteArrayUnsigned(ba).negate(); + } else { + return BigInteger.fromByteArrayUnsigned(ba); + } + }; + // Copyright Stephan Thomas (end) --- // + + + + + // ****** REDUCTION ******* // + + // Modular reduction using "classic" algorithm + var Classic = GLOBAL.Classic = function Classic(m) { + this.m = m; + } + Classic.prototype.convert = function (x) { + if (x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); + else return x; + }; + Classic.prototype.revert = function (x) { + return x; + }; + Classic.prototype.reduce = function (x) { + x.divRemTo(this.m, null, x); + }; + Classic.prototype.mulTo = function (x, y, r) { + x.multiplyTo(y, r); + this.reduce(r); + }; + Classic.prototype.sqrTo = function (x, r) { + x.squareTo(r); + this.reduce(r); + }; + + + + + + // Montgomery reduction + var Montgomery = GLOBAL.Montgomery = function Montgomery(m) { + this.m = m; + this.mp = m.invDigit(); + this.mpl = this.mp & 0x7fff; + this.mph = this.mp >> 15; + this.um = (1 << (m.DB - 15)) - 1; + this.mt2 = 2 * m.t; + } + // xR mod m + Montgomery.prototype.convert = function (x) { + var r = nbi(); + x.abs().dlShiftTo(this.m.t, r); + r.divRemTo(this.m, null, r); + if (x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r, r); + return r; + } + // x/R mod m + Montgomery.prototype.revert = function (x) { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; + }; + // x = x/R mod m (HAC 14.32) + Montgomery.prototype.reduce = function (x) { + while (x.t <= this.mt2) // pad x so am has enough room later + x[x.t++] = 0; + for (var i = 0; i < this.m.t; ++i) { + // faster way of calculating u0 = x[i]*mp mod DV + var j = x[i] & 0x7fff; + var u0 = (j * this.mpl + (((j * this.mph + (x[i] >> 15) * this.mpl) & this.um) << 15)) & x.DM; + // use am to combine the multiply-shift-add into one call + j = i + this.m.t; + x[j] += this.m.am(0, u0, x, i, 0, this.m.t); + // propagate carry + while (x[j] >= x.DV) { + x[j] -= x.DV; + x[++j]++; + } + } + x.clamp(); + x.drShiftTo(this.m.t, x); + if (x.compareTo(this.m) >= 0) x.subTo(this.m, x); + }; + // r = "xy/R mod m"; x,y != r + Montgomery.prototype.mulTo = function (x, y, r) { + x.multiplyTo(y, r); + this.reduce(r); + }; + // r = "x^2/R mod m"; x != r + Montgomery.prototype.sqrTo = function (x, r) { + x.squareTo(r); + this.reduce(r); + }; + + + + + + // A "null" reducer + var NullExp = GLOBAL.NullExp = function NullExp() { } + NullExp.prototype.convert = function (x) { + return x; + }; + NullExp.prototype.revert = function (x) { + return x; + }; + NullExp.prototype.mulTo = function (x, y, r) { + x.multiplyTo(y, r); + }; + NullExp.prototype.sqrTo = function (x, r) { + x.squareTo(r); + }; + + + + + + // Barrett modular reduction + var Barrett = GLOBAL.Barrett = function Barrett(m) { + // setup Barrett + this.r2 = nbi(); + this.q3 = nbi(); + BigInteger.ONE.dlShiftTo(2 * m.t, this.r2); + this.mu = this.r2.divide(m); + this.m = m; + } + Barrett.prototype.convert = function (x) { + if (x.s < 0 || x.t > 2 * this.m.t) return x.mod(this.m); + else if (x.compareTo(this.m) < 0) return x; + else { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; + } + }; + Barrett.prototype.revert = function (x) { + return x; + }; + // x = x mod m (HAC 14.42) + Barrett.prototype.reduce = function (x) { + x.drShiftTo(this.m.t - 1, this.r2); + if (x.t > this.m.t + 1) { + x.t = this.m.t + 1; + x.clamp(); + } + this.mu.multiplyUpperTo(this.r2, this.m.t + 1, this.q3); + this.m.multiplyLowerTo(this.q3, this.m.t + 1, this.r2); + while (x.compareTo(this.r2) < 0) x.dAddOffset(1, this.m.t + 1); + x.subTo(this.r2, x); + while (x.compareTo(this.m) >= 0) x.subTo(this.m, x); + }; + // r = x*y mod m; x,y != r + Barrett.prototype.mulTo = function (x, y, r) { + x.multiplyTo(y, r); + this.reduce(r); + }; + // r = x^2 mod m; x != r + Barrett.prototype.sqrTo = function (x, r) { + x.squareTo(r); + this.reduce(r); + }; + + // BigInteger interfaces not implemented in jsbn: + + // BigInteger(int signum, byte[] magnitude) + // double doubleValue() + // float floatValue() + // int hashCode() + // long longValue() + // static BigInteger valueOf(long val) + })(); + + //ellipticcurve.js + (function () { + /*! + * Basic Javascript Elliptic Curve implementation + * Ported loosely from BouncyCastle's Java EC code + * Only Fp curves implemented for now + * + * Copyright Tom Wu, bitaddress.org BSD License. + * http://www-cs-students.stanford.edu/~tjw/jsbn/LICENSE + */ + // Constructor function of Global EllipticCurve object + var ec = GLOBAL.EllipticCurve = function () { }; + + // ---------------- + // ECFieldElementFp constructor + // q instanceof BigInteger + // x instanceof BigInteger + ec.FieldElementFp = function (q, x) { + this.x = x; + // TODO if(x.compareTo(q) >= 0) error + this.q = q; + }; + + ec.FieldElementFp.prototype.equals = function (other) { + if (other == this) return true; + return (this.q.equals(other.q) && this.x.equals(other.x)); + }; + + ec.FieldElementFp.prototype.toBigInteger = function () { + return this.x; + }; + + ec.FieldElementFp.prototype.negate = function () { + return new ec.FieldElementFp(this.q, this.x.negate().mod(this.q)); + }; + + ec.FieldElementFp.prototype.add = function (b) { + return new ec.FieldElementFp(this.q, this.x.add(b.toBigInteger()).mod(this.q)); + }; + + ec.FieldElementFp.prototype.subtract = function (b) { + return new ec.FieldElementFp(this.q, this.x.subtract(b.toBigInteger()).mod(this.q)); + }; + + ec.FieldElementFp.prototype.multiply = function (b) { + return new ec.FieldElementFp(this.q, this.x.multiply(b.toBigInteger()).mod(this.q)); + }; + + ec.FieldElementFp.prototype.square = function () { + return new ec.FieldElementFp(this.q, this.x.square().mod(this.q)); + }; + + ec.FieldElementFp.prototype.divide = function (b) { + return new ec.FieldElementFp(this.q, this.x.multiply(b.toBigInteger().modInverse(this.q)).mod( + this.q)); + }; + + ec.FieldElementFp.prototype.getByteLength = function () { + return Math.floor((this.toBigInteger().bitLength() + 7) / 8); + }; + + // D.1.4 91 + /** + * return a sqrt root - the routine verifies that the calculation + * returns the right value - if none exists it returns null. + * + * Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org) + * Ported to JavaScript by bitaddress.org + */ + ec.FieldElementFp.prototype.sqrt = function () { + if (!this.q.testBit(0)) throw new Error("even value of q"); + + // p mod 4 == 3 + if (this.q.testBit(1)) { + // z = g^(u+1) + p, p = 4u + 3 + var z = new ec.FieldElementFp(this.q, this.x.modPow(this.q.shiftRight(2).add(BigInteger.ONE), + this.q)); + return z.square().equals(this) ? z : null; + } + + // p mod 4 == 1 + var qMinusOne = this.q.subtract(BigInteger.ONE); + var legendreExponent = qMinusOne.shiftRight(1); + if (!(this.x.modPow(legendreExponent, this.q).equals(BigInteger.ONE))) return null; + var u = qMinusOne.shiftRight(2); + var k = u.shiftLeft(1).add(BigInteger.ONE); + var Q = this.x; + var fourQ = Q.shiftLeft(2).mod(this.q); + var U, V; + + do { + var rand = new SecureRandom(); + var P; + do { + P = new BigInteger(this.q.bitLength(), rand); + } + while (P.compareTo(this.q) >= 0 || !(P.multiply(P).subtract(fourQ).modPow(legendreExponent, + this.q).equals(qMinusOne))); + + var result = ec.FieldElementFp.fastLucasSequence(this.q, P, Q, k); + + U = result[0]; + V = result[1]; + if (V.multiply(V).mod(this.q).equals(fourQ)) { + // Integer division by 2, mod q + if (V.testBit(0)) { + V = V.add(this.q); + } + V = V.shiftRight(1); + return new ec.FieldElementFp(this.q, V); + } + } + while (U.equals(BigInteger.ONE) || U.equals(qMinusOne)); + + return null; + }; + /*! + * Crypto-JS 2.5.4 BlockModes.js + * contribution from Simon Greatrix + */ + + (function (C) { + + // Create pad namespace + var C_pad = C.pad = {}; + + // Calculate the number of padding bytes required. + function _requiredPadding(cipher, message) { + var blockSizeInBytes = cipher._blocksize * 4; + var reqd = blockSizeInBytes - message.length % blockSizeInBytes; + return reqd; + } + + // Remove padding when the final byte gives the number of padding bytes. + var _unpadLength = function (cipher, message, alg, padding) { + var pad = message.pop(); + if (pad == 0) { + throw new Error("Invalid zero-length padding specified for " + alg + + ". Wrong cipher specification or key used?"); + } + var maxPad = cipher._blocksize * 4; + if (pad > maxPad) { + throw new Error("Invalid padding length of " + pad + + " specified for " + alg + + ". Wrong cipher specification or key used?"); + } + for (var i = 1; i < pad; i++) { + var b = message.pop(); + if (padding != undefined && padding != b) { + throw new Error("Invalid padding byte of 0x" + b.toString(16) + + " specified for " + alg + + ". Wrong cipher specification or key used?"); + } + } + }; + + // No-operation padding, used for stream ciphers + C_pad.NoPadding = { + pad: function (cipher, message) { }, + unpad: function (cipher, message) { } + }; + + // Zero Padding. + // + // If the message is not an exact number of blocks, the final block is + // completed with 0x00 bytes. There is no unpadding. + C_pad.ZeroPadding = { + pad: function (cipher, message) { + var blockSizeInBytes = cipher._blocksize * 4; + var reqd = message.length % blockSizeInBytes; + if (reqd != 0) { + for (reqd = blockSizeInBytes - reqd; reqd > 0; reqd--) { + message.push(0x00); + } + } + }, + + unpad: function (cipher, message) { + while (message[message.length - 1] == 0) { + message.pop(); + } + } + }; + + // ISO/IEC 7816-4 padding. + // + // Pads the plain text with an 0x80 byte followed by as many 0x00 + // bytes are required to complete the block. + C_pad.iso7816 = { + pad: function (cipher, message) { + var reqd = _requiredPadding(cipher, message); + message.push(0x80); + for (; reqd > 1; reqd--) { + message.push(0x00); + } + }, + + unpad: function (cipher, message) { + var padLength; + for (padLength = cipher._blocksize * 4; padLength > 0; padLength--) { + var b = message.pop(); + if (b == 0x80) return; + if (b != 0x00) { + throw new Error("ISO-7816 padding byte must be 0, not 0x" + b.toString(16) + + ". Wrong cipher specification or key used?"); + } + } + throw new Error( + "ISO-7816 padded beyond cipher block size. Wrong cipher specification or key used?" + ); + } + }; + + // ANSI X.923 padding + // + // The final block is padded with zeros except for the last byte of the + // last block which contains the number of padding bytes. + C_pad.ansix923 = { + pad: function (cipher, message) { + var reqd = _requiredPadding(cipher, message); + for (var i = 1; i < reqd; i++) { + message.push(0x00); + } + message.push(reqd); + }, + + unpad: function (cipher, message) { + _unpadLength(cipher, message, "ANSI X.923", 0); + } + }; + + // ISO 10126 + // + // The final block is padded with random bytes except for the last + // byte of the last block which contains the number of padding bytes. + C_pad.iso10126 = { + pad: function (cipher, message) { + var reqd = _requiredPadding(cipher, message); + for (var i = 1; i < reqd; i++) { + message.push(Math.floor(securedMathRandom() * 256)); + } + message.push(reqd); + }, + + unpad: function (cipher, message) { + _unpadLength(cipher, message, "ISO 10126", undefined); + } + }; + + // PKCS7 padding + // + // PKCS7 is described in RFC 5652. Padding is in whole bytes. The + // value of each added byte is the number of bytes that are added, + // i.e. N bytes, each of value N are added. + C_pad.pkcs7 = { + pad: function (cipher, message) { + var reqd = _requiredPadding(cipher, message); + for (var i = 0; i < reqd; i++) { + message.push(reqd); + } + }, + + unpad: function (cipher, message) { + _unpadLength(cipher, message, "PKCS 7", message[message.length - 1]); + } + }; + + // Create mode namespace + var C_mode = C.mode = {}; + + /** + * Mode base "class". + */ + var Mode = C_mode.Mode = function (padding) { + if (padding) { + this._padding = padding; + } + }; + + Mode.prototype = { + encrypt: function (cipher, m, iv) { + this._padding.pad(cipher, m); + this._doEncrypt(cipher, m, iv); + }, + + decrypt: function (cipher, m, iv) { + this._doDecrypt(cipher, m, iv); + this._padding.unpad(cipher, m); + }, + + // Default padding + _padding: C_pad.iso7816 + }; + + + /** + * Electronic Code Book mode. + * + * ECB applies the cipher directly against each block of the input. + * + * ECB does not require an initialization vector. + */ + var ECB = C_mode.ECB = function () { + // Call parent constructor + Mode.apply(this, arguments); + }; + + // Inherit from Mode + var ECB_prototype = ECB.prototype = new Mode; + + // Concrete steps for Mode template + ECB_prototype._doEncrypt = function (cipher, m, iv) { + var blockSizeInBytes = cipher._blocksize * 4; + // Encrypt each block + for (var offset = 0; offset < m.length; offset += blockSizeInBytes) { + cipher._encryptblock(m, offset); + } + }; + ECB_prototype._doDecrypt = function (cipher, c, iv) { + var blockSizeInBytes = cipher._blocksize * 4; + // Decrypt each block + for (var offset = 0; offset < c.length; offset += blockSizeInBytes) { + cipher._decryptblock(c, offset); + } + }; + + // ECB never uses an IV + ECB_prototype.fixOptions = function (options) { + options.iv = []; + }; + + + /** + * Cipher block chaining + * + * The first block is XORed with the IV. Subsequent blocks are XOR with the + * previous cipher output. + */ + var CBC = C_mode.CBC = function () { + // Call parent constructor + Mode.apply(this, arguments); + }; + + // Inherit from Mode + var CBC_prototype = CBC.prototype = new Mode; + + // Concrete steps for Mode template + CBC_prototype._doEncrypt = function (cipher, m, iv) { + var blockSizeInBytes = cipher._blocksize * 4; + + // Encrypt each block + for (var offset = 0; offset < m.length; offset += blockSizeInBytes) { + if (offset == 0) { + // XOR first block using IV + for (var i = 0; i < blockSizeInBytes; i++) + m[i] ^= iv[i]; + } else { + // XOR this block using previous crypted block + for (var i = 0; i < blockSizeInBytes; i++) + m[offset + i] ^= m[offset + i - blockSizeInBytes]; + } + // Encrypt block + cipher._encryptblock(m, offset); + } + }; + CBC_prototype._doDecrypt = function (cipher, c, iv) { + var blockSizeInBytes = cipher._blocksize * 4; + + // At the start, the previously crypted block is the IV + var prevCryptedBlock = iv; + + // Decrypt each block + for (var offset = 0; offset < c.length; offset += blockSizeInBytes) { + // Save this crypted block + var thisCryptedBlock = c.slice(offset, offset + blockSizeInBytes); + // Decrypt block + cipher._decryptblock(c, offset); + // XOR decrypted block using previous crypted block + for (var i = 0; i < blockSizeInBytes; i++) { + c[offset + i] ^= prevCryptedBlock[i]; + } + prevCryptedBlock = thisCryptedBlock; + } + }; + + + /** + * Cipher feed back + * + * The cipher output is XORed with the plain text to produce the cipher output, + * which is then fed back into the cipher to produce a bit pattern to XOR the + * next block with. + * + * This is a stream cipher mode and does not require padding. + */ + var CFB = C_mode.CFB = function () { + // Call parent constructor + Mode.apply(this, arguments); + }; + + // Inherit from Mode + var CFB_prototype = CFB.prototype = new Mode; + + // Override padding + CFB_prototype._padding = C_pad.NoPadding; + + // Concrete steps for Mode template + CFB_prototype._doEncrypt = function (cipher, m, iv) { + var blockSizeInBytes = cipher._blocksize * 4, + keystream = iv.slice(0); + + // Encrypt each byte + for (var i = 0; i < m.length; i++) { + + var j = i % blockSizeInBytes; + if (j == 0) cipher._encryptblock(keystream, 0); + + m[i] ^= keystream[j]; + keystream[j] = m[i]; + } + }; + CFB_prototype._doDecrypt = function (cipher, c, iv) { + var blockSizeInBytes = cipher._blocksize * 4, + keystream = iv.slice(0); + + // Encrypt each byte + for (var i = 0; i < c.length; i++) { + + var j = i % blockSizeInBytes; + if (j == 0) cipher._encryptblock(keystream, 0); + + var b = c[i]; + c[i] ^= keystream[j]; + keystream[j] = b; + } + }; + + + /** + * Output feed back + * + * The cipher repeatedly encrypts its own output. The output is XORed with the + * plain text to produce the cipher text. + * + * This is a stream cipher mode and does not require padding. + */ + var OFB = C_mode.OFB = function () { + // Call parent constructor + Mode.apply(this, arguments); + }; + + // Inherit from Mode + var OFB_prototype = OFB.prototype = new Mode; + + // Override padding + OFB_prototype._padding = C_pad.NoPadding; + + // Concrete steps for Mode template + OFB_prototype._doEncrypt = function (cipher, m, iv) { + + var blockSizeInBytes = cipher._blocksize * 4, + keystream = iv.slice(0); + + // Encrypt each byte + for (var i = 0; i < m.length; i++) { + + // Generate keystream + if (i % blockSizeInBytes == 0) + cipher._encryptblock(keystream, 0); + + // Encrypt byte + m[i] ^= keystream[i % blockSizeInBytes]; + + } + }; + OFB_prototype._doDecrypt = OFB_prototype._doEncrypt; + + /** + * Counter + * @author Gergely Risko + * + * After every block the last 4 bytes of the IV is increased by one + * with carry and that IV is used for the next block. + * + * This is a stream cipher mode and does not require padding. + */ + var CTR = C_mode.CTR = function () { + // Call parent constructor + Mode.apply(this, arguments); + }; + + // Inherit from Mode + var CTR_prototype = CTR.prototype = new Mode; + + // Override padding + CTR_prototype._padding = C_pad.NoPadding; + + CTR_prototype._doEncrypt = function (cipher, m, iv) { + var blockSizeInBytes = cipher._blocksize * 4; + var counter = iv.slice(0); + + for (var i = 0; i < m.length;) { + // do not lose iv + var keystream = counter.slice(0); + + // Generate keystream for next block + cipher._encryptblock(keystream, 0); + + // XOR keystream with block + for (var j = 0; i < m.length && j < blockSizeInBytes; j++, i++) { + m[i] ^= keystream[j]; + } + + // Increase counter + if (++(counter[blockSizeInBytes - 1]) == 256) { + counter[blockSizeInBytes - 1] = 0; + if (++(counter[blockSizeInBytes - 2]) == 256) { + counter[blockSizeInBytes - 2] = 0; + if (++(counter[blockSizeInBytes - 3]) == 256) { + counter[blockSizeInBytes - 3] = 0; + ++(counter[blockSizeInBytes - 4]); + } + } + } + } + }; + CTR_prototype._doDecrypt = CTR_prototype._doEncrypt; + + })(Crypto); + + /*! + * Crypto-JS v2.5.4 PBKDF2.js + * http://code.google.com/p/crypto-js/ + * Copyright (c) 2009-2013, Jeff Mott. All rights reserved. + * http://code.google.com/p/crypto-js/wiki/License + */ + (function () { + + // Shortcuts + var C = Crypto, + util = C.util, + charenc = C.charenc, + UTF8 = charenc.UTF8, + Binary = charenc.Binary; + + C.PBKDF2 = function (password, salt, keylen, options) { + + // Convert to byte arrays + if (password.constructor == String) password = UTF8.stringToBytes(password); + if (salt.constructor == String) salt = UTF8.stringToBytes(salt); + /* else, assume byte arrays already */ + + // Defaults + var hasher = options && options.hasher || C.SHA1, + iterations = options && options.iterations || 1; + + // Pseudo-random function + function PRF(password, salt) { + return C.HMAC(hasher, salt, password, { + asBytes: true + }); + } + + // Generate key + var derivedKeyBytes = [], + blockindex = 1; + while (derivedKeyBytes.length < keylen) { + var block = PRF(password, salt.concat(util.wordsToBytes([blockindex]))); + for (var u = block, i = 1; i < iterations; i++) { + u = PRF(password, u); + for (var j = 0; j < block.length; j++) block[j] ^= u[j]; + } + derivedKeyBytes = derivedKeyBytes.concat(block); + blockindex++; + } + + // Truncate excess bytes + derivedKeyBytes.length = keylen; + + return options && options.asBytes ? derivedKeyBytes : + options && options.asString ? Binary.bytesToString(derivedKeyBytes) : + util.bytesToHex(derivedKeyBytes); + + }; + + })(); + + /* + * Copyright (c) 2010-2011 Intalio Pte, All Rights Reserved + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + // https://github.com/cheongwy/node-scrypt-js + (function () { + + var MAX_VALUE = 2147483647; + var workerUrl = null; + + //function scrypt(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen) + /* + * N = Cpu cost + * r = Memory cost + * p = parallelization cost + * + */ + GLOBAL.Crypto_scrypt = function (passwd, salt, N, r, p, dkLen, callback) { + if (N == 0 || (N & (N - 1)) != 0) throw Error("N must be > 0 and a power of 2"); + + if (N > MAX_VALUE / 128 / r) throw Error("Parameter N is too large"); + if (r > MAX_VALUE / 128 / p) throw Error("Parameter r is too large"); + + var PBKDF2_opts = { + iterations: 1, + hasher: Crypto.SHA256, + asBytes: true + }; + + var B = Crypto.PBKDF2(passwd, salt, p * 128 * r, PBKDF2_opts); + + try { + var i = 0; + var worksDone = 0; + var makeWorker = function () { + if (!workerUrl) { + var code = '(' + scryptCore.toString() + ')()'; + var blob; + try { + blob = new Blob([code], { + type: "text/javascript" + }); + } catch (e) { + GLOBAL.BlobBuilder = GLOBAL.BlobBuilder || GLOBAL.WebKitBlobBuilder || + GLOBAL.MozBlobBuilder || + GLOBAL.MSBlobBuilder; + blob = new BlobBuilder(); + blob.append(code); + blob = blob.getBlob("text/javascript"); + } + workerUrl = URL.createObjectURL(blob); + } + var worker = new Worker(workerUrl); + worker.onmessage = function (event) { + var Bi = event.data[0], + Bslice = event.data[1]; + worksDone++; + + if (i < p) { + worker.postMessage([N, r, p, B, i++]); + } + + var length = Bslice.length, + destPos = Bi * 128 * r, + srcPos = 0; + while (length--) { + B[destPos++] = Bslice[srcPos++]; + } + + if (worksDone == p) { + callback(Crypto.PBKDF2(passwd, B, dkLen, PBKDF2_opts)); + } + }; + return worker; + }; + var workers = [makeWorker(), makeWorker()]; + workers[0].postMessage([N, r, p, B, i++]); + if (p > 1) { + workers[1].postMessage([N, r, p, B, i++]); + } + } catch (e) { + GLOBAL.setTimeout(function () { + scryptCore(); + callback(Crypto.PBKDF2(passwd, B, dkLen, PBKDF2_opts)); + }, 0); + } + + // using this function to enclose everything needed to create a worker (but also invokable directly for synchronous use) + function scryptCore() { + var XY = [], + V = []; + + if (typeof B === 'undefined') { + onmessage = function (event) { + var data = event.data; + var N = data[0], + r = data[1], + p = data[2], + B = data[3], + i = data[4]; + + var Bslice = []; + arraycopy32(B, i * 128 * r, Bslice, 0, 128 * r); + smix(Bslice, 0, r, N, V, XY); + + postMessage([i, Bslice]); + }; + } else { + for (var i = 0; i < p; i++) { + smix(B, i * 128 * r, r, N, V, XY); + } + } + + function smix(B, Bi, r, N, V, XY) { + var Xi = 0; + var Yi = 128 * r; + var i; + + arraycopy32(B, Bi, XY, Xi, Yi); + + for (i = 0; i < N; i++) { + arraycopy32(XY, Xi, V, i * Yi, Yi); + blockmix_salsa8(XY, Xi, Yi, r); + } + + for (i = 0; i < N; i++) { + var j = integerify(XY, Xi, r) & (N - 1); + blockxor(V, j * Yi, XY, Xi, Yi); + blockmix_salsa8(XY, Xi, Yi, r); + } + + arraycopy32(XY, Xi, B, Bi, Yi); + } + + function blockmix_salsa8(BY, Bi, Yi, r) { + var X = []; + var i; + + arraycopy32(BY, Bi + (2 * r - 1) * 64, X, 0, 64); + + for (i = 0; i < 2 * r; i++) { + blockxor(BY, i * 64, X, 0, 64); + salsa20_8(X); + arraycopy32(X, 0, BY, Yi + (i * 64), 64); + } + + for (i = 0; i < r; i++) { + arraycopy32(BY, Yi + (i * 2) * 64, BY, Bi + (i * 64), 64); + } + + for (i = 0; i < r; i++) { + arraycopy32(BY, Yi + (i * 2 + 1) * 64, BY, Bi + (i + r) * 64, 64); + } + } + + function R(a, b) { + return (a << b) | (a >>> (32 - b)); + } + + function salsa20_8(B) { + var B32 = new Array(32); + var x = new Array(32); + var i; + + for (i = 0; i < 16; i++) { + B32[i] = (B[i * 4 + 0] & 0xff) << 0; + B32[i] |= (B[i * 4 + 1] & 0xff) << 8; + B32[i] |= (B[i * 4 + 2] & 0xff) << 16; + B32[i] |= (B[i * 4 + 3] & 0xff) << 24; + } + + arraycopy(B32, 0, x, 0, 16); + + for (i = 8; i > 0; i -= 2) { + x[4] ^= R(x[0] + x[12], 7); + x[8] ^= R(x[4] + x[0], 9); + x[12] ^= R(x[8] + x[4], 13); + x[0] ^= R(x[12] + x[8], 18); + x[9] ^= R(x[5] + x[1], 7); + x[13] ^= R(x[9] + x[5], 9); + x[1] ^= R(x[13] + x[9], 13); + x[5] ^= R(x[1] + x[13], 18); + x[14] ^= R(x[10] + x[6], 7); + x[2] ^= R(x[14] + x[10], 9); + x[6] ^= R(x[2] + x[14], 13); + x[10] ^= R(x[6] + x[2], 18); + x[3] ^= R(x[15] + x[11], 7); + x[7] ^= R(x[3] + x[15], 9); + x[11] ^= R(x[7] + x[3], 13); + x[15] ^= R(x[11] + x[7], 18); + x[1] ^= R(x[0] + x[3], 7); + x[2] ^= R(x[1] + x[0], 9); + x[3] ^= R(x[2] + x[1], 13); + x[0] ^= R(x[3] + x[2], 18); + x[6] ^= R(x[5] + x[4], 7); + x[7] ^= R(x[6] + x[5], 9); + x[4] ^= R(x[7] + x[6], 13); + x[5] ^= R(x[4] + x[7], 18); + x[11] ^= R(x[10] + x[9], 7); + x[8] ^= R(x[11] + x[10], 9); + x[9] ^= R(x[8] + x[11], 13); + x[10] ^= R(x[9] + x[8], 18); + x[12] ^= R(x[15] + x[14], 7); + x[13] ^= R(x[12] + x[15], 9); + x[14] ^= R(x[13] + x[12], 13); + x[15] ^= R(x[14] + x[13], 18); + } + + for (i = 0; i < 16; ++i) B32[i] = x[i] + B32[i]; + + for (i = 0; i < 16; i++) { + var bi = i * 4; + B[bi + 0] = (B32[i] >> 0 & 0xff); + B[bi + 1] = (B32[i] >> 8 & 0xff); + B[bi + 2] = (B32[i] >> 16 & 0xff); + B[bi + 3] = (B32[i] >> 24 & 0xff); + } + } + + function blockxor(S, Si, D, Di, len) { + var i = len >> 6; + while (i--) { + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + D[Di++] ^= S[Si++]; + } + } + + function integerify(B, bi, r) { + var n; + + bi += (2 * r - 1) * 64; + + n = (B[bi + 0] & 0xff) << 0; + n |= (B[bi + 1] & 0xff) << 8; + n |= (B[bi + 2] & 0xff) << 16; + n |= (B[bi + 3] & 0xff) << 24; + + return n; + } + + function arraycopy(src, srcPos, dest, destPos, length) { + while (length--) { + dest[destPos++] = src[srcPos++]; + } + } + + function arraycopy32(src, srcPos, dest, destPos, length) { + var i = length >> 5; + while (i--) { + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + } + } + } // scryptCore + }; // GLOBAL.Crypto_scrypt + })(); + + /*! + * Crypto-JS v2.5.4 AES.js + * http://code.google.com/p/crypto-js/ + * Copyright (c) 2009-2013, Jeff Mott. All rights reserved. + * http://code.google.com/p/crypto-js/wiki/License + */ + (function () { + + // Shortcuts + var C = Crypto, + util = C.util, + charenc = C.charenc, + UTF8 = charenc.UTF8; + + // Precomputed SBOX + var SBOX = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, + 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, + 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, + 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, + 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, + 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, + 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, + 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, + 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, + 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, + 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, + 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, + 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, + 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, + 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, + 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, + 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 + ]; + + // Compute inverse SBOX lookup table + for (var INVSBOX = [], i = 0; i < 256; i++) INVSBOX[SBOX[i]] = i; + + // Compute multiplication in GF(2^8) lookup tables + var MULT2 = [], + MULT3 = [], + MULT9 = [], + MULTB = [], + MULTD = [], + MULTE = []; + + function xtime(a, b) { + for (var result = 0, i = 0; i < 8; i++) { + if (b & 1) result ^= a; + var hiBitSet = a & 0x80; + a = (a << 1) & 0xFF; + if (hiBitSet) a ^= 0x1b; + b >>>= 1; + } + return result; + } + + for (var i = 0; i < 256; i++) { + MULT2[i] = xtime(i, 2); + MULT3[i] = xtime(i, 3); + MULT9[i] = xtime(i, 9); + MULTB[i] = xtime(i, 0xB); + MULTD[i] = xtime(i, 0xD); + MULTE[i] = xtime(i, 0xE); + } + + // Precomputed RCon lookup + var RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; + + // Inner state + var state = [ + [], + [], + [], + [] + ], + keylength, + nrounds, + keyschedule; + + var AES = C.AES = { + + /** + * Public API + */ + + encrypt: function (message, password, options) { + + options = options || {}; + + // Determine mode + var mode = options.mode || new C.mode.OFB; + + // Allow mode to override options + if (mode.fixOptions) mode.fixOptions(options); + + var + + // Convert to bytes if message is a string + m = ( + message.constructor == String ? + UTF8.stringToBytes(message) : + message + ), + + // Generate random IV + iv = options.iv || util.randomBytes(AES._blocksize * 4), + + // Generate key + k = ( + password.constructor == String ? + // Derive key from pass-phrase + C.PBKDF2(password, iv, 32, { + asBytes: true + }) : + // else, assume byte array representing cryptographic key + password + ); + + // Encrypt + AES._init(k); + mode.encrypt(AES, m, iv); + + // Return ciphertext + m = options.iv ? m : iv.concat(m); + return (options && options.asBytes) ? m : util.bytesToBase64(m); + + }, + + decrypt: function (ciphertext, password, options) { + + options = options || {}; + + // Determine mode + var mode = options.mode || new C.mode.OFB; + + // Allow mode to override options + if (mode.fixOptions) mode.fixOptions(options); + + var + + // Convert to bytes if ciphertext is a string + c = ( + ciphertext.constructor == String ? + util.base64ToBytes(ciphertext) : + ciphertext + ), + + // Separate IV and message + iv = options.iv || c.splice(0, AES._blocksize * 4), + + // Generate key + k = ( + password.constructor == String ? + // Derive key from pass-phrase + C.PBKDF2(password, iv, 32, { + asBytes: true + }) : + // else, assume byte array representing cryptographic key + password + ); + + // Decrypt + AES._init(k); + mode.decrypt(AES, c, iv); + + // Return plaintext + return (options && options.asBytes) ? c : UTF8.bytesToString(c); + + }, + + + /** + * Package private methods and properties + */ + + _blocksize: 4, + + _encryptblock: function (m, offset) { + + // Set input + for (var row = 0; row < AES._blocksize; row++) { + for (var col = 0; col < 4; col++) + state[row][col] = m[offset + col * 4 + row]; + } + + // Add round key + for (var row = 0; row < 4; row++) { + for (var col = 0; col < 4; col++) + state[row][col] ^= keyschedule[col][row]; + } + + for (var round = 1; round < nrounds; round++) { + + // Sub bytes + for (var row = 0; row < 4; row++) { + for (var col = 0; col < 4; col++) + state[row][col] = SBOX[state[row][col]]; + } + + // Shift rows + state[1].push(state[1].shift()); + state[2].push(state[2].shift()); + state[2].push(state[2].shift()); + state[3].unshift(state[3].pop()); + + // Mix columns + for (var col = 0; col < 4; col++) { + + var s0 = state[0][col], + s1 = state[1][col], + s2 = state[2][col], + s3 = state[3][col]; + + state[0][col] = MULT2[s0] ^ MULT3[s1] ^ s2 ^ s3; + state[1][col] = s0 ^ MULT2[s1] ^ MULT3[s2] ^ s3; + state[2][col] = s0 ^ s1 ^ MULT2[s2] ^ MULT3[s3]; + state[3][col] = MULT3[s0] ^ s1 ^ s2 ^ MULT2[s3]; + + } + + // Add round key + for (var row = 0; row < 4; row++) { + for (var col = 0; col < 4; col++) + state[row][col] ^= keyschedule[round * 4 + col][row]; + } + + } + + // Sub bytes + for (var row = 0; row < 4; row++) { + for (var col = 0; col < 4; col++) + state[row][col] = SBOX[state[row][col]]; + } + + // Shift rows + state[1].push(state[1].shift()); + state[2].push(state[2].shift()); + state[2].push(state[2].shift()); + state[3].unshift(state[3].pop()); + + // Add round key + for (var row = 0; row < 4; row++) { + for (var col = 0; col < 4; col++) + state[row][col] ^= keyschedule[nrounds * 4 + col][row]; + } + + // Set output + for (var row = 0; row < AES._blocksize; row++) { + for (var col = 0; col < 4; col++) + m[offset + col * 4 + row] = state[row][col]; + } + + }, + + _decryptblock: function (c, offset) { + + // Set input + for (var row = 0; row < AES._blocksize; row++) { + for (var col = 0; col < 4; col++) + state[row][col] = c[offset + col * 4 + row]; + } + + // Add round key + for (var row = 0; row < 4; row++) { + for (var col = 0; col < 4; col++) + state[row][col] ^= keyschedule[nrounds * 4 + col][row]; + } + + for (var round = 1; round < nrounds; round++) { + + // Inv shift rows + state[1].unshift(state[1].pop()); + state[2].push(state[2].shift()); + state[2].push(state[2].shift()); + state[3].push(state[3].shift()); + + // Inv sub bytes + for (var row = 0; row < 4; row++) { + for (var col = 0; col < 4; col++) + state[row][col] = INVSBOX[state[row][col]]; + } + + // Add round key + for (var row = 0; row < 4; row++) { + for (var col = 0; col < 4; col++) + state[row][col] ^= keyschedule[(nrounds - round) * 4 + col][row]; + } + + // Inv mix columns + for (var col = 0; col < 4; col++) { + + var s0 = state[0][col], + s1 = state[1][col], + s2 = state[2][col], + s3 = state[3][col]; + + state[0][col] = MULTE[s0] ^ MULTB[s1] ^ MULTD[s2] ^ MULT9[s3]; + state[1][col] = MULT9[s0] ^ MULTE[s1] ^ MULTB[s2] ^ MULTD[s3]; + state[2][col] = MULTD[s0] ^ MULT9[s1] ^ MULTE[s2] ^ MULTB[s3]; + state[3][col] = MULTB[s0] ^ MULTD[s1] ^ MULT9[s2] ^ MULTE[s3]; + + } + + } + + // Inv shift rows + state[1].unshift(state[1].pop()); + state[2].push(state[2].shift()); + state[2].push(state[2].shift()); + state[3].push(state[3].shift()); + + // Inv sub bytes + for (var row = 0; row < 4; row++) { + for (var col = 0; col < 4; col++) + state[row][col] = INVSBOX[state[row][col]]; + } + + // Add round key + for (var row = 0; row < 4; row++) { + for (var col = 0; col < 4; col++) + state[row][col] ^= keyschedule[col][row]; + } + + // Set output + for (var row = 0; row < AES._blocksize; row++) { + for (var col = 0; col < 4; col++) + c[offset + col * 4 + row] = state[row][col]; + } + + }, + + + /** + * Private methods + */ + + _init: function (k) { + keylength = k.length / 4; + nrounds = keylength + 6; + AES._keyexpansion(k); + }, + + // Generate a key schedule + _keyexpansion: function (k) { + + keyschedule = []; + + for (var row = 0; row < keylength; row++) { + keyschedule[row] = [ + k[row * 4], + k[row * 4 + 1], + k[row * 4 + 2], + k[row * 4 + 3] + ]; + } + + for (var row = keylength; row < AES._blocksize * (nrounds + 1); row++) { + + var temp = [ + keyschedule[row - 1][0], + keyschedule[row - 1][1], + keyschedule[row - 1][2], + keyschedule[row - 1][3] + ]; + + if (row % keylength == 0) { + + // Rot word + temp.push(temp.shift()); + + // Sub word + temp[0] = SBOX[temp[0]]; + temp[1] = SBOX[temp[1]]; + temp[2] = SBOX[temp[2]]; + temp[3] = SBOX[temp[3]]; + + temp[0] ^= RCON[row / keylength]; + + } else if (keylength > 6 && row % keylength == 4) { + + // Sub word + temp[0] = SBOX[temp[0]]; + temp[1] = SBOX[temp[1]]; + temp[2] = SBOX[temp[2]]; + temp[3] = SBOX[temp[3]]; + + } + + keyschedule[row] = [ + keyschedule[row - keylength][0] ^ temp[0], + keyschedule[row - keylength][1] ^ temp[1], + keyschedule[row - keylength][2] ^ temp[2], + keyschedule[row - keylength][3] ^ temp[3] + ]; + + } + + } + + }; + + })(); + + /* + * Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org) + * Ported to JavaScript by bitaddress.org + */ + ec.FieldElementFp.fastLucasSequence = function (p, P, Q, k) { + // TODO Research and apply "common-multiplicand multiplication here" + + var n = k.bitLength(); + var s = k.getLowestSetBit(); + var Uh = BigInteger.ONE; + var Vl = BigInteger.TWO; + var Vh = P; + var Ql = BigInteger.ONE; + var Qh = BigInteger.ONE; + + for (var j = n - 1; j >= s + 1; --j) { + Ql = Ql.multiply(Qh).mod(p); + if (k.testBit(j)) { + Qh = Ql.multiply(Q).mod(p); + Uh = Uh.multiply(Vh).mod(p); + Vl = Vh.multiply(Vl).subtract(P.multiply(Ql)).mod(p); + Vh = Vh.multiply(Vh).subtract(Qh.shiftLeft(1)).mod(p); + } else { + Qh = Ql; + Uh = Uh.multiply(Vl).subtract(Ql).mod(p); + Vh = Vh.multiply(Vl).subtract(P.multiply(Ql)).mod(p); + Vl = Vl.multiply(Vl).subtract(Ql.shiftLeft(1)).mod(p); + } + } + + Ql = Ql.multiply(Qh).mod(p); + Qh = Ql.multiply(Q).mod(p); + Uh = Uh.multiply(Vl).subtract(Ql).mod(p); + Vl = Vh.multiply(Vl).subtract(P.multiply(Ql)).mod(p); + Ql = Ql.multiply(Qh).mod(p); + + for (var j = 1; j <= s; ++j) { + Uh = Uh.multiply(Vl).mod(p); + Vl = Vl.multiply(Vl).subtract(Ql.shiftLeft(1)).mod(p); + Ql = Ql.multiply(Ql).mod(p); + } + + return [Uh, Vl]; + }; + + // ---------------- + // ECPointFp constructor + ec.PointFp = function (curve, x, y, z, compressed) { + this.curve = curve; + this.x = x; + this.y = y; + // Projective coordinates: either zinv == null or z * zinv == 1 + // z and zinv are just BigIntegers, not fieldElements + if (z == null) { + this.z = BigInteger.ONE; + } else { + this.z = z; + } + this.zinv = null; + // compression flag + this.compressed = !!compressed; + }; + + ec.PointFp.prototype.getX = function () { + if (this.zinv == null) { + this.zinv = this.z.modInverse(this.curve.q); + } + var r = this.x.toBigInteger().multiply(this.zinv); + this.curve.reduce(r); + return this.curve.fromBigInteger(r); + }; + + ec.PointFp.prototype.getY = function () { + if (this.zinv == null) { + this.zinv = this.z.modInverse(this.curve.q); + } + var r = this.y.toBigInteger().multiply(this.zinv); + this.curve.reduce(r); + return this.curve.fromBigInteger(r); + }; + + ec.PointFp.prototype.equals = function (other) { + if (other == this) return true; + if (this.isInfinity()) return other.isInfinity(); + if (other.isInfinity()) return this.isInfinity(); + var u, v; + // u = Y2 * Z1 - Y1 * Z2 + u = other.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(other.z)).mod( + this.curve.q); + if (!u.equals(BigInteger.ZERO)) return false; + // v = X2 * Z1 - X1 * Z2 + v = other.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(other.z)).mod( + this.curve.q); + return v.equals(BigInteger.ZERO); + }; + + ec.PointFp.prototype.isInfinity = function () { + if ((this.x == null) && (this.y == null)) return true; + return this.z.equals(BigInteger.ZERO) && !this.y.toBigInteger().equals(BigInteger.ZERO); + }; + + ec.PointFp.prototype.negate = function () { + return new ec.PointFp(this.curve, this.x, this.y.negate(), this.z); + }; + + ec.PointFp.prototype.add = function (b) { + if (this.isInfinity()) return b; + if (b.isInfinity()) return this; + + // u = Y2 * Z1 - Y1 * Z2 + var u = b.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(b.z)).mod( + this.curve.q); + // v = X2 * Z1 - X1 * Z2 + var v = b.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(b.z)).mod( + this.curve.q); + + + if (BigInteger.ZERO.equals(v)) { + if (BigInteger.ZERO.equals(u)) { + return this.twice(); // this == b, so double + } + return this.curve.getInfinity(); // this = -b, so infinity + } + + var THREE = new BigInteger("3"); + var x1 = this.x.toBigInteger(); + var y1 = this.y.toBigInteger(); + var x2 = b.x.toBigInteger(); + var y2 = b.y.toBigInteger(); + + var v2 = v.square(); + var v3 = v2.multiply(v); + var x1v2 = x1.multiply(v2); + var zu2 = u.square().multiply(this.z); + + // x3 = v * (z2 * (z1 * u^2 - 2 * x1 * v^2) - v^3) + var x3 = zu2.subtract(x1v2.shiftLeft(1)).multiply(b.z).subtract(v3).multiply(v).mod(this.curve.q); + // y3 = z2 * (3 * x1 * u * v^2 - y1 * v^3 - z1 * u^3) + u * v^3 + var y3 = x1v2.multiply(THREE).multiply(u).subtract(y1.multiply(v3)).subtract(zu2.multiply(u)).multiply( + b.z).add(u.multiply(v3)).mod(this.curve.q); + // z3 = v^3 * z1 * z2 + var z3 = v3.multiply(this.z).multiply(b.z).mod(this.curve.q); + + return new ec.PointFp(this.curve, this.curve.fromBigInteger(x3), this.curve.fromBigInteger(y3), + z3); + }; + + ec.PointFp.prototype.twice = function () { + if (this.isInfinity()) return this; + if (this.y.toBigInteger().signum() == 0) return this.curve.getInfinity(); + + // TODO: optimized handling of constants + var THREE = new BigInteger("3"); + var x1 = this.x.toBigInteger(); + var y1 = this.y.toBigInteger(); + + var y1z1 = y1.multiply(this.z); + var y1sqz1 = y1z1.multiply(y1).mod(this.curve.q); + var a = this.curve.a.toBigInteger(); + + // w = 3 * x1^2 + a * z1^2 + var w = x1.square().multiply(THREE); + if (!BigInteger.ZERO.equals(a)) { + w = w.add(this.z.square().multiply(a)); + } + w = w.mod(this.curve.q); + //this.curve.reduce(w); + // x3 = 2 * y1 * z1 * (w^2 - 8 * x1 * y1^2 * z1) + var x3 = w.square().subtract(x1.shiftLeft(3).multiply(y1sqz1)).shiftLeft(1).multiply(y1z1).mod( + this.curve.q); + // y3 = 4 * y1^2 * z1 * (3 * w * x1 - 2 * y1^2 * z1) - w^3 + var y3 = w.multiply(THREE).multiply(x1).subtract(y1sqz1.shiftLeft(1)).shiftLeft(2).multiply( + y1sqz1).subtract(w.square().multiply(w)).mod(this.curve.q); + // z3 = 8 * (y1 * z1)^3 + var z3 = y1z1.square().multiply(y1z1).shiftLeft(3).mod(this.curve.q); + + return new ec.PointFp(this.curve, this.curve.fromBigInteger(x3), this.curve.fromBigInteger(y3), + z3); + }; + + // Simple NAF (Non-Adjacent Form) multiplication algorithm + // TODO: modularize the multiplication algorithm + ec.PointFp.prototype.multiply = function (k) { + if (this.isInfinity()) return this; + if (k.signum() == 0) return this.curve.getInfinity(); + + var e = k; + var h = e.multiply(new BigInteger("3")); + + var neg = this.negate(); + var R = this; + + var i; + for (i = h.bitLength() - 2; i > 0; --i) { + R = R.twice(); + + var hBit = h.testBit(i); + var eBit = e.testBit(i); + + if (hBit != eBit) { + R = R.add(hBit ? this : neg); + } + } + + return R; + }; + + // Compute this*j + x*k (simultaneous multiplication) + ec.PointFp.prototype.multiplyTwo = function (j, x, k) { + var i; + if (j.bitLength() > k.bitLength()) + i = j.bitLength() - 1; + else + i = k.bitLength() - 1; + + var R = this.curve.getInfinity(); + var both = this.add(x); + while (i >= 0) { + R = R.twice(); + if (j.testBit(i)) { + if (k.testBit(i)) { + R = R.add(both); + } else { + R = R.add(this); + } + } else { + if (k.testBit(i)) { + R = R.add(x); + } + } + --i; + } + + return R; + }; + + // patched by bitaddress.org and Casascius for use with Bitcoin.ECKey + // patched by coretechs to support compressed public keys + ec.PointFp.prototype.getEncoded = function (compressed) { + var x = this.getX().toBigInteger(); + var y = this.getY().toBigInteger(); + var len = 32; // integerToBytes will zero pad if integer is less than 32 bytes. 32 bytes length is required by the Bitcoin protocol. + var enc = ec.integerToBytes(x, len); + + // when compressed prepend byte depending if y point is even or odd + if (compressed) { + if (y.isEven()) { + enc.unshift(0x02); + } else { + enc.unshift(0x03); + } + } else { + enc.unshift(0x04); + enc = enc.concat(ec.integerToBytes(y, len)); // uncompressed public key appends the bytes of the y point + } + return enc; + }; + + ec.PointFp.decodeFrom = function (curve, enc) { + var type = enc[0]; + var dataLen = enc.length - 1; + + // Extract x and y as byte arrays + var xBa = enc.slice(1, 1 + dataLen / 2); + var yBa = enc.slice(1 + dataLen / 2, 1 + dataLen); + + // Prepend zero byte to prevent interpretation as negative integer + xBa.unshift(0); + yBa.unshift(0); + + // Convert to BigIntegers + var x = new BigInteger(xBa); + var y = new BigInteger(yBa); + + // Return point + return new ec.PointFp(curve, curve.fromBigInteger(x), curve.fromBigInteger(y)); + }; + + ec.PointFp.prototype.add2D = function (b) { + if (this.isInfinity()) return b; + if (b.isInfinity()) return this; + + if (this.x.equals(b.x)) { + if (this.y.equals(b.y)) { + // this = b, i.e. this must be doubled + return this.twice(); + } + // this = -b, i.e. the result is the point at infinity + return this.curve.getInfinity(); + } + + var x_x = b.x.subtract(this.x); + var y_y = b.y.subtract(this.y); + var gamma = y_y.divide(x_x); + + var x3 = gamma.square().subtract(this.x).subtract(b.x); + var y3 = gamma.multiply(this.x.subtract(x3)).subtract(this.y); + + return new ec.PointFp(this.curve, x3, y3); + }; + + ec.PointFp.prototype.twice2D = function () { + if (this.isInfinity()) return this; + if (this.y.toBigInteger().signum() == 0) { + // if y1 == 0, then (x1, y1) == (x1, -y1) + // and hence this = -this and thus 2(x1, y1) == infinity + return this.curve.getInfinity(); + } + + var TWO = this.curve.fromBigInteger(BigInteger.valueOf(2)); + var THREE = this.curve.fromBigInteger(BigInteger.valueOf(3)); + var gamma = this.x.square().multiply(THREE).add(this.curve.a).divide(this.y.multiply(TWO)); + + var x3 = gamma.square().subtract(this.x.multiply(TWO)); + var y3 = gamma.multiply(this.x.subtract(x3)).subtract(this.y); + + return new ec.PointFp(this.curve, x3, y3); + }; + + ec.PointFp.prototype.multiply2D = function (k) { + if (this.isInfinity()) return this; + if (k.signum() == 0) return this.curve.getInfinity(); + + var e = k; + var h = e.multiply(new BigInteger("3")); + + var neg = this.negate(); + var R = this; + + var i; + for (i = h.bitLength() - 2; i > 0; --i) { + R = R.twice(); + + var hBit = h.testBit(i); + var eBit = e.testBit(i); + + if (hBit != eBit) { + R = R.add2D(hBit ? this : neg); + } + } + + return R; + }; + + ec.PointFp.prototype.isOnCurve = function () { + var x = this.getX().toBigInteger(); + var y = this.getY().toBigInteger(); + var a = this.curve.getA().toBigInteger(); + var b = this.curve.getB().toBigInteger(); + var n = this.curve.getQ(); + var lhs = y.multiply(y).mod(n); + var rhs = x.multiply(x).multiply(x).add(a.multiply(x)).add(b).mod(n); + return lhs.equals(rhs); + }; + + ec.PointFp.prototype.toString = function () { + return '(' + this.getX().toBigInteger().toString() + ',' + this.getY().toBigInteger().toString() + + ')'; + }; + + /** + * Validate an elliptic curve point. + * + * See SEC 1, section 3.2.2.1: Elliptic Curve Public Key Validation Primitive + */ + ec.PointFp.prototype.validate = function () { + var n = this.curve.getQ(); + + // Check Q != O + if (this.isInfinity()) { + throw new Error("Point is at infinity."); + } + + // Check coordinate bounds + var x = this.getX().toBigInteger(); + var y = this.getY().toBigInteger(); + if (x.compareTo(BigInteger.ONE) < 0 || x.compareTo(n.subtract(BigInteger.ONE)) > 0) { + throw new Error('x coordinate out of bounds'); + } + if (y.compareTo(BigInteger.ONE) < 0 || y.compareTo(n.subtract(BigInteger.ONE)) > 0) { + throw new Error('y coordinate out of bounds'); + } + + // Check y^2 = x^3 + ax + b (mod n) + if (!this.isOnCurve()) { + throw new Error("Point is not on the curve."); + } + + // Check nQ = 0 (Q is a scalar multiple of G) + if (this.multiply(n).isInfinity()) { + // TODO: This check doesn't work - fix. + throw new Error("Point is not a scalar multiple of G."); + } + + return true; + }; + + + + + // ---------------- + // ECCurveFp constructor + ec.CurveFp = function (q, a, b) { + this.q = q; + this.a = this.fromBigInteger(a); + this.b = this.fromBigInteger(b); + this.infinity = new ec.PointFp(this, null, null); + this.reducer = new Barrett(this.q); + } + + ec.CurveFp.prototype.getQ = function () { + return this.q; + }; + + ec.CurveFp.prototype.getA = function () { + return this.a; + }; + + ec.CurveFp.prototype.getB = function () { + return this.b; + }; + + ec.CurveFp.prototype.equals = function (other) { + if (other == this) return true; + return (this.q.equals(other.q) && this.a.equals(other.a) && this.b.equals(other.b)); + }; + + ec.CurveFp.prototype.getInfinity = function () { + return this.infinity; + }; + + ec.CurveFp.prototype.fromBigInteger = function (x) { + return new ec.FieldElementFp(this.q, x); + }; + + ec.CurveFp.prototype.reduce = function (x) { + this.reducer.reduce(x); + }; + + // for now, work with hex strings because they're easier in JS + // compressed support added by bitaddress.org + ec.CurveFp.prototype.decodePointHex = function (s) { + var firstByte = parseInt(s.substr(0, 2), 16); + switch (firstByte) { // first byte + case 0: + return this.infinity; + case 2: // compressed + case 3: // compressed + var yTilde = firstByte & 1; + var xHex = s.substr(2, s.length - 2); + var X1 = new BigInteger(xHex, 16); + return this.decompressPoint(yTilde, X1); + case 4: // uncompressed + case 6: // hybrid + case 7: // hybrid + var len = (s.length - 2) / 2; + var xHex = s.substr(2, len); + var yHex = s.substr(len + 2, len); + + return new ec.PointFp(this, + this.fromBigInteger(new BigInteger(xHex, 16)), + this.fromBigInteger(new BigInteger(yHex, 16))); + + default: // unsupported + return null; + } + }; + + ec.CurveFp.prototype.encodePointHex = function (p) { + if (p.isInfinity()) return "00"; + var xHex = p.getX().toBigInteger().toString(16); + var yHex = p.getY().toBigInteger().toString(16); + var oLen = this.getQ().toString(16).length; + if ((oLen % 2) != 0) oLen++; + while (xHex.length < oLen) { + xHex = "0" + xHex; + } + while (yHex.length < oLen) { + yHex = "0" + yHex; + } + return "04" + xHex + yHex; + }; + + /* + * Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org) + * Ported to JavaScript by bitaddress.org + * + * Number yTilde + * BigInteger X1 + */ + ec.CurveFp.prototype.decompressPoint = function (yTilde, X1) { + var x = this.fromBigInteger(X1); + var alpha = x.multiply(x.square().add(this.getA())).add(this.getB()); + var beta = alpha.sqrt(); + // if we can't find a sqrt we haven't got a point on the curve - run! + if (beta == null) throw new Error("Invalid point compression"); + var betaValue = beta.toBigInteger(); + var bit0 = betaValue.testBit(0) ? 1 : 0; + if (bit0 != yTilde) { + // Use the other root + beta = this.fromBigInteger(this.getQ().subtract(betaValue)); + } + return new ec.PointFp(this, x, beta, null, true); + }; + + + ec.fromHex = function (s) { + return new BigInteger(s, 16); + }; + + ec.integerToBytes = function (i, len) { + var bytes = i.toByteArrayUnsigned(); + if (len < bytes.length) { + bytes = bytes.slice(bytes.length - len); + } else + while (len > bytes.length) { + bytes.unshift(0); + } + return bytes; + }; + + + // Named EC curves + // ---------------- + // X9ECParameters constructor + ec.X9Parameters = function (curve, g, n, h) { + this.curve = curve; + this.g = g; + this.n = n; + this.h = h; + } + ec.X9Parameters.prototype.getCurve = function () { + return this.curve; + }; + ec.X9Parameters.prototype.getG = function () { + return this.g; + }; + ec.X9Parameters.prototype.getN = function () { + return this.n; + }; + ec.X9Parameters.prototype.getH = function () { + return this.h; + }; + + // secp256k1 is the Curve used by Bitcoin + ec.secNamedCurves = { + // used by Bitcoin + "secp256k1": function () { + // p = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1 + var p = ec.fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"); + var a = BigInteger.ZERO; + var b = ec.fromHex("7"); + var n = ec.fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"); + var h = BigInteger.ONE; + var curve = new ec.CurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" + + "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"); + return new ec.X9Parameters(curve, G, n, h); + } + }; + + // secp256k1 called by Bitcoin's ECKEY + ec.getSECCurveByName = function (name) { + if (ec.secNamedCurves[name] == undefined) return null; + return ec.secNamedCurves[name](); + } + })(); + + //bitTrx.js + (function () { + + var bitjs = GLOBAL.bitjs = function () { }; + + /* public vars */ + bitjs.pub = 0x23; // flochange - changed the prefix to FLO Mainnet PublicKey Prefix 0x23 + bitjs.priv = 0xa3; //flochange - changed the prefix to FLO Mainnet Private key prefix 0xa3 + bitjs.multisig = 0x5e; //flochange - prefix for FLO Mainnet Multisig 0x5e + bitjs.compressed = false; + + if (GLOBAL.cryptocoin == 'FLO_TEST') { + bitjs.pub = 0x73; // flochange - changed the prefix to FLO TestNet PublicKey Prefix 0x73 + bitjs.priv = 0xa3; //flochange - changed the prefix to FLO TestNet Private key prefix 0xa3 + bitjs.multisig = 0xc6; //flochange - prefix for FLO TestNet Multisig 0xc6 + } + + /* provide a privkey and return an WIF */ + bitjs.privkey2wif = function (h) { + var r = Crypto.util.hexToBytes(h); + + if (bitjs.compressed == true) { + r.push(0x01); + } + + r.unshift(bitjs.priv); + var hash = Crypto.SHA256(Crypto.SHA256(r, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + + return B58.encode(r.concat(checksum)); + } + + /* convert a wif key back to a private key */ + bitjs.wif2privkey = function (wif) { + var compressed = false; + var decode = B58.decode(wif); + var key = decode.slice(0, decode.length - 4); + key = key.slice(1, key.length); + if (key.length >= 33 && key[key.length - 1] == 0x01) { + key = key.slice(0, key.length - 1); + compressed = true; + } + return { + 'privkey': Crypto.util.bytesToHex(key), + 'compressed': compressed + }; + } + + /* convert a wif to a pubkey */ + bitjs.wif2pubkey = function (wif) { + var compressed = bitjs.compressed; + var r = bitjs.wif2privkey(wif); + bitjs.compressed = r['compressed']; + var pubkey = bitjs.newPubkey(r['privkey']); + bitjs.compressed = compressed; + return { + 'pubkey': pubkey, + 'compressed': r['compressed'] + }; + } + + /* convert a wif to a address */ + bitjs.wif2address = function (wif) { + var r = bitjs.wif2pubkey(wif); + return { + 'address': bitjs.pubkey2address(r['pubkey']), + 'compressed': r['compressed'] + }; + } + + /* generate a public key from a private key */ + bitjs.newPubkey = function (hash) { + var privateKeyBigInt = BigInteger.fromByteArrayUnsigned(Crypto.util.hexToBytes(hash)); + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + + var curvePt = curve.getG().multiply(privateKeyBigInt); + var x = curvePt.getX().toBigInteger(); + var y = curvePt.getY().toBigInteger(); + + var publicKeyBytes = EllipticCurve.integerToBytes(x, 32); + publicKeyBytes = publicKeyBytes.concat(EllipticCurve.integerToBytes(y, 32)); + publicKeyBytes.unshift(0x04); + + if (bitjs.compressed == true) { + var publicKeyBytesCompressed = EllipticCurve.integerToBytes(x, 32) + if (y.isEven()) { + publicKeyBytesCompressed.unshift(0x02) + } else { + publicKeyBytesCompressed.unshift(0x03) + } + return Crypto.util.bytesToHex(publicKeyBytesCompressed); + } else { + return Crypto.util.bytesToHex(publicKeyBytes); + } + } + + /* provide a public key and return address */ + bitjs.pubkey2address = function (h, byte) { + var r = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(h), { + asBytes: true + })); + r.unshift(byte || bitjs.pub); + var hash = Crypto.SHA256(Crypto.SHA256(r, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + return B58.encode(r.concat(checksum)); + } + + /* generate a multisig address from pubkeys and required signatures */ + bitjs.pubkeys2multisig = function (pubkeys, required) { + var s = []; + s.push(80 + required); //OP_1 + for (var i = 0; i < pubkeys.length; ++i) { + let bytes = Crypto.util.hexToBytes(pubkeys[i]); + s.push(bytes.length); + s = s.concat(bytes); + } + s.push(80 + pubkeys.length); //OP_1 + s.push(174); //OP_CHECKMULTISIG + + if (s.length > 520) { // too large + throw Error(`redeemScript size(=${s.length}) too large`) + } + + var x = ripemd160(Crypto.SHA256(s, { + asBytes: true + }), { + asBytes: true + }); + x.unshift(bitjs.multisig); + var r = x; + r = Crypto.SHA256(Crypto.SHA256(r, { + asBytes: true + }), { + asBytes: true + }); + var checksum = r.slice(0, 4); + var redeemScript = Crypto.util.bytesToHex(s); + var address = B58.encode(x.concat(checksum)); + + return { + 'address': address, + 'redeemScript': redeemScript, + 'size': s.length + }; + } + + bitjs.transaction = function (tx_data = undefined) { + var btrx = {}; + btrx.version = 2; //flochange look at this version + btrx.inputs = []; + btrx.outputs = []; + btrx.locktime = 0; + btrx.floData = ""; //flochange .. look at this + + + btrx.addinput = function (txid, index, scriptPubKey, sequence) { + var o = {}; + o.outpoint = { + 'hash': txid, + 'index': index + }; + o.script = Crypto.util.hexToBytes(scriptPubKey); //push previous output pubkey script + o.sequence = sequence || ((btrx.locktime == 0) ? 4294967295 : 0); + return this.inputs.push(o); + } + + btrx.addoutput = function (address, value) { + var o = {}; + var buf = []; + var addr = this.addressDecode(address); + o.value = new BigInteger('' + Math.round((value * 1) * 1e8), 10); + + if (addr.version === bitjs.pub) { // regular address + buf.push(118); //OP_DUP + buf.push(169); //OP_HASH160 + buf = this.writeBytesToScriptBuffer(buf, addr.bytes);// address in bytes + buf.push(136); //OP_EQUALVERIFY + buf.push(172); //OP_CHECKSIG + } else if (addr.version === bitjs.multisig) { // multisig address + buf.push(169); //OP_HASH160 + buf = this.writeBytesToScriptBuffer(buf, addr.bytes);// address in bytes + buf.push(135); //OP_EQUAL + } + + o.script = buf; + return this.outputs.push(o); + } + + // flochange - Added fn to assign flodata to tx + btrx.addflodata = function (data) { + //checks for valid flo-data string + if (typeof data !== "string") + throw Error("floData should be String"); + if (data.length > 1040) + throw Error("floData Character Limit Exceeded"); + if (bitjs.strToBytes(data).some(c => c < 32 || c > 127)) + throw Error("floData contains Invalid characters (only ASCII characters allowed"); + + this.floData = data; + return this.floData; + } + + + // Only standard addresses (standard multisig supported) + btrx.addressDecode = function (address) { + var bytes = B58.decode(address); + var front = bytes.slice(0, bytes.length - 4); + var back = bytes.slice(bytes.length - 4); + var checksum = Crypto.SHA256(Crypto.SHA256(front, { + asBytes: true + }), { + asBytes: true + }).slice(0, 4); + if (checksum + "" == back + "") { + return { + version: front[0], + bytes: front.slice(1) + }; + } + } + + /* generate the transaction hash to sign from a transaction input */ + btrx.transactionHash = function (index, sigHashType) { + + var clone = bitjs.clone(this); + var shType = sigHashType || 1; + + /* black out all other ins, except this one */ + for (var i = 0; i < clone.inputs.length; i++) { + if (index != i) { + clone.inputs[i].script = []; + } + } + + + if ((clone.inputs) && clone.inputs[index]) { + + /* SIGHASH : For more info on sig hashs see https://en.bitcoin.it/wiki/OP_CHECKSIG + and https://bitcoin.org/en/developer-guide#signature-hash-type */ + + if (shType == 1) { + //SIGHASH_ALL 0x01 + + } else if (shType == 2) { + //SIGHASH_NONE 0x02 + clone.outputs = []; + for (var i = 0; i < clone.inputs.length; i++) { + if (index != i) { + clone.inputs[i].sequence = 0; + } + } + + } else if (shType == 3) { + + //SIGHASH_SINGLE 0x03 + clone.outputs.length = index + 1; + + for (var i = 0; i < index; i++) { + clone.outputs[i].value = -1; + clone.outputs[i].script = []; + } + + for (var i = 0; i < clone.inputs.length; i++) { + if (index != i) { + clone.inputs[i].sequence = 0; + } + } + + } else if (shType >= 128) { + //SIGHASH_ANYONECANPAY 0x80 + clone.inputs = [clone.inputs[index]]; + + if (shType == 129) { + // SIGHASH_ALL + SIGHASH_ANYONECANPAY + + } else if (shType == 130) { + // SIGHASH_NONE + SIGHASH_ANYONECANPAY + clone.outputs = []; + + } else if (shType == 131) { + // SIGHASH_SINGLE + SIGHASH_ANYONECANPAY + clone.outputs.length = index + 1; + for (var i = 0; i < index; i++) { + clone.outputs[i].value = -1; + clone.outputs[i].script = []; + } + } + } + + var buffer = Crypto.util.hexToBytes(clone.serialize()); + buffer = buffer.concat(bitjs.numToBytes(parseInt(shType), 4)); + var hash = Crypto.SHA256(buffer, { + asBytes: true + }); + var r = Crypto.util.bytesToHex(Crypto.SHA256(hash, { + asBytes: true + })); + return r; + } else { + return false; + } + } + + /* generate a signature from a transaction hash */ + btrx.transactionSig = function (index, wif, sigHashType, txhash) { + + function serializeSig(r, s) { + var rBa = r.toByteArraySigned(); + var sBa = s.toByteArraySigned(); + + var sequence = []; + sequence.push(0x02); // INTEGER + sequence.push(rBa.length); + sequence = sequence.concat(rBa); + + sequence.push(0x02); // INTEGER + sequence.push(sBa.length); + sequence = sequence.concat(sBa); + + sequence.unshift(sequence.length); + sequence.unshift(0x30); // SEQUENCE + + return sequence; + } + + var shType = sigHashType || 1; + var hash = txhash || Crypto.util.hexToBytes(this.transactionHash(index, shType)); + + if (hash) { + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + var key = bitjs.wif2privkey(wif); + var priv = BigInteger.fromByteArrayUnsigned(Crypto.util.hexToBytes(key['privkey'])); + var n = curve.getN(); + var e = BigInteger.fromByteArrayUnsigned(hash); + var badrs = 0 + do { + var k = this.deterministicK(wif, hash, badrs); + var G = curve.getG(); + var Q = G.multiply(k); + var r = Q.getX().toBigInteger().mod(n); + var s = k.modInverse(n).multiply(e.add(priv.multiply(r))).mod(n); + badrs++ + } while (r.compareTo(BigInteger.ZERO) <= 0 || s.compareTo(BigInteger.ZERO) <= 0); + + // Force lower s values per BIP62 + var halfn = n.shiftRight(1); + if (s.compareTo(halfn) > 0) { + s = n.subtract(s); + }; + + var sig = serializeSig(r, s); + sig.push(parseInt(shType, 10)); + + return Crypto.util.bytesToHex(sig); + } else { + return false; + } + } + + // https://tools.ietf.org/html/rfc6979#section-3.2 + btrx.deterministicK = function (wif, hash, badrs) { + // if r or s were invalid when this function was used in signing, + // we do not want to actually compute r, s here for efficiency, so, + // we can increment badrs. explained at end of RFC 6979 section 3.2 + + // wif is b58check encoded wif privkey. + // hash is byte array of transaction digest. + // badrs is used only if the k resulted in bad r or s. + + // some necessary things out of the way for clarity. + badrs = badrs || 0; + var key = bitjs.wif2privkey(wif); + var x = Crypto.util.hexToBytes(key['privkey']) + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + var N = curve.getN(); + + // Step: a + // hash is a byteArray of the message digest. so h1 == hash in our case + + // Step: b + var v = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1 + ]; + + // Step: c + var k = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + + // Step: d + k = Crypto.HMAC(Crypto.SHA256, v.concat([0]).concat(x).concat(hash), k, { + asBytes: true + }); + + // Step: e + v = Crypto.HMAC(Crypto.SHA256, v, k, { + asBytes: true + }); + + // Step: f + k = Crypto.HMAC(Crypto.SHA256, v.concat([1]).concat(x).concat(hash), k, { + asBytes: true + }); + + // Step: g + v = Crypto.HMAC(Crypto.SHA256, v, k, { + asBytes: true + }); + + // Step: h1 + var T = []; + + // Step: h2 (since we know tlen = qlen, just copy v to T.) + v = Crypto.HMAC(Crypto.SHA256, v, k, { + asBytes: true + }); + T = v; + + // Step: h3 + var KBigInt = BigInteger.fromByteArrayUnsigned(T); + + // loop if KBigInt is not in the range of [1, N-1] or if badrs needs incrementing. + var i = 0 + while (KBigInt.compareTo(N) >= 0 || KBigInt.compareTo(BigInteger.ZERO) <= 0 || i < + badrs) { + k = Crypto.HMAC(Crypto.SHA256, v.concat([0]), k, { + asBytes: true + }); + v = Crypto.HMAC(Crypto.SHA256, v, k, { + asBytes: true + }); + v = Crypto.HMAC(Crypto.SHA256, v, k, { + asBytes: true + }); + T = v; + KBigInt = BigInteger.fromByteArrayUnsigned(T); + i++ + }; + + return KBigInt; + }; + + btrx.writeBytesToScriptBuffer = function (buf, bytes) { + if (bytes.length < 76) { //OP_PUSHDATA1 + buf.push(bytes.length); + } else if (bytes.length <= 0xff) { + buf.push(76); //OP_PUSHDATA1 + buf.push(bytes.length); + } else if (bytes.length <= 0xffff) { + buf.push(77); //OP_PUSHDATA2 + buf.push(bytes.length & 0xff); + buf.push((bytes.length >>> 8) & 0xff); + } else { + buf.push(78); //OP_PUSHDATA4 + buf.push(bytes.length & 0xff); + buf.push((bytes.length >>> 8) & 0xff); + buf.push((bytes.length >>> 16) & 0xff); + buf.push((bytes.length >>> 24) & 0xff); + } + buf = buf.concat(bytes); + return buf; + } + + btrx.parseScript = function (script) { + + var chunks = []; + var i = 0; + + function readChunk(n) { + chunks.push(script.slice(i, i + n)); + i += n; + }; + + while (i < script.length) { + var opcode = script[i++]; + if (opcode >= 0xF0) { + opcode = (opcode << 8) | script[i++]; + } + + var len; + if (opcode > 0 && opcode < 76) { //OP_PUSHDATA1 + readChunk(opcode); + } else if (opcode == 76) { //OP_PUSHDATA1 + len = script[i++]; + readChunk(len); + } else if (opcode == 77) { //OP_PUSHDATA2 + len = (script[i++] << 8) | script[i++]; + readChunk(len); + } else if (opcode == 78) { //OP_PUSHDATA4 + len = (script[i++] << 24) | (script[i++] << 16) | (script[i++] << 8) | script[i++]; + readChunk(len); + } else { + chunks.push(opcode); + } + + if (i < 0x00) { + break; + } + } + + return chunks; + } + + btrx.decodeRedeemScript = function (rs) { + if (typeof rs == "string") + rs = Crypto.util.hexToBytes(rs); + var script = this.parseScript(rs); + if (!(script[0] > 80 && script[script.length - 2] > 80 && script[script.length - 1] == 174)) //OP_CHECKMULTISIG + throw "Invalid RedeemScript"; + var r = {}; + r.required = script[0] - 80; + r.pubkeys = []; + for (var i = 1; i < script.length - 2; i++) + r.pubkeys.push(Crypto.util.bytesToHex(script[i])); + r.address = bitjs.pubkeys2multisig(r.pubkeys, r.required).address; + r.redeemscript = Crypto.util.bytesToHex(rs); + return r; + } + + /* sign a "standard" input */ + btrx.signinput = function (index, wif, sigHashType) { + var key = bitjs.wif2pubkey(wif); + var shType = sigHashType || 1; + var signature = this.transactionSig(index, wif, shType); + var buf = []; + var sigBytes = Crypto.util.hexToBytes(signature); + buf = this.writeBytesToScriptBuffer(buf, sigBytes); + var pubKeyBytes = Crypto.util.hexToBytes(key['pubkey']); + buf.push(pubKeyBytes.length); + buf = buf.concat(pubKeyBytes); + this.inputs[index].script = buf; + return true; + } + + /* sign a multisig input */ + btrx.signmultisig = function (index, wif, sigHashType) { + + var script = Array.from(this.inputs[index].script); + var redeemScript, sigsList = []; + + if (script[0] == 0) { //script with signatures + script = this.parseScript(script); + for (var i = 0; i < script.length; i++) { + if (Array.isArray(script[i])) { + if (script[i][0] == 48) //0x30 DERSequence + sigsList.push(script[i]); + else if (script[i][0] >= 80 && script[i][script[i].length - 1] == 174) //OP_CHECKMULTISIG + redeemScript = script[i]; + } + } + } else { //script = redeemscript + redeemScript = script; + } + + var pubkeyList = this.decodeRedeemScript(redeemScript).pubkeys; + var pubkey = bitjs.wif2pubkey(wif)['pubkey']; + if (!pubkeyList.includes(pubkey)) //wif not a part of this multisig + return false; + + pubkeyList = pubkeyList.map(pub => Crypto.util.hexToBytes(bitjs.pubkeydecompress(pub))); //decompress pubkeys + + var shType = sigHashType || 1; + this.inputs[index].script = redeemScript; //script to be signed is redeemscript + var signature = Crypto.util.hexToBytes(this.transactionSig(index, wif, shType)); + sigsList.push(signature); + + var buf = []; + buf.push(0); + + //verify signatures and order them (also remove duplicate sigs) + for (let x in pubkeyList) { + for (let y in sigsList) { + var sighash = Crypto.util.hexToBytes(this.transactionHash(index, sigsList[y].slice(-1)[0] * 1)); + if (bitjs.verifySignature(sighash, sigsList[y], pubkeyList[x])) { + buf = this.writeBytesToScriptBuffer(buf, sigsList[y]); + break; //ensures duplicate sigs from same pubkey are not added + } + } + } + + //append redeemscript + buf = this.writeBytesToScriptBuffer(buf, redeemScript); + + this.inputs[index].script = buf; + return true; + } + + /* sign inputs */ + btrx.sign = function (wif, sigHashType) { + var shType = sigHashType || 1; + for (var i = 0; i < this.inputs.length; i++) { + + var decodedScript = this.scriptDecode(i); + + if (decodedScript.type == "scriptpubkey" && decodedScript.signed == false) { //regular + var addr = bitjs.wif2address(wif)["address"];; + if (decodedScript.pubhash == Crypto.util.bytesToHex(this.addressDecode(addr).bytes)) //input belongs to wif + this.signinput(i, wif, shType); + } else if (decodedScript.type == "multisig") { //multisig + this.signmultisig(i, wif, shType); + } + } + return this.serialize(); + } + + // function to find type of the script in input + btrx.scriptDecode = function (index) { + var script = this.parseScript(this.inputs[index].script); + if (script.length == 5 && script[script.length - 1] == 172) { + //OP_DUP OP_HASH160 [address bytes] OP_EQUALVERIFY OP_CHECKSIG + // regular scriptPubkey (not signed) + return { type: 'scriptpubkey', signed: false, pubhash: Crypto.util.bytesToHex(script[2]) }; + } else if (script.length == 2 && script[0][0] == 48) { + //[signature] [pubkey] + //(probably) regular signed + return { type: 'scriptpubkey', signed: true }; + } else if (script[0] == 0 && script[script.length - 1][script[script.length - 1].length - 1] == 174) { + //0 [signatues] [redeemscript OP_CHECKMULTISIG] + // multisig with signature + return { type: 'multisig', rs: script[script.length - 1] }; + } else if (script[0] >= 80 && script[script.length - 1] == 174) { + //redeemscript: 80+ [pubkeys] OP_CHECKMULTISIG + // multisig without signature + return { type: 'multisig', rs: Array.from(this.inputs[index].script) }; + } + } + + /* serialize a transaction */ + btrx.serialize = function () { + var buffer = []; + buffer = buffer.concat(bitjs.numToBytes(parseInt(this.version), 4)); + + buffer = buffer.concat(bitjs.numToVarInt(this.inputs.length)); + for (var i = 0; i < this.inputs.length; i++) { + var txin = this.inputs[i]; + buffer = buffer.concat(Crypto.util.hexToBytes(txin.outpoint.hash).reverse()); + buffer = buffer.concat(bitjs.numToBytes(parseInt(txin.outpoint.index), 4)); + var scriptBytes = txin.script; + buffer = buffer.concat(bitjs.numToVarInt(scriptBytes.length)); + buffer = buffer.concat(scriptBytes); + buffer = buffer.concat(bitjs.numToBytes(parseInt(txin.sequence), 4)); + + } + buffer = buffer.concat(bitjs.numToVarInt(this.outputs.length)); + + for (var i = 0; i < this.outputs.length; i++) { + var txout = this.outputs[i]; + buffer = buffer.concat(bitjs.numToBytes(txout.value, 8)); + var scriptBytes = txout.script; + buffer = buffer.concat(bitjs.numToVarInt(scriptBytes.length)); + buffer = buffer.concat(scriptBytes); + } + + buffer = buffer.concat(bitjs.numToBytes(parseInt(this.locktime), 4)); + + //flochange -- append floData field + buffer = buffer.concat(bitjs.numToVarInt(this.floData.length)); + buffer = buffer.concat(bitjs.strToBytes(this.floData)) + + return Crypto.util.bytesToHex(buffer); + } + + /* deserialize a transaction */ + function deserialize(buffer) { + if (typeof buffer == "string") { + buffer = Crypto.util.hexToBytes(buffer) + } + + var pos = 0; + + var readAsInt = function (bytes) { + if (bytes == 0) return 0; + pos++; + return buffer[pos - 1] + readAsInt(bytes - 1) * 256; + } + + var readVarInt = function () { + pos++; + if (buffer[pos - 1] < 253) { + return buffer[pos - 1]; + } + return readAsInt(buffer[pos - 1] - 251); + } + + var readBytes = function (bytes) { + pos += bytes; + return buffer.slice(pos - bytes, pos); + } + + var readVarString = function () { + var size = readVarInt(); + return readBytes(size); + } + + var bytesToStr = function (bytes) { + return bytes.map(b => String.fromCharCode(b)).join(''); + } + + const self = btrx; + + self.version = readAsInt(4); + + var ins = readVarInt(); + for (var i = 0; i < ins; i++) { + self.inputs.push({ + outpoint: { + hash: Crypto.util.bytesToHex(readBytes(32).reverse()), + index: readAsInt(4) + }, + script: readVarString(), + sequence: readAsInt(4) + }); + } + + var outs = readVarInt(); + for (var i = 0; i < outs; i++) { + self.outputs.push({ + value: bitjs.bytesToNum(readBytes(8)), + script: readVarString() + }); + } + + self.lock_time = readAsInt(4); + + //flochange - floData field + self.floData = bytesToStr(readVarString()); + + return self; + } + + //deserialize the data if passed + if (tx_data) + deserialize(tx_data); + + return btrx; + + } + + bitjs.numToBytes = function (num, bytes) { + if (typeof bytes === "undefined") bytes = 8; + if (bytes == 0) { + return []; + } else if (num == -1) { + return Crypto.util.hexToBytes("ffffffffffffffff"); + } else { + return [num % 256].concat(bitjs.numToBytes(Math.floor(num / 256), bytes - 1)); + } + } + + bitjs.numToByteArray = function (num) { + if (num <= 256) { + return [num]; + } else { + return [num % 256].concat(bitjs.numToByteArray(Math.floor(num / 256))); + } + } + + bitjs.numToVarInt = function (num) { + if (num < 253) { + return [num]; + } else if (num < 65536) { + return [253].concat(bitjs.numToBytes(num, 2)); + } else if (num < 4294967296) { + return [254].concat(bitjs.numToBytes(num, 4)); + } else { + return [255].concat(bitjs.numToBytes(num, 8)); + } + } + + bitjs.bytesToNum = function (bytes) { + if (bytes.length == 0) return 0; + else return bytes[0] + 256 * bitjs.bytesToNum(bytes.slice(1)); + } + + //flochange - adding fn to convert string (for flodata) to byte + bitjs.strToBytes = function (str) { + return str.split('').map(c => c.charCodeAt(0)); + } + + /* decompress an compressed public key */ + bitjs.pubkeydecompress = function (pubkey) { + if ((typeof (pubkey) == 'string') && pubkey.match(/^[a-f0-9]+$/i)) { + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + try { + var pt = curve.curve.decodePointHex(pubkey); + var x = pt.getX().toBigInteger(); + var y = pt.getY().toBigInteger(); + + var publicKeyBytes = EllipticCurve.integerToBytes(x, 32); + publicKeyBytes = publicKeyBytes.concat(EllipticCurve.integerToBytes(y, 32)); + publicKeyBytes.unshift(0x04); + return Crypto.util.bytesToHex(publicKeyBytes); + } catch (e) { + // console.log(e); + return false; + } + } + return false; + } + + bitjs.verifySignature = function (hash, sig, pubkey) { + return Bitcoin.ECDSA.verify(hash, sig, pubkey); + } + + /* clone an object */ + bitjs.clone = function (obj) { + if (obj == null || typeof (obj) != 'object') return obj; + var temp = new obj.constructor(); + + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + temp[key] = bitjs.clone(obj[key]); + } + } + return temp; + } + + var B58 = bitjs.Base58 = { + alphabet: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", + validRegex: /^[1-9A-HJ-NP-Za-km-z]+$/, + base: BigInteger.valueOf(58), + + /** + * Convert a byte array to a base58-encoded string. + * + * Written by Mike Hearn for BitcoinJ. + * Copyright (c) 2011 Google Inc. + * + * Ported to JavaScript by Stefan Thomas. + */ + encode: function (input) { + var bi = BigInteger.fromByteArrayUnsigned(input); + var chars = []; + + while (bi.compareTo(B58.base) >= 0) { + var mod = bi.mod(B58.base); + chars.unshift(B58.alphabet[mod.intValue()]); + bi = bi.subtract(mod).divide(B58.base); + } + chars.unshift(B58.alphabet[bi.intValue()]); + + // Convert leading zeros too. + for (var i = 0; i < input.length; i++) { + if (input[i] == 0x00) { + chars.unshift(B58.alphabet[0]); + } else break; + } + + return chars.join(''); + }, + + /** + * Convert a base58-encoded string to a byte array. + * + * Written by Mike Hearn for BitcoinJ. + * Copyright (c) 2011 Google Inc. + * + * Ported to JavaScript by Stefan Thomas. + */ + decode: function (input) { + var bi = BigInteger.valueOf(0); + var leadingZerosNum = 0; + for (var i = input.length - 1; i >= 0; i--) { + var alphaIndex = B58.alphabet.indexOf(input[i]); + if (alphaIndex < 0) { + throw "Invalid character"; + } + bi = bi.add(BigInteger.valueOf(alphaIndex) + .multiply(B58.base.pow(input.length - 1 - i))); + + // This counts leading zero bytes + if (input[i] == "1") leadingZerosNum++; + else leadingZerosNum = 0; + } + var bytes = bi.toByteArrayUnsigned(); + + // Add leading zeros + while (leadingZerosNum-- > 0) bytes.unshift(0); + + return bytes; + } + } + return bitjs; + + })(); + + //Bitcoin.js + (function () { + /* + Copyright (c) 2011 Stefan Thomas + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + var Bitcoin = GLOBAL.Bitcoin = {}; + + //https://raw.github.com/bitcoinjs/bitcoinjs-lib/c952aaeb3ee472e3776655b8ea07299ebed702c7/src/base58.js + var B58 = Bitcoin.Base58 = { + alphabet: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", + validRegex: /^[1-9A-HJ-NP-Za-km-z]+$/, + base: BigInteger.valueOf(58), + + /** + * Convert a byte array to a base58-encoded string. + * + * Written by Mike Hearn for BitcoinJ. + * Copyright (c) 2011 Google Inc. + * + * Ported to JavaScript by Stefan Thomas. + */ + encode: function (input) { + var bi = BigInteger.fromByteArrayUnsigned(input); + var chars = []; + + while (bi.compareTo(B58.base) >= 0) { + var mod = bi.mod(B58.base); + chars.unshift(B58.alphabet[mod.intValue()]); + bi = bi.subtract(mod).divide(B58.base); + } + chars.unshift(B58.alphabet[bi.intValue()]); + + // Convert leading zeros too. + for (var i = 0; i < input.length; i++) { + if (input[i] == 0x00) { + chars.unshift(B58.alphabet[0]); + } else break; + } + + return chars.join(''); + }, + + /** + * Convert a base58-encoded string to a byte array. + * + * Written by Mike Hearn for BitcoinJ. + * Copyright (c) 2011 Google Inc. + * + * Ported to JavaScript by Stefan Thomas. + */ + decode: function (input) { + var bi = BigInteger.valueOf(0); + var leadingZerosNum = 0; + for (var i = input.length - 1; i >= 0; i--) { + var alphaIndex = B58.alphabet.indexOf(input[i]); + if (alphaIndex < 0) { + throw "Invalid character"; + } + bi = bi.add(BigInteger.valueOf(alphaIndex) + .multiply(B58.base.pow(input.length - 1 - i))); + + // This counts leading zero bytes + if (input[i] == "1") leadingZerosNum++; + else leadingZerosNum = 0; + } + var bytes = bi.toByteArrayUnsigned(); + + // Add leading zeros + while (leadingZerosNum-- > 0) bytes.unshift(0); + + return bytes; + } + }; + + //https://raw.github.com/bitcoinjs/bitcoinjs-lib/09e8c6e184d6501a0c2c59d73ca64db5c0d3eb95/src/address.js + Bitcoin.Address = function (bytes) { + if ("string" == typeof bytes) { + var d = Bitcoin.Address.decodeString(bytes); + bytes = d.hash; + if (d.version == Bitcoin.Address.standardVersion || d.version == Bitcoin.Address.multisigVersion) + this.version = d.version; + else throw "Version (prefix) " + d.version + " not supported!"; + } else { + this.version = Bitcoin.Address.standardVersion; + } + this.hash = bytes; + }; + + Bitcoin.Address.standardVersion = 0x23; // (FLO mainnet 0x23, 35D), (Bitcoin Mainnet, 0x00, 0D) + Bitcoin.Address.multisigVersion = 0x5e; // (FLO multisig 0x5e, 94D) + + if (GLOBAL.cryptocoin == "FLO_TEST") { + Bitcoin.Address.standardVersion = 0x73; // (FLO testnet 0x73, 115D), (Bitcoin Mainnet, 0x00, 0D) + Bitcoin.Address.multisigVersion = 0xc6; // (FLO testnet multisig 0xc6, 198D) + } + + /** + * Serialize this object as a standard Bitcoin address. + * + * Returns the address as a base58-encoded string in the standardized format. + */ + Bitcoin.Address.prototype.toString = function (version = null) { + // Get a copy of the hash + var hash = this.hash.slice(0); + + // Version + hash.unshift(version !== null ? version : this.version); + var checksum = Crypto.SHA256(Crypto.SHA256(hash, { + asBytes: true + }), { + asBytes: true + }); + var bytes = hash.concat(checksum.slice(0, 4)); + return Bitcoin.Base58.encode(bytes); + }; + + Bitcoin.Address.prototype.getHashBase64 = function () { + return Crypto.util.bytesToBase64(this.hash); + }; + + /** + * Parse a Bitcoin address contained in a string. + */ + Bitcoin.Address.decodeString = function (string) { + var bytes = Bitcoin.Base58.decode(string); + var hash = bytes.slice(0, 21); + var checksum = Crypto.SHA256(Crypto.SHA256(hash, { + asBytes: true + }), { + asBytes: true + }); + + if (checksum[0] != bytes[21] || + checksum[1] != bytes[22] || + checksum[2] != bytes[23] || + checksum[3] != bytes[24]) { + throw "Checksum validation failed!"; + } + + /*if (version != hash.shift()) { + throw "Version " + hash.shift() + " not supported!"; + }*/ + + var version = hash.shift(); + return { version, hash }; + }; + //https://raw.github.com/bitcoinjs/bitcoinjs-lib/e90780d3d3b8fc0d027d2bcb38b80479902f223e/src/ecdsa.js + Bitcoin.ECDSA = (function () { + var ecparams = EllipticCurve.getSECCurveByName("secp256k1"); + var rng = new SecureRandom(); + + var P_OVER_FOUR = null; + + function implShamirsTrick(P, k, Q, l) { + var m = Math.max(k.bitLength(), l.bitLength()); + var Z = P.add2D(Q); + var R = P.curve.getInfinity(); + + for (var i = m - 1; i >= 0; --i) { + R = R.twice2D(); + + R.z = BigInteger.ONE; + + if (k.testBit(i)) { + if (l.testBit(i)) { + R = R.add2D(Z); + } else { + R = R.add2D(P); + } + } else { + if (l.testBit(i)) { + R = R.add2D(Q); + } + } + } + + return R; + }; + + var ECDSA = { + getBigRandom: function (limit) { + return new BigInteger(limit.bitLength(), rng) + .mod(limit.subtract(BigInteger.ONE)) + .add(BigInteger.ONE); + }, + sign: function (hash, priv) { + var d = priv; + var n = ecparams.getN(); + var e = BigInteger.fromByteArrayUnsigned(hash); + + do { + var k = ECDSA.getBigRandom(n); + var G = ecparams.getG(); + var Q = G.multiply(k); + var r = Q.getX().toBigInteger().mod(n); + } while (r.compareTo(BigInteger.ZERO) <= 0); + + var s = k.modInverse(n).multiply(e.add(d.multiply(r))).mod(n); + + return ECDSA.serializeSig(r, s); + }, + + verify: function (hash, sig, pubkey) { + var r, s; + if (Bitcoin.Util.isArray(sig)) { + var obj = ECDSA.parseSig(sig); + r = obj.r; + s = obj.s; + } else if ("object" === typeof sig && sig.r && sig.s) { + r = sig.r; + s = sig.s; + } else { + throw "Invalid value for signature"; + } + + var Q; + if (pubkey instanceof EllipticCurve.PointFp) { + Q = pubkey; + } else if (Bitcoin.Util.isArray(pubkey)) { + Q = EllipticCurve.PointFp.decodeFrom(ecparams.getCurve(), pubkey); + } else { + throw "Invalid format for pubkey value, must be byte array or ec.PointFp"; + } + var e = BigInteger.fromByteArrayUnsigned(hash); + + return ECDSA.verifyRaw(e, r, s, Q); + }, + + verifyRaw: function (e, r, s, Q) { + var n = ecparams.getN(); + var G = ecparams.getG(); + + if (r.compareTo(BigInteger.ONE) < 0 || + r.compareTo(n) >= 0) + return false; + + if (s.compareTo(BigInteger.ONE) < 0 || + s.compareTo(n) >= 0) + return false; + + var c = s.modInverse(n); + + var u1 = e.multiply(c).mod(n); + var u2 = r.multiply(c).mod(n); + + // TODO(!!!): For some reason Shamir's trick isn't working with + // signed message verification!? Probably an implementation + // error! + //var point = implShamirsTrick(G, u1, Q, u2); + var point = G.multiply(u1).add(Q.multiply(u2)); + + var v = point.getX().toBigInteger().mod(n); + + return v.equals(r); + }, + + /** + * Serialize a signature into DER format. + * + * Takes two BigIntegers representing r and s and returns a byte array. + */ + serializeSig: function (r, s) { + var rBa = r.toByteArraySigned(); + var sBa = s.toByteArraySigned(); + + var sequence = []; + sequence.push(0x02); // INTEGER + sequence.push(rBa.length); + sequence = sequence.concat(rBa); + + sequence.push(0x02); // INTEGER + sequence.push(sBa.length); + sequence = sequence.concat(sBa); + + sequence.unshift(sequence.length); + sequence.unshift(0x30); // SEQUENCE + + return sequence; + }, + + /** + * Parses a byte array containing a DER-encoded signature. + * + * This function will return an object of the form: + * + * { + * r: BigInteger, + * s: BigInteger + * } + */ + parseSig: function (sig) { + var cursor; + if (sig[0] != 0x30) + throw new Error("Signature not a valid DERSequence"); + + cursor = 2; + if (sig[cursor] != 0x02) + throw new Error("First element in signature must be a DERInteger");; + var rBa = sig.slice(cursor + 2, cursor + 2 + sig[cursor + 1]); + + cursor += 2 + sig[cursor + 1]; + if (sig[cursor] != 0x02) + throw new Error("Second element in signature must be a DERInteger"); + var sBa = sig.slice(cursor + 2, cursor + 2 + sig[cursor + 1]); + + cursor += 2 + sig[cursor + 1]; + + //if (cursor != sig.length) + // throw new Error("Extra bytes in signature"); + + var r = BigInteger.fromByteArrayUnsigned(rBa); + var s = BigInteger.fromByteArrayUnsigned(sBa); + + return { + r: r, + s: s + }; + }, + + parseSigCompact: function (sig) { + if (sig.length !== 65) { + throw "Signature has the wrong length"; + } + + // Signature is prefixed with a type byte storing three bits of + // information. + var i = sig[0] - 27; + if (i < 0 || i > 7) { + throw "Invalid signature type"; + } + + var n = ecparams.getN(); + var r = BigInteger.fromByteArrayUnsigned(sig.slice(1, 33)).mod(n); + var s = BigInteger.fromByteArrayUnsigned(sig.slice(33, 65)).mod(n); + + return { + r: r, + s: s, + i: i + }; + }, + + /** + * Recover a public key from a signature. + * + * See SEC 1: Elliptic Curve Cryptography, section 4.1.6, "Public + * Key Recovery Operation". + * + * http://www.secg.org/download/aid-780/sec1-v2.pdf + */ + recoverPubKey: function (r, s, hash, i) { + // The recovery parameter i has two bits. + i = i & 3; + + // The less significant bit specifies whether the y coordinate + // of the compressed point is even or not. + var isYEven = i & 1; + + // The more significant bit specifies whether we should use the + // first or second candidate key. + var isSecondKey = i >> 1; + + var n = ecparams.getN(); + var G = ecparams.getG(); + var curve = ecparams.getCurve(); + var p = curve.getQ(); + var a = curve.getA().toBigInteger(); + var b = curve.getB().toBigInteger(); + + // We precalculate (p + 1) / 4 where p is if the field order + if (!P_OVER_FOUR) { + P_OVER_FOUR = p.add(BigInteger.ONE).divide(BigInteger.valueOf(4)); + } + + // 1.1 Compute x + var x = isSecondKey ? r.add(n) : r; + + // 1.3 Convert x to point + var alpha = x.multiply(x).multiply(x).add(a.multiply(x)).add(b).mod(p); + var beta = alpha.modPow(P_OVER_FOUR, p); + + var xorOdd = beta.isEven() ? (i % 2) : ((i + 1) % 2); + // If beta is even, but y isn't or vice versa, then convert it, + // otherwise we're done and y == beta. + var y = (beta.isEven() ? !isYEven : isYEven) ? beta : p.subtract(beta); + + // 1.4 Check that nR is at infinity + var R = new EllipticCurve.PointFp(curve, + curve.fromBigInteger(x), + curve.fromBigInteger(y)); + R.validate(); + + // 1.5 Compute e from M + var e = BigInteger.fromByteArrayUnsigned(hash); + var eNeg = BigInteger.ZERO.subtract(e).mod(n); + + // 1.6 Compute Q = r^-1 (sR - eG) + var rInv = r.modInverse(n); + var Q = implShamirsTrick(R, s, G, eNeg).multiply(rInv); + + Q.validate(); + if (!ECDSA.verifyRaw(e, r, s, Q)) { + throw "Pubkey recovery unsuccessful"; + } + + var pubKey = new Bitcoin.ECKey(); + pubKey.pub = Q; + return pubKey; + }, + + /** + * Calculate pubkey extraction parameter. + * + * When extracting a pubkey from a signature, we have to + * distinguish four different cases. Rather than putting this + * burden on the verifier, Bitcoin includes a 2-bit value with the + * signature. + * + * This function simply tries all four cases and returns the value + * that resulted in a successful pubkey recovery. + */ + calcPubkeyRecoveryParam: function (address, r, s, hash) { + for (var i = 0; i < 4; i++) { + try { + var pubkey = Bitcoin.ECDSA.recoverPubKey(r, s, hash, i); + if (pubkey.getBitcoinAddress().toString() == address) { + return i; + } + } catch (e) { } + } + throw "Unable to find valid recovery factor"; + } + }; + + return ECDSA; + })(); + Bitcoin.KeyPool = (function () { + var KeyPool = function () { + this.keyArray = []; + + this.push = function (item) { + if (item == null || item.priv == null) return; + var doAdd = true; + // prevent duplicates from being added to the array + for (var index in this.keyArray) { + var currentItem = this.keyArray[index]; + if (currentItem != null && currentItem.priv != null && item.getBitcoinAddress() == currentItem.getBitcoinAddress()) { + doAdd = false; + break; + } + } + if (doAdd) this.keyArray.push(item); + }; + + this.reset = function () { + this.keyArray = []; + }; + + this.getArray = function () { + // copy array + return this.keyArray.slice(0); + }; + + this.setArray = function (ka) { + this.keyArray = ka; + }; + + this.length = function () { + return this.keyArray.length; + }; + + this.toString = function () { + var keyPoolString = "# = " + this.length() + "\n"; + var pool = this.getArray(); + for (var index in pool) { + var item = pool[index]; + if (Bitcoin.Util.hasMethods(item, 'getBitcoinAddress', 'toString')) { + if (item != null) { + keyPoolString += "\"" + item.getBitcoinAddress() + "\"" + ", \"" + item.toString("wif") + "\"\n"; + } + } + } + + return keyPoolString; + }; + + return this; + }; + + return new KeyPool(); + })(); + + Bitcoin.Bip38Key = (function () { + var Bip38 = function (address, encryptedKey) { + this.address = address; + this.priv = encryptedKey; + }; + + Bip38.prototype.getBitcoinAddress = function () { + return this.address; + }; + + Bip38.prototype.toString = function () { + return this.priv; + }; + + return Bip38; + })(); + + //https://raw.github.com/pointbiz/bitcoinjs-lib/9b2f94a028a7bc9bed94e0722563e9ff1d8e8db8/src/eckey.js + Bitcoin.ECKey = (function () { + var ECDSA = Bitcoin.ECDSA; + var KeyPool = Bitcoin.KeyPool; + var ecparams = EllipticCurve.getSECCurveByName("secp256k1"); + + var ECKey = function (input) { + if (!input) { + // Generate new key + var n = ecparams.getN(); + this.priv = ECDSA.getBigRandom(n); + } else if (input instanceof BigInteger) { + // Input is a private key value + this.priv = input; + } else if (Bitcoin.Util.isArray(input)) { + // Prepend zero byte to prevent interpretation as negative integer + this.priv = BigInteger.fromByteArrayUnsigned(input); + } else if ("string" == typeof input) { + var bytes = null; + try { + + // This part is edited for FLO. FLO WIF are always compressed WIF (length of 52). + if ((/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{52}$/.test(input))) { + bytes = ECKey.decodeCompressedWalletImportFormat(input); + this.compressed = true; + } else if (ECKey.isHexFormat(input)) { + bytes = Crypto.util.hexToBytes(input); + } + + + /* + if (ECKey.isWalletImportFormat(input)) { + bytes = ECKey.decodeWalletImportFormat(input); + } else if (ECKey.isCompressedWalletImportFormat(input)) { + bytes = ECKey.decodeCompressedWalletImportFormat(input); + this.compressed = true; + } else if (ECKey.isMiniFormat(input)) { + bytes = Crypto.SHA256(input, { asBytes: true }); + } else if (ECKey.isHexFormat(input)) { + bytes = Crypto.util.hexToBytes(input); + } else if (ECKey.isBase64Format(input)) { + bytes = Crypto.util.base64ToBytes(input); + } + */ + } catch (exc1) { + this.setError(exc1); + } + + if (ECKey.isBase6Format(input)) { + this.priv = new BigInteger(input, 6); + } else if (bytes == null || bytes.length != 32) { + this.priv = null; + } else { + // Prepend zero byte to prevent interpretation as negative integer + this.priv = BigInteger.fromByteArrayUnsigned(bytes); + } + } + + this.compressed = (this.compressed == undefined) ? !!ECKey.compressByDefault : this.compressed; + try { + // check not zero + if (this.priv != null && BigInteger.ZERO.compareTo(this.priv) == 0) this.setError("Error: BigInteger equal to zero."); + // valid range [0x1, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140]) + var hexKeyRangeLimit = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140"; + var rangeLimitBytes = Crypto.util.hexToBytes(hexKeyRangeLimit); + var limitBigInt = BigInteger.fromByteArrayUnsigned(rangeLimitBytes); + if (this.priv != null && limitBigInt.compareTo(this.priv) < 0) this.setError("Error: BigInteger outside of curve range.") + + if (this.priv != null) { + KeyPool.push(this); + } + } catch (exc2) { + this.setError(exc2); + } + }; + + if (GLOBAL.cryptocoin == "FLO") + ECKey.privateKeyPrefix = 0xA3; //(Bitcoin mainnet 0x80 testnet 0xEF) (FLO mainnet 0xA3 163 D) + else if (GLOBAL.cryptocoin == "FLO_TEST") + ECKey.privateKeyPrefix = 0xEF; //FLO testnet + + /** + * Whether public keys should be returned compressed by default. + */ + ECKey.compressByDefault = false; + + /** + * Set whether the public key should be returned compressed or not. + */ + ECKey.prototype.setError = function (err) { + this.error = err; + this.priv = null; + return this; + }; + + /** + * Set whether the public key should be returned compressed or not. + */ + ECKey.prototype.setCompressed = function (v) { + this.compressed = !!v; + if (this.pubPoint) this.pubPoint.compressed = this.compressed; + return this; + }; + + /* + * Return public key as a byte array in DER encoding + */ + ECKey.prototype.getPub = function () { + if (this.compressed) { + if (this.pubComp) return this.pubComp; + return this.pubComp = this.getPubPoint().getEncoded(1); + } else { + if (this.pubUncomp) return this.pubUncomp; + return this.pubUncomp = this.getPubPoint().getEncoded(0); + } + }; + + /** + * Return public point as ECPoint object. + */ + ECKey.prototype.getPubPoint = function () { + if (!this.pubPoint) { + this.pubPoint = ecparams.getG().multiply(this.priv); + this.pubPoint.compressed = this.compressed; + } + return this.pubPoint; + }; + + ECKey.prototype.getPubKeyHex = function () { + if (this.compressed) { + if (this.pubKeyHexComp) return this.pubKeyHexComp; + return this.pubKeyHexComp = Crypto.util.bytesToHex(this.getPub()).toString().toUpperCase(); + } else { + if (this.pubKeyHexUncomp) return this.pubKeyHexUncomp; + return this.pubKeyHexUncomp = Crypto.util.bytesToHex(this.getPub()).toString().toUpperCase(); + } + }; + + /** + * Get the pubKeyHash for this key. + * + * This is calculated as RIPE160(SHA256([encoded pubkey])) and returned as + * a byte array. + */ + ECKey.prototype.getPubKeyHash = function () { + if (this.compressed) { + if (this.pubKeyHashComp) return this.pubKeyHashComp; + return this.pubKeyHashComp = Bitcoin.Util.sha256ripe160(this.getPub()); + } else { + if (this.pubKeyHashUncomp) return this.pubKeyHashUncomp; + return this.pubKeyHashUncomp = Bitcoin.Util.sha256ripe160(this.getPub()); + } + }; + + ECKey.prototype.getBitcoinAddress = function () { + var hash = this.getPubKeyHash(); + var addr = new Bitcoin.Address(hash); + return addr.toString(); + }; + + /* + * Takes a public point as a hex string or byte array + */ + ECKey.prototype.setPub = function (pub) { + // byte array + if (Bitcoin.Util.isArray(pub)) { + pub = Crypto.util.bytesToHex(pub).toString().toUpperCase(); + } + var ecPoint = ecparams.getCurve().decodePointHex(pub); + this.setCompressed(ecPoint.compressed); + this.pubPoint = ecPoint; + return this; + }; + + // Sipa Private Key Wallet Import Format + ECKey.prototype.getBitcoinWalletImportFormat = function () { + var bytes = this.getBitcoinPrivateKeyByteArray(); + if (bytes == null) return ""; + bytes.unshift(ECKey.privateKeyPrefix); // prepend 0x80 byte + if (this.compressed) bytes.push(0x01); // append 0x01 byte for compressed format + var checksum = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + bytes = bytes.concat(checksum.slice(0, 4)); + var privWif = Bitcoin.Base58.encode(bytes); + return privWif; + }; + + // Private Key Hex Format + ECKey.prototype.getBitcoinHexFormat = function () { + return Crypto.util.bytesToHex(this.getBitcoinPrivateKeyByteArray()).toString().toUpperCase(); + }; + + // Private Key Base64 Format + ECKey.prototype.getBitcoinBase64Format = function () { + return Crypto.util.bytesToBase64(this.getBitcoinPrivateKeyByteArray()); + }; + + ECKey.prototype.getBitcoinPrivateKeyByteArray = function () { + if (this.priv == null) return null; + // Get a copy of private key as a byte array + var bytes = this.priv.toByteArrayUnsigned(); + // zero pad if private key is less than 32 bytes + while (bytes.length < 32) bytes.unshift(0x00); + return bytes; + }; + + ECKey.prototype.toString = function (format) { + format = format || ""; + if (format.toString().toLowerCase() == "base64" || format.toString().toLowerCase() == "b64") { + return this.getBitcoinBase64Format(); + } + // Wallet Import Format + else if (format.toString().toLowerCase() == "wif") { + return this.getBitcoinWalletImportFormat(); + } else { + return this.getBitcoinHexFormat(); + } + }; + + ECKey.prototype.sign = function (hash) { + return ECDSA.sign(hash, this.priv); + }; + + ECKey.prototype.verify = function (hash, sig) { + return ECDSA.verify(hash, sig, this.getPub()); + }; + + /** + * Parse a wallet import format private key contained in a string. + */ + ECKey.decodeWalletImportFormat = function (privStr) { + var bytes = Bitcoin.Base58.decode(privStr); + var hash = bytes.slice(0, 33); + var checksum = Crypto.SHA256(Crypto.SHA256(hash, { + asBytes: true + }), { + asBytes: true + }); + if (checksum[0] != bytes[33] || + checksum[1] != bytes[34] || + checksum[2] != bytes[35] || + checksum[3] != bytes[36]) { + throw "Checksum validation failed!"; + + } + var version = hash.shift(); + /* + if (version != ECKey.privateKeyPrefix) { + throw "Version " + version + " not supported!"; + } + */ + return hash; + }; + + /** + * Parse a compressed wallet import format private key contained in a string. + */ + ECKey.decodeCompressedWalletImportFormat = function (privStr) { + var bytes = Bitcoin.Base58.decode(privStr); + var hash = bytes.slice(0, 34); + var checksum = Crypto.SHA256(Crypto.SHA256(hash, { + asBytes: true + }), { + asBytes: true + }); + if (checksum[0] != bytes[34] || + checksum[1] != bytes[35] || + checksum[2] != bytes[36] || + checksum[3] != bytes[37]) { + throw "Checksum validation failed!"; + } + var version = hash.shift(); + /* + if (version != ECKey.privateKeyPrefix) { + throw "Version " + version + " not supported!"; + } + */ + hash.pop(); + return hash; + }; + + // 64 characters [0-9A-F] + ECKey.isHexFormat = function (key) { + key = key.toString(); + return /^[A-Fa-f0-9]{64}$/.test(key); + }; + + // 51 characters base58, always starts with a '5' + ECKey.isWalletImportFormat = function (key) { + key = key.toString(); + return (ECKey.privateKeyPrefix == 0x80) ? + (/^5[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{50}$/.test(key)) : + (/^R[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{50}$/.test(key)); + }; + + // 52 characters base58 + ECKey.isCompressedWalletImportFormat = function (key) { + key = key.toString(); + return (ECKey.privateKeyPrefix == 0x80) ? + (/^[LK][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{51}$/.test(key)) : + (/^R[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{51}$/.test(key)); + }; + + // 44 characters + ECKey.isBase64Format = function (key) { + key = key.toString(); + return (/^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789=+\/]{44}$/.test(key)); + }; + + // 99 characters, 1=1, if using dice convert 6 to 0 + ECKey.isBase6Format = function (key) { + key = key.toString(); + return (/^[012345]{99}$/.test(key)); + }; + + // 22, 26 or 30 characters, always starts with an 'S' + ECKey.isMiniFormat = function (key) { + key = key.toString(); + var validChars22 = /^S[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21}$/.test(key); + var validChars26 = /^S[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{25}$/.test(key); + var validChars30 = /^S[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{29}$/.test(key); + var testBytes = Crypto.SHA256(key + "?", { + asBytes: true + }); + + return ((testBytes[0] === 0x00 || testBytes[0] === 0x01) && (validChars22 || validChars26 || validChars30)); + }; + + return ECKey; + })(); + //https://raw.github.com/bitcoinjs/bitcoinjs-lib/09e8c6e184d6501a0c2c59d73ca64db5c0d3eb95/src/util.js + // Bitcoin utility functions + Bitcoin.Util = { + /** + * Cross-browser compatibility version of Array.isArray. + */ + isArray: Array.isArray || function (o) { + return Object.prototype.toString.call(o) === '[object Array]'; + }, + /** + * Create an array of a certain length filled with a specific value. + */ + makeFilledArray: function (len, val) { + var array = []; + var i = 0; + while (i < len) { + array[i++] = val; + } + return array; + }, + /** + * Turn an integer into a "var_int". + * + * "var_int" is a variable length integer used by Bitcoin's binary format. + * + * Returns a byte array. + */ + numToVarInt: function (i) { + if (i < 0xfd) { + // unsigned char + return [i]; + } else if (i <= 1 << 16) { + // unsigned short (LE) + return [0xfd, i >>> 8, i & 255]; + } else if (i <= 1 << 32) { + // unsigned int (LE) + return [0xfe].concat(Crypto.util.wordsToBytes([i])); + } else { + // unsigned long long (LE) + return [0xff].concat(Crypto.util.wordsToBytes([i >>> 32, i])); + } + }, + /** + * Parse a Bitcoin value byte array, returning a BigInteger. + */ + valueToBigInt: function (valueBuffer) { + if (valueBuffer instanceof BigInteger) return valueBuffer; + + // Prepend zero byte to prevent interpretation as negative integer + return BigInteger.fromByteArrayUnsigned(valueBuffer); + }, + /** + * Format a Bitcoin value as a string. + * + * Takes a BigInteger or byte-array and returns that amount of Bitcoins in a + * nice standard formatting. + * + * Examples: + * 12.3555 + * 0.1234 + * 900.99998888 + * 34.00 + */ + formatValue: function (valueBuffer) { + var value = this.valueToBigInt(valueBuffer).toString(); + var integerPart = value.length > 8 ? value.substr(0, value.length - 8) : '0'; + var decimalPart = value.length > 8 ? value.substr(value.length - 8) : value; + while (decimalPart.length < 8) decimalPart = "0" + decimalPart; + decimalPart = decimalPart.replace(/0*$/, ''); + while (decimalPart.length < 2) decimalPart += "0"; + return integerPart + "." + decimalPart; + }, + /** + * Parse a floating point string as a Bitcoin value. + * + * Keep in mind that parsing user input is messy. You should always display + * the parsed value back to the user to make sure we understood his input + * correctly. + */ + parseValue: function (valueString) { + // TODO: Detect other number formats (e.g. comma as decimal separator) + var valueComp = valueString.split('.'); + var integralPart = valueComp[0]; + var fractionalPart = valueComp[1] || "0"; + while (fractionalPart.length < 8) fractionalPart += "0"; + fractionalPart = fractionalPart.replace(/^0+/g, ''); + var value = BigInteger.valueOf(parseInt(integralPart)); + value = value.multiply(BigInteger.valueOf(100000000)); + value = value.add(BigInteger.valueOf(parseInt(fractionalPart))); + return value; + }, + /** + * Calculate RIPEMD160(SHA256(data)). + * + * Takes an arbitrary byte array as inputs and returns the hash as a byte + * array. + */ + sha256ripe160: function (data) { + return ripemd160(Crypto.SHA256(data, { + asBytes: true + }), { + asBytes: true + }); + }, + // double sha256 + dsha256: function (data) { + return Crypto.SHA256(Crypto.SHA256(data, { + asBytes: true + }), { + asBytes: true + }); + }, + // duck typing method + hasMethods: function (obj /*, method list as strings */) { + var i = 1, + methodName; + while ((methodName = arguments[i++])) { + if (typeof obj[methodName] != 'function') { + return false; + } + } + return true; + } + }; + })(); + + //ellipticCurveEncryption.js + (function () { + (function (ellipticCurveType) { + + //Defining Elliptic Encryption Object + var ellipticEncryption = GLOBAL.ellipticCurveEncryption = function () { }; + + ellipticEncryption.rng = new SecureRandom(); + + ellipticEncryption.getCurveParameters = function (curveName) { + + //Default is secp256k1 + curveName = typeof curveName !== 'undefined' ? curveName : "secp256k1"; + + var c = EllipticCurve.getSECCurveByName(curveName); + var curveDetails = { + Q: "", + A: "", + B: "", + GX: "", + GY: "", + N: "" + }; + + curveDetails.Q = c.getCurve().getQ().toString(); + curveDetails.A = c.getCurve().getA().toBigInteger().toString(); + curveDetails.B = c.getCurve().getB().toBigInteger().toString(); + curveDetails.GX = c.getG().getX().toBigInteger().toString(); + curveDetails.GY = c.getG().getY().toBigInteger().toString(); + curveDetails.N = c.getN().toString(); + + return curveDetails; + + } + + ellipticEncryption.selectedCurve = ellipticEncryption.getCurveParameters(ellipticCurveType); + + ellipticEncryption.get_curve = function () { + return new EllipticCurve.CurveFp(new BigInteger(this.selectedCurve.Q), + new BigInteger(this.selectedCurve.A), + new BigInteger(this.selectedCurve.B)); + } + + ellipticEncryption.get_G = function (curve) { + return new EllipticCurve.PointFp(curve, + curve.fromBigInteger(new BigInteger(this.selectedCurve.GX)), + curve.fromBigInteger(new BigInteger(this.selectedCurve.GY))); + } + + ellipticEncryption.pick_rand = function () { + var n = new BigInteger(this.selectedCurve.N); + var n1 = n.subtract(BigInteger.ONE); + var r = new BigInteger(n.bitLength(), this.rng); + return r.mod(n1).add(BigInteger.ONE); + } + + ellipticEncryption.senderRandom = function () { + var r = this.pick_rand(); + return r.toString(); + }; + + ellipticEncryption.receiverRandom = function () { + + //This is receivers private key. For now we will use random. CHANGE IT LATER + var r = this.pick_rand(); + return r.toString(); + } + + ellipticEncryption.senderPublicString = function (senderPrivateKey) { + + var senderKeyECData = {}; + + var curve = this.get_curve(); + var G = this.get_G(curve); + var a = new BigInteger(senderPrivateKey); + var P = G.multiply(a); + senderKeyECData.XValuePublicString = P.getX().toBigInteger().toString(); + senderKeyECData.YValuePublicString = P.getY().toBigInteger().toString(); + + return senderKeyECData; + } + + //In real life ellipticEncryption.receiverPublicString is the public key of the receiver. + //you don't have to run receiverRandom and the bottom function + ellipticEncryption.receiverPublicString = function (receiverPublicKey) { + + var receiverKeyECData = {}; + + var curve = this.get_curve(); + var G = this.get_G(curve); + var a = new BigInteger(receiverPublicKey); + var P = G.multiply(a); + receiverKeyECData.XValuePublicString = P.getX().toBigInteger().toString(); + receiverKeyECData.YValuePublicString = P.getY().toBigInteger().toString(); + + return receiverKeyECData; + } + + ellipticEncryption.senderSharedKeyDerivation = function (receiverPublicStringXValue, + receiverPublicStringYValue, senderPrivateKey) { + + var senderDerivedKey = {}; + var curve = this.get_curve(); + var P = new EllipticCurve.PointFp(curve, + curve.fromBigInteger(new BigInteger(receiverPublicStringXValue)), + curve.fromBigInteger(new BigInteger(receiverPublicStringYValue))); + var a = new BigInteger(senderPrivateKey); + var S = P.multiply(a); + + senderDerivedKey.XValue = S.getX().toBigInteger().toString(); + senderDerivedKey.YValue = S.getY().toBigInteger().toString(); + + return senderDerivedKey; + } + + ellipticEncryption.receiverSharedKeyDerivation = function (senderPublicStringXValue, + senderPublicStringYValue, receiverPrivateKey) { + + var receiverDerivedKey = {}; + var curve = this.get_curve(); + var P = new EllipticCurve.PointFp(curve, + curve.fromBigInteger(new BigInteger(senderPublicStringXValue)), + curve.fromBigInteger(new BigInteger(senderPublicStringYValue))); + var a = new BigInteger(receiverPrivateKey); + var S = P.multiply(a); + + receiverDerivedKey.XValue = S.getX().toBigInteger().toString(); + receiverDerivedKey.YValue = S.getY().toBigInteger().toString(); + + return receiverDerivedKey; + } + + })("secp256k1"); + })(); + + //sha512.js + (function () { + /* + A JavaScript implementation of the SHA family of hashes, as defined in FIPS + PUB 180-2 as well as the corresponding HMAC implementation as defined in + FIPS PUB 198a + + Copyright Brian Turek 2008-2012 + Distributed under the BSD License + See http://caligatio.github.com/jsSHA/ for more information + + Several functions taken from Paul Johnson + */ + function n(a) { + throw a; + } + var q = null; + + function s(a, b) { + this.a = a; + this.b = b + } + + function u(a, b) { + var d = [], + h = (1 << b) - 1, + f = a.length * b, + g; + for (g = 0; g < f; g += b) d[g >>> 5] |= (a.charCodeAt(g / b) & h) << 32 - b - g % 32; + return { + value: d, + binLen: f + } + } + + function x(a) { + var b = [], + d = a.length, + h, f; + 0 !== d % 2 && n("String of HEX type must be in byte increments"); + for (h = 0; h < d; h += 2) f = parseInt(a.substr(h, 2), 16), isNaN(f) && n("String of HEX type contains invalid characters"), b[h >>> 3] |= f << 24 - 4 * (h % 8); + return { + value: b, + binLen: 4 * d + } + } + + function B(a) { + var b = [], + d = 0, + h, f, g, k, m; - 1 === a.search(/^[a-zA-Z0-9=+\/]+$/) && n("Invalid character in base-64 string"); + h = a.indexOf("="); + a = a.replace(/\=/g, ""); - 1 !== h && h < a.length && n("Invalid '=' found in base-64 string"); + for (f = 0; f < a.length; f += 4) { + m = a.substr(f, 4); + for (g = k = 0; g < m.length; g += 1) h = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(m[g]), k |= h << 18 - 6 * g; + for (g = 0; g < m.length - 1; g += 1) b[d >> 2] |= (k >>> 16 - 8 * g & 255) << 24 - 8 * (d % 4), d += 1 + } + return { + value: b, + binLen: 8 * d + } + } + + function E(a, b) { + var d = "", + h = 4 * a.length, + f, g; + for (f = 0; f < h; f += 1) g = a[f >>> 2] >>> 8 * (3 - f % 4), d += "0123456789abcdef".charAt(g >>> 4 & 15) + "0123456789abcdef".charAt(g & 15); + return b.outputUpper ? d.toUpperCase() : d + } + + function F(a, b) { + var d = "", + h = 4 * a.length, + f, g, k; + for (f = 0; f < h; f += 3) { + k = (a[f >>> 2] >>> 8 * (3 - f % 4) & 255) << 16 | (a[f + 1 >>> 2] >>> 8 * (3 - (f + 1) % 4) & 255) << 8 | a[f + 2 >>> 2] >>> 8 * (3 - (f + 2) % 4) & 255; + for (g = 0; 4 > g; g += 1) d = 8 * f + 6 * g <= 32 * a.length ? d + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(k >>> 6 * (3 - g) & 63) : d + b.b64Pad + } + return d + } + + function G(a) { + var b = { + outputUpper: !1, + b64Pad: "=" + }; + try { + a.hasOwnProperty("outputUpper") && (b.outputUpper = a.outputUpper), a.hasOwnProperty("b64Pad") && (b.b64Pad = a.b64Pad) + } catch (d) { } + "boolean" !== typeof b.outputUpper && n("Invalid outputUpper formatting option"); + "string" !== typeof b.b64Pad && n("Invalid b64Pad formatting option"); + return b + } + + function H(a, b) { + var d = q, + d = new s(a.a, a.b); + return d = 32 >= b ? new s(d.a >>> b | d.b << 32 - b & 4294967295, d.b >>> b | d.a << 32 - b & 4294967295) : new s(d.b >>> b - 32 | d.a << 64 - b & 4294967295, d.a >>> b - 32 | d.b << 64 - b & 4294967295) + } + + function I(a, b) { + var d = q; + return d = 32 >= b ? new s(a.a >>> b, a.b >>> b | a.a << 32 - b & 4294967295) : new s(0, a.a >>> b - 32) + } + + function J(a, b, d) { + return new s(a.a & b.a ^ ~a.a & d.a, a.b & b.b ^ ~a.b & d.b) + } + + function U(a, b, d) { + return new s(a.a & b.a ^ a.a & d.a ^ b.a & d.a, a.b & b.b ^ a.b & d.b ^ b.b & d.b) + } + + function V(a) { + var b = H(a, 28), + d = H(a, 34); + a = H(a, 39); + return new s(b.a ^ d.a ^ a.a, b.b ^ d.b ^ a.b) + } + + function W(a) { + var b = H(a, 14), + d = H(a, 18); + a = H(a, 41); + return new s(b.a ^ d.a ^ a.a, b.b ^ d.b ^ a.b) + } + + function X(a) { + var b = H(a, 1), + d = H(a, 8); + a = I(a, 7); + return new s(b.a ^ d.a ^ a.a, b.b ^ d.b ^ a.b) + } + + function Y(a) { + var b = H(a, 19), + d = H(a, 61); + a = I(a, 6); + return new s(b.a ^ d.a ^ a.a, b.b ^ d.b ^ a.b) + } + + function Z(a, b) { + var d, h, f; + d = (a.b & 65535) + (b.b & 65535); + h = (a.b >>> 16) + (b.b >>> 16) + (d >>> 16); + f = (h & 65535) << 16 | d & 65535; + d = (a.a & 65535) + (b.a & 65535) + (h >>> 16); + h = (a.a >>> 16) + (b.a >>> 16) + (d >>> 16); + return new s((h & 65535) << 16 | d & 65535, f) + } + + function aa(a, b, d, h) { + var f, g, k; + f = (a.b & 65535) + (b.b & 65535) + (d.b & 65535) + (h.b & 65535); + g = (a.b >>> 16) + (b.b >>> 16) + (d.b >>> 16) + (h.b >>> 16) + (f >>> 16); + k = (g & 65535) << 16 | f & 65535; + f = (a.a & 65535) + (b.a & 65535) + (d.a & 65535) + (h.a & 65535) + (g >>> 16); + g = (a.a >>> 16) + (b.a >>> 16) + (d.a >>> 16) + (h.a >>> 16) + (f >>> 16); + return new s((g & 65535) << 16 | f & 65535, k) + } + + function ba(a, b, d, h, f) { + var g, k, m; + g = (a.b & 65535) + (b.b & 65535) + (d.b & 65535) + (h.b & 65535) + (f.b & 65535); + k = (a.b >>> 16) + (b.b >>> 16) + (d.b >>> 16) + (h.b >>> 16) + (f.b >>> 16) + (g >>> 16); + m = (k & 65535) << 16 | g & 65535; + g = (a.a & 65535) + (b.a & 65535) + (d.a & 65535) + (h.a & 65535) + (f.a & 65535) + (k >>> 16); + k = (a.a >>> 16) + (b.a >>> 16) + (d.a >>> 16) + (h.a >>> 16) + (f.a >>> 16) + (g >>> 16); + return new s((k & 65535) << 16 | g & 65535, m) + } + + function $(a, b, d) { + var h, f, g, k, m, j, A, C, K, e, L, v, l, M, t, p, y, z, r, N, O, P, Q, R, c, S, w = [], + T, D; + "SHA-384" === d || "SHA-512" === d ? (L = 80, h = (b + 128 >>> 10 << 5) + 31, M = 32, t = 2, c = s, p = Z, y = aa, z = ba, r = X, N = Y, O = V, P = W, R = U, Q = J, S = [new c(1116352408, 3609767458), new c(1899447441, 602891725), new c(3049323471, 3964484399), new c(3921009573, 2173295548), new c(961987163, 4081628472), new c(1508970993, 3053834265), new c(2453635748, 2937671579), new c(2870763221, 3664609560), new c(3624381080, 2734883394), new c(310598401, 1164996542), new c(607225278, 1323610764), + new c(1426881987, 3590304994), new c(1925078388, 4068182383), new c(2162078206, 991336113), new c(2614888103, 633803317), new c(3248222580, 3479774868), new c(3835390401, 2666613458), new c(4022224774, 944711139), new c(264347078, 2341262773), new c(604807628, 2007800933), new c(770255983, 1495990901), new c(1249150122, 1856431235), new c(1555081692, 3175218132), new c(1996064986, 2198950837), new c(2554220882, 3999719339), new c(2821834349, 766784016), new c(2952996808, 2566594879), new c(3210313671, 3203337956), new c(3336571891, + 1034457026), new c(3584528711, 2466948901), new c(113926993, 3758326383), new c(338241895, 168717936), new c(666307205, 1188179964), new c(773529912, 1546045734), new c(1294757372, 1522805485), new c(1396182291, 2643833823), new c(1695183700, 2343527390), new c(1986661051, 1014477480), new c(2177026350, 1206759142), new c(2456956037, 344077627), new c(2730485921, 1290863460), new c(2820302411, 3158454273), new c(3259730800, 3505952657), new c(3345764771, 106217008), new c(3516065817, 3606008344), new c(3600352804, 1432725776), new c(4094571909, + 1467031594), new c(275423344, 851169720), new c(430227734, 3100823752), new c(506948616, 1363258195), new c(659060556, 3750685593), new c(883997877, 3785050280), new c(958139571, 3318307427), new c(1322822218, 3812723403), new c(1537002063, 2003034995), new c(1747873779, 3602036899), new c(1955562222, 1575990012), new c(2024104815, 1125592928), new c(2227730452, 2716904306), new c(2361852424, 442776044), new c(2428436474, 593698344), new c(2756734187, 3733110249), new c(3204031479, 2999351573), new c(3329325298, 3815920427), new c(3391569614, + 3928383900), new c(3515267271, 566280711), new c(3940187606, 3454069534), new c(4118630271, 4000239992), new c(116418474, 1914138554), new c(174292421, 2731055270), new c(289380356, 3203993006), new c(460393269, 320620315), new c(685471733, 587496836), new c(852142971, 1086792851), new c(1017036298, 365543100), new c(1126000580, 2618297676), new c(1288033470, 3409855158), new c(1501505948, 4234509866), new c(1607167915, 987167468), new c(1816402316, 1246189591) + ], e = "SHA-384" === d ? [new c(3418070365, 3238371032), new c(1654270250, 914150663), + new c(2438529370, 812702999), new c(355462360, 4144912697), new c(1731405415, 4290775857), new c(41048885895, 1750603025), new c(3675008525, 1694076839), new c(1203062813, 3204075428) + ] : [new c(1779033703, 4089235720), new c(3144134277, 2227873595), new c(1013904242, 4271175723), new c(2773480762, 1595750129), new c(1359893119, 2917565137), new c(2600822924, 725511199), new c(528734635, 4215389547), new c(1541459225, 327033209)]) : n("Unexpected error in SHA-2 implementation"); + a[b >>> 5] |= 128 << 24 - b % 32; + a[h] = b; + T = a.length; + for (v = 0; v < + T; v += M) { + b = e[0]; + h = e[1]; + f = e[2]; + g = e[3]; + k = e[4]; + m = e[5]; + j = e[6]; + A = e[7]; + for (l = 0; l < L; l += 1) w[l] = 16 > l ? new c(a[l * t + v], a[l * t + v + 1]) : y(N(w[l - 2]), w[l - 7], r(w[l - 15]), w[l - 16]), C = z(A, P(k), Q(k, m, j), S[l], w[l]), K = p(O(b), R(b, h, f)), A = j, j = m, m = k, k = p(g, C), g = f, f = h, h = b, b = p(C, K); + e[0] = p(b, e[0]); + e[1] = p(h, e[1]); + e[2] = p(f, e[2]); + e[3] = p(g, e[3]); + e[4] = p(k, e[4]); + e[5] = p(m, e[5]); + e[6] = p(j, e[6]); + e[7] = p(A, e[7]) + } + "SHA-384" === d ? D = [e[0].a, e[0].b, e[1].a, e[1].b, e[2].a, e[2].b, e[3].a, e[3].b, e[4].a, e[4].b, e[5].a, e[5].b] : "SHA-512" === d ? D = [e[0].a, e[0].b, + e[1].a, e[1].b, e[2].a, e[2].b, e[3].a, e[3].b, e[4].a, e[4].b, e[5].a, e[5].b, e[6].a, e[6].b, e[7].a, e[7].b + ] : n("Unexpected error in SHA-2 implementation"); + return D + } + GLOBAL.jsSHA = function (a, b, d) { + var h = q, + f = q, + g = 0, + k = [0], + m = 0, + j = q, + m = "undefined" !== typeof d ? d : 8; + 8 === m || 16 === m || n("charSize must be 8 or 16"); + "HEX" === b ? (0 !== a.length % 2 && n("srcString of HEX type must be in byte increments"), j = x(a), g = j.binLen, k = j.value) : "ASCII" === b || "TEXT" === b ? (j = u(a, m), g = j.binLen, k = j.value) : "B64" === b ? (j = B(a), g = j.binLen, k = j.value) : n("inputFormat must be HEX, TEXT, ASCII, or B64"); + this.getHash = function (a, b, d) { + var e = q, + m = k.slice(), + j = ""; + switch (b) { + case "HEX": + e = E; + break; + case "B64": + e = F; + break; + default: + n("format must be HEX or B64") + } + "SHA-384" === + a ? (q === h && (h = $(m, g, a)), j = e(h, G(d))) : "SHA-512" === a ? (q === f && (f = $(m, g, a)), j = e(f, G(d))) : n("Chosen SHA variant is not supported"); + return j + }; + this.getHMAC = function (a, b, d, e, f) { + var h, l, j, t, p, y = [], + z = [], + r = q; + switch (e) { + case "HEX": + h = E; + break; + case "B64": + h = F; + break; + default: + n("outputFormat must be HEX or B64") + } + "SHA-384" === d ? (j = 128, p = 384) : "SHA-512" === d ? (j = 128, p = 512) : n("Chosen SHA variant is not supported"); + "HEX" === b ? (r = x(a), t = r.binLen, l = r.value) : "ASCII" === b || "TEXT" === b ? (r = u(a, m), t = r.binLen, l = r.value) : "B64" === b ? (r = B(a), + t = r.binLen, l = r.value) : n("inputFormat must be HEX, TEXT, ASCII, or B64"); + a = 8 * j; + b = j / 4 - 1; + j < t / 8 ? (l = $(l, t, d), l[b] &= 4294967040) : j > t / 8 && (l[b] &= 4294967040); + for (j = 0; j <= b; j += 1) y[j] = l[j] ^ 909522486, z[j] = l[j] ^ 1549556828; + d = $(z.concat($(y.concat(k), a + g, d)), a + p, d); + return h(d, G(f)) + } + }; + })(); + + //coin.js + (function () { + /* + Coinjs 0.01 beta by OutCast3k{at}gmail.com + A bitcoin framework. + http://github.com/OutCast3k/coinjs or http://coinb.in/coinjs + */ + var coinjs = GLOBAL.coinjs = function () { }; + + /* public vars */ + coinjs.pub = 0x00; + coinjs.priv = 0x80; + coinjs.multisig = 0x05; + coinjs.hdkey = { + 'prv': 0x0488ade4, + 'pub': 0x0488b21e + }; + coinjs.bech32 = { + 'charset': 'qpzry9x8gf2tvdw0s3jn54khce6mua7l', + 'version': 0, + 'hrp': 'bc' + }; + + coinjs.compressed = false; + + /* other vars */ + coinjs.developer = '33tht1bKDgZVxb39MnZsWa8oxHXHvUYE4G'; //bitcoin + + /* bit(coinb.in) api vars + coinjs.hostname = ((document.location.hostname.split(".")[(document.location.hostname.split(".")).length - 1]) == 'onion') ? 'coinbin3ravkwb24f7rmxx6w3snkjw45jhs5lxbh3yfeg3vpt6janwqd.onion' : 'coinb.in'; + coinjs.host = ('https:' == document.location.protocol ? 'https://' : 'http://') + coinjs.hostname + '/api/'; + coinjs.uid = '1'; + coinjs.key = '12345678901234567890123456789012'; + */ + + /* start of address functions */ + + /* generate a private and public keypair, with address and WIF address */ + coinjs.newKeys = function (input) { + var privkey = (input) ? Crypto.SHA256(input) : this.newPrivkey(); + var pubkey = this.newPubkey(privkey); + return { + 'privkey': privkey, + 'pubkey': pubkey, + 'address': this.pubkey2address(pubkey), + 'wif': this.privkey2wif(privkey), + 'compressed': this.compressed + }; + } + + /* generate a new random private key */ + coinjs.newPrivkey = function () { + var x = GLOBAL.location; + x += (GLOBAL.screen.height * GLOBAL.screen.width * GLOBAL.screen.colorDepth); + x += coinjs.random(64); + x += (GLOBAL.screen.availHeight * GLOBAL.screen.availWidth * GLOBAL.screen.pixelDepth); + x += navigator.language; + x += GLOBAL.history.length; + x += coinjs.random(64); + x += navigator.userAgent; + x += 'coinb.in'; + x += (Crypto.util.randomBytes(64)).join(""); + x += x.length; + var dateObj = new Date(); + x += dateObj.getTimezoneOffset(); + x += coinjs.random(64); + x += (document.getElementById("entropybucket")) ? document.getElementById("entropybucket").innerHTML : ''; + x += x + '' + x; + var r = x; + for (let i = 0; i < (x).length / 25; i++) { + r = Crypto.SHA256(r.concat(x)); + } + var checkrBigInt = new BigInteger(r); + var orderBigInt = new BigInteger("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + while (checkrBigInt.compareTo(orderBigInt) >= 0 || checkrBigInt.equals(BigInteger.ZERO) || checkrBigInt.equals(BigInteger.ONE)) { + r = Crypto.SHA256(r.concat(x)); + checkrBigInt = new BigInteger(r); + } + return r; + } + + /* generate a public key from a private key */ + coinjs.newPubkey = function (hash) { + var privateKeyBigInt = BigInteger.fromByteArrayUnsigned(Crypto.util.hexToBytes(hash)); + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + + var curvePt = curve.getG().multiply(privateKeyBigInt); + var x = curvePt.getX().toBigInteger(); + var y = curvePt.getY().toBigInteger(); + + var publicKeyBytes = EllipticCurve.integerToBytes(x, 32); + publicKeyBytes = publicKeyBytes.concat(EllipticCurve.integerToBytes(y, 32)); + publicKeyBytes.unshift(0x04); + + if (coinjs.compressed == true) { + var publicKeyBytesCompressed = EllipticCurve.integerToBytes(x, 32) + if (y.isEven()) { + publicKeyBytesCompressed.unshift(0x02) + } else { + publicKeyBytesCompressed.unshift(0x03) + } + return Crypto.util.bytesToHex(publicKeyBytesCompressed); + } else { + return Crypto.util.bytesToHex(publicKeyBytes); + } + } + + /* provide a public key and return address */ + coinjs.pubkey2address = function (h, byte) { + var r = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(h), { + asBytes: true + })); + r.unshift(byte || coinjs.pub); + var hash = Crypto.SHA256(Crypto.SHA256(r, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + return coinjs.base58encode(r.concat(checksum)); + } + + /* provide a scripthash and return address */ + coinjs.scripthash2address = function (h) { + var x = Crypto.util.hexToBytes(h); + x.unshift(coinjs.pub); + var r = x; + r = Crypto.SHA256(Crypto.SHA256(r, { + asBytes: true + }), { + asBytes: true + }); + var checksum = r.slice(0, 4); + return coinjs.base58encode(x.concat(checksum)); + } + + /* new multisig address, provide the pubkeys AND required signatures to release the funds */ + coinjs.pubkeys2MultisigAddress = function (pubkeys, required) { + var s = coinjs.script(); + s.writeOp(81 + (required * 1) - 1); //OP_1 + for (var i = 0; i < pubkeys.length; ++i) { + s.writeBytes(Crypto.util.hexToBytes(pubkeys[i])); + } + s.writeOp(81 + pubkeys.length - 1); //OP_1 + s.writeOp(174); //OP_CHECKMULTISIG + var x = ripemd160(Crypto.SHA256(s.buffer, { + asBytes: true + }), { + asBytes: true + }); + x.unshift(coinjs.multisig); + var r = x; + r = Crypto.SHA256(Crypto.SHA256(r, { + asBytes: true + }), { + asBytes: true + }); + var checksum = r.slice(0, 4); + var redeemScript = Crypto.util.bytesToHex(s.buffer); + var address = coinjs.base58encode(x.concat(checksum)); + + if (s.buffer.length > 520) { // too large + address = 'invalid'; + redeemScript = 'invalid'; + } + + return { + 'address': address, + 'redeemScript': redeemScript, + 'size': s.buffer.length + }; + } + + //Return a Bech32 address for the multisig. Format is same as above + coinjs.pubkeys2MultisigAddressBech32 = function (pubkeys, required) { + var r = coinjs.pubkeys2MultisigAddress(pubkeys, required); + var program = Crypto.SHA256(Crypto.util.hexToBytes(r.redeemScript), { + asBytes: true + }); + var address = coinjs.bech32_encode(coinjs.bech32.hrp, [coinjs.bech32.version].concat(coinjs.bech32_convert(program, 8, 5, true))); + return { + 'address': address, + 'redeemScript': r.redeemScript, + 'scripthash': Crypto.util.bytesToHex(program), + 'size': r.size + }; + } + + /* new time locked address, provide the pubkey and time necessary to unlock the funds. + when time is greater than 500000000, it should be a unix timestamp (seconds since epoch), + otherwise it should be the block height required before this transaction can be released. + + may throw a string on failure! + */ + coinjs.simpleHodlAddress = function (pubkey, checklocktimeverify) { + + if (checklocktimeverify < 0) { + throw "Parameter for OP_CHECKLOCKTIMEVERIFY is negative."; + } + + var s = coinjs.script(); + if (checklocktimeverify <= 16 && checklocktimeverify >= 1) { + s.writeOp(0x50 + checklocktimeverify); //OP_1 to OP_16 for minimal encoding + } else { + s.writeBytes(coinjs.numToScriptNumBytes(checklocktimeverify)); + } + s.writeOp(177); //OP_CHECKLOCKTIMEVERIFY + s.writeOp(117); //OP_DROP + s.writeBytes(Crypto.util.hexToBytes(pubkey)); + s.writeOp(172); //OP_CHECKSIG + + var x = ripemd160(Crypto.SHA256(s.buffer, { + asBytes: true + }), { + asBytes: true + }); + x.unshift(coinjs.multisig); + var r = x; + r = Crypto.SHA256(Crypto.SHA256(r, { + asBytes: true + }), { + asBytes: true + }); + var checksum = r.slice(0, 4); + var redeemScript = Crypto.util.bytesToHex(s.buffer); + var address = coinjs.base58encode(x.concat(checksum)); + + return { + 'address': address, + 'redeemScript': redeemScript + }; + } + + /* create a new segwit address */ + coinjs.segwitAddress = function (pubkey) { + var keyhash = [0x00, 0x14].concat(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubkey), { + asBytes: true + }), { + asBytes: true + })); + var x = ripemd160(Crypto.SHA256(keyhash, { + asBytes: true + }), { + asBytes: true + }); + x.unshift(coinjs.multisig); + var r = x; + r = Crypto.SHA256(Crypto.SHA256(r, { + asBytes: true + }), { + asBytes: true + }); + var checksum = r.slice(0, 4); + var address = coinjs.base58encode(x.concat(checksum)); + + return { + 'address': address, + 'type': 'segwit', + 'redeemscript': Crypto.util.bytesToHex(keyhash) + }; + } + + /* create a new segwit bech32 encoded address */ + coinjs.bech32Address = function (pubkey) { + var program = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubkey), { + asBytes: true + }), { + asBytes: true + }); + var address = coinjs.bech32_encode(coinjs.bech32.hrp, [coinjs.bech32.version].concat(coinjs.bech32_convert(program, 8, 5, true))); + return { + 'address': address, + 'type': 'bech32', + 'redeemscript': Crypto.util.bytesToHex(program) + }; + } + + coinjs.multisigBech32Address = function (redeemscript) { + var program = Crypto.SHA256(Crypto.util.hexToBytes(redeemscript), { + asBytes: true + }); + var address = coinjs.bech32_encode(coinjs.bech32.hrp, [coinjs.bech32.version].concat(coinjs.bech32_convert(program, 8, 5, true))); + return { + 'address': address, + 'type': 'multisigBech32', + 'redeemScript': redeemscript, + 'scripthash': Crypto.util.bytesToHex(program) + }; + } + + /* extract the redeemscript from a bech32 address */ + coinjs.bech32redeemscript = function (address) { + var r = false; + var decode = coinjs.bech32_decode(address); + if (decode) { + decode.data.shift(); + return Crypto.util.bytesToHex(coinjs.bech32_convert(decode.data, 5, 8, false)); + } + return r; + } + + /* provide a privkey and return an WIF */ + coinjs.privkey2wif = function (h) { + var r = Crypto.util.hexToBytes(h); + + if (coinjs.compressed == true) { + r.push(0x01); + } + + r.unshift(coinjs.priv); + var hash = Crypto.SHA256(Crypto.SHA256(r, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + + return coinjs.base58encode(r.concat(checksum)); + } + + /* convert a wif key back to a private key */ + coinjs.wif2privkey = function (wif) { + var compressed = false; + var decode = coinjs.base58decode(wif); + var key = decode.slice(0, decode.length - 4); + key = key.slice(1, key.length); + if (key.length >= 33 && key[key.length - 1] == 0x01) { + key = key.slice(0, key.length - 1); + compressed = true; + } + return { + 'privkey': Crypto.util.bytesToHex(key), + 'compressed': compressed + }; + } + + /* convert a wif to a pubkey */ + coinjs.wif2pubkey = function (wif) { + var compressed = coinjs.compressed; + var r = coinjs.wif2privkey(wif); + coinjs.compressed = r['compressed']; + var pubkey = coinjs.newPubkey(r['privkey']); + coinjs.compressed = compressed; + return { + 'pubkey': pubkey, + 'compressed': r['compressed'] + }; + } + + /* convert a wif to a address */ + coinjs.wif2address = function (wif) { + var r = coinjs.wif2pubkey(wif); + return { + 'address': coinjs.pubkey2address(r['pubkey']), + 'compressed': r['compressed'] + }; + } + + /* decode or validate an address and return the hash */ + coinjs.addressDecode = function (addr) { + try { + var bytes = coinjs.base58decode(addr); + var front = bytes.slice(0, bytes.length - 4); + var back = bytes.slice(bytes.length - 4); + var checksum = Crypto.SHA256(Crypto.SHA256(front, { + asBytes: true + }), { + asBytes: true + }).slice(0, 4); + if (checksum + "" == back + "") { + + var o = {}; + o.bytes = front.slice(1); + o.version = front[0]; + + if (o.version == coinjs.pub) { // standard address + o.type = 'standard'; + + } else if (o.version == coinjs.multisig) { // multisig address + o.type = 'multisig'; + + } else if (o.version == coinjs.multisigBech32) { // multisigBech32 added + o.type = 'multisigBech32'; + + } else if (o.version == coinjs.priv) { // wifkey + o.type = 'wifkey'; + + } else if (o.version == 42) { // stealth address + o.type = 'stealth'; + + o.option = front[1]; + if (o.option != 0) { + alert("Stealth Address option other than 0 is currently not supported!"); + return false; + }; + + o.scankey = Crypto.util.bytesToHex(front.slice(2, 35)); + o.n = front[35]; + + if (o.n > 1) { + alert("Stealth Multisig is currently not supported!"); + return false; + }; + + o.spendkey = Crypto.util.bytesToHex(front.slice(36, 69)); + o.m = front[69]; + o.prefixlen = front[70]; + + if (o.prefixlen > 0) { + alert("Stealth Address Prefixes are currently not supported!"); + return false; + }; + o.prefix = front.slice(71); + + } else { // everything else + o.type = 'other'; // address is still valid but unknown version + } + + return o; + } else { + throw "Invalid checksum"; + } + } catch (e) { + let bech32rs = coinjs.bech32redeemscript(addr); + if (bech32rs && bech32rs.length == 40) { + return { + 'type': 'bech32', + 'redeemscript': bech32rs + }; + } else if (bech32rs && bech32rs.length == 64) { + return { + 'type': 'multisigBech32', + 'redeemscript': bech32rs + }; + } else { + return false; + } + } + } + + /* retreive the balance from a given address */ + coinjs.addressBalance = function (address, callback) { + coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=addresses&request=bal&address=' + address + '&r=' + Math.random(), callback, "GET"); + } + + /* decompress an compressed public key */ + coinjs.pubkeydecompress = function (pubkey) { + if ((typeof (pubkey) == 'string') && pubkey.match(/^[a-f0-9]+$/i)) { + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + try { + var pt = curve.curve.decodePointHex(pubkey); + var x = pt.getX().toBigInteger(); + var y = pt.getY().toBigInteger(); + + var publicKeyBytes = EllipticCurve.integerToBytes(x, 32); + publicKeyBytes = publicKeyBytes.concat(EllipticCurve.integerToBytes(y, 32)); + publicKeyBytes.unshift(0x04); + return Crypto.util.bytesToHex(publicKeyBytes); + } catch (e) { + // console.log(e); + return false; + } + } + return false; + } + + coinjs.bech32_polymod = function (values) { + var chk = 1; + var BECH32_GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; + for (var p = 0; p < values.length; ++p) { + var top = chk >> 25; + chk = (chk & 0x1ffffff) << 5 ^ values[p]; + for (var i = 0; i < 5; ++i) { + if ((top >> i) & 1) { + chk ^= BECH32_GENERATOR[i]; + } + } + } + return chk; + } + + coinjs.bech32_hrpExpand = function (hrp) { + var ret = []; + var p; + for (p = 0; p < hrp.length; ++p) { + ret.push(hrp.charCodeAt(p) >> 5); + } + ret.push(0); + for (p = 0; p < hrp.length; ++p) { + ret.push(hrp.charCodeAt(p) & 31); + } + return ret; + } + + coinjs.bech32_verifyChecksum = function (hrp, data) { + return coinjs.bech32_polymod(coinjs.bech32_hrpExpand(hrp).concat(data)) === 1; + } + + coinjs.bech32_createChecksum = function (hrp, data) { + var values = coinjs.bech32_hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]); + var mod = coinjs.bech32_polymod(values) ^ 1; + var ret = []; + for (var p = 0; p < 6; ++p) { + ret.push((mod >> 5 * (5 - p)) & 31); + } + return ret; + } + + coinjs.bech32_encode = function (hrp, data) { + var combined = data.concat(coinjs.bech32_createChecksum(hrp, data)); + var ret = hrp + '1'; + for (var p = 0; p < combined.length; ++p) { + ret += coinjs.bech32.charset.charAt(combined[p]); + } + return ret; + } + + coinjs.bech32_decode = function (bechString) { + var p; + var has_lower = false; + var has_upper = false; + for (p = 0; p < bechString.length; ++p) { + if (bechString.charCodeAt(p) < 33 || bechString.charCodeAt(p) > 126) { + return null; + } + if (bechString.charCodeAt(p) >= 97 && bechString.charCodeAt(p) <= 122) { + has_lower = true; + } + if (bechString.charCodeAt(p) >= 65 && bechString.charCodeAt(p) <= 90) { + has_upper = true; + } + } + if (has_lower && has_upper) { + return null; + } + bechString = bechString.toLowerCase(); + var pos = bechString.lastIndexOf('1'); + if (pos < 1 || pos + 7 > bechString.length || bechString.length > 90) { + return null; + } + var hrp = bechString.substring(0, pos); + var data = []; + for (p = pos + 1; p < bechString.length; ++p) { + var d = coinjs.bech32.charset.indexOf(bechString.charAt(p)); + if (d === -1) { + return null; + } + data.push(d); + } + if (!coinjs.bech32_verifyChecksum(hrp, data)) { + return null; + } + return { + hrp: hrp, + data: data.slice(0, data.length - 6) + }; + } + + coinjs.bech32_convert = function (data, inBits, outBits, pad) { + var value = 0; + var bits = 0; + var maxV = (1 << outBits) - 1; + + var result = []; + for (var i = 0; i < data.length; ++i) { + value = (value << inBits) | data[i]; + bits += inBits; + + while (bits >= outBits) { + bits -= outBits; + result.push((value >> bits) & maxV); + } + } + + if (pad) { + if (bits > 0) { + result.push((value << (outBits - bits)) & maxV); + } + } else { + if (bits >= inBits) throw new Error('Excess padding'); + if ((value << (outBits - bits)) & maxV) throw new Error('Non-zero padding'); + } + + return result; + } + + coinjs.testdeterministicK = function () { + // https://github.com/bitpay/bitcore/blob/9a5193d8e94b0bd5b8e7f00038e7c0b935405a03/test/crypto/ecdsa.js + // Line 21 and 22 specify digest hash and privkey for the first 2 test vectors. + // Line 96-117 tells expected result. + + var tx = coinjs.transaction(); + + var test_vectors = [{ + 'message': 'test data', + 'privkey': 'fee0a1f7afebf9d2a5a80c0c98a31c709681cce195cbcd06342b517970c0be1e', + 'k_bad00': 'fcce1de7a9bcd6b2d3defade6afa1913fb9229e3b7ddf4749b55c4848b2a196e', + 'k_bad01': '727fbcb59eb48b1d7d46f95a04991fc512eb9dbf9105628e3aec87428df28fd8', + 'k_bad15': '398f0e2c9f79728f7b3d84d447ac3a86d8b2083c8f234a0ffa9c4043d68bd258' + }, + { + 'message': 'Everything should be made as simple as possible, but not simpler.', + 'privkey': '0000000000000000000000000000000000000000000000000000000000000001', + 'k_bad00': 'ec633bd56a5774a0940cb97e27a9e4e51dc94af737596a0c5cbb3d30332d92a5', + 'k_bad01': 'df55b6d1b5c48184622b0ead41a0e02bfa5ac3ebdb4c34701454e80aabf36f56', + 'k_bad15': 'def007a9a3c2f7c769c75da9d47f2af84075af95cadd1407393dc1e26086ef87' + }, + { + 'message': 'Satoshi Nakamoto', + 'privkey': '0000000000000000000000000000000000000000000000000000000000000002', + 'k_bad00': 'd3edc1b8224e953f6ee05c8bbf7ae228f461030e47caf97cde91430b4607405e', + 'k_bad01': 'f86d8e43c09a6a83953f0ab6d0af59fb7446b4660119902e9967067596b58374', + 'k_bad15': '241d1f57d6cfd2f73b1ada7907b199951f95ef5ad362b13aed84009656e0254a' + }, + { + 'message': 'Diffie Hellman', + 'privkey': '7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f', + 'k_bad00': 'c378a41cb17dce12340788dd3503635f54f894c306d52f6e9bc4b8f18d27afcc', + 'k_bad01': '90756c96fef41152ac9abe08819c4e95f16da2af472880192c69a2b7bac29114', + 'k_bad15': '7b3f53300ab0ccd0f698f4d67db87c44cf3e9e513d9df61137256652b2e94e7c' + }, + { + 'message': 'Japan', + 'privkey': '8080808080808080808080808080808080808080808080808080808080808080', + 'k_bad00': 'f471e61b51d2d8db78f3dae19d973616f57cdc54caaa81c269394b8c34edcf59', + 'k_bad01': '6819d85b9730acc876fdf59e162bf309e9f63dd35550edf20869d23c2f3e6d17', + 'k_bad15': 'd8e8bae3ee330a198d1f5e00ad7c5f9ed7c24c357c0a004322abca5d9cd17847' + }, + { + 'message': 'Bitcoin', + 'privkey': 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', + 'k_bad00': '36c848ffb2cbecc5422c33a994955b807665317c1ce2a0f59c689321aaa631cc', + 'k_bad01': '4ed8de1ec952a4f5b3bd79d1ff96446bcd45cabb00fc6ca127183e14671bcb85', + 'k_bad15': '56b6f47babc1662c011d3b1f93aa51a6e9b5f6512e9f2e16821a238d450a31f8' + }, + { + 'message': 'i2FLPP8WEus5WPjpoHwheXOMSobUJVaZM1JPMQZq', + 'privkey': 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', + 'k_bad00': '6e9b434fcc6bbb081a0463c094356b47d62d7efae7da9c518ed7bac23f4e2ed6', + 'k_bad01': 'ae5323ae338d6117ce8520a43b92eacd2ea1312ae514d53d8e34010154c593bb', + 'k_bad15': '3eaa1b61d1b8ab2f1ca71219c399f2b8b3defa624719f1e96fe3957628c2c4ea' + }, + { + 'message': 'lEE55EJNP7aLrMtjkeJKKux4Yg0E8E1SAJnWTCEh', + 'privkey': '3881e5286abc580bb6139fe8e83d7c8271c6fe5e5c2d640c1f0ed0e1ee37edc9', + 'k_bad00': '5b606665a16da29cc1c5411d744ab554640479dd8abd3c04ff23bd6b302e7034', + 'k_bad01': 'f8b25263152c042807c992eacd2ac2cc5790d1e9957c394f77ea368e3d9923bd', + 'k_bad15': 'ea624578f7e7964ac1d84adb5b5087dd14f0ee78b49072aa19051cc15dab6f33' + }, + { + 'message': '2SaVPvhxkAPrayIVKcsoQO5DKA8Uv5X/esZFlf+y', + 'privkey': '7259dff07922de7f9c4c5720d68c9745e230b32508c497dd24cb95ef18856631', + 'k_bad00': '3ab6c19ab5d3aea6aa0c6da37516b1d6e28e3985019b3adb388714e8f536686b', + 'k_bad01': '19af21b05004b0ce9cdca82458a371a9d2cf0dc35a813108c557b551c08eb52e', + 'k_bad15': '117a32665fca1b7137a91c4739ac5719fec0cf2e146f40f8e7c21b45a07ebc6a' + }, + { + 'message': '00A0OwO2THi7j5Z/jp0FmN6nn7N/DQd6eBnCS+/b', + 'privkey': '0d6ea45d62b334777d6995052965c795a4f8506044b4fd7dc59c15656a28f7aa', + 'k_bad00': '79487de0c8799158294d94c0eb92ee4b567e4dc7ca18addc86e49d31ce1d2db6', + 'k_bad01': '9561d2401164a48a8f600882753b3105ebdd35e2358f4f808c4f549c91490009', + 'k_bad15': 'b0d273634129ff4dbdf0df317d4062a1dbc58818f88878ffdb4ec511c77976c0' + } + ]; + + var result_txt = '\n----------------------\nResults\n----------------------\n\n'; + + for (i = 0; i < test_vectors.length; i++) { + var hash = Crypto.SHA256(test_vectors[i]['message'].split('').map(function (c) { + return c.charCodeAt(0); + }), { + asBytes: true + }); + var wif = coinjs.privkey2wif(test_vectors[i]['privkey']); + + var KBigInt = tx.deterministicK(wif, hash); + var KBigInt0 = tx.deterministicK(wif, hash, 0); + var KBigInt1 = tx.deterministicK(wif, hash, 1); + var KBigInt15 = tx.deterministicK(wif, hash, 15); + + var K = Crypto.util.bytesToHex(KBigInt.toByteArrayUnsigned()); + var K0 = Crypto.util.bytesToHex(KBigInt0.toByteArrayUnsigned()); + var K1 = Crypto.util.bytesToHex(KBigInt1.toByteArrayUnsigned()); + var K15 = Crypto.util.bytesToHex(KBigInt15.toByteArrayUnsigned()); + + if (K != test_vectors[i]['k_bad00']) { + result_txt += 'Failed Test #' + (i + 1) + '\n K = ' + K + '\nExpected = ' + test_vectors[i]['k_bad00'] + '\n\n'; + } else if (K0 != test_vectors[i]['k_bad00']) { + result_txt += 'Failed Test #' + (i + 1) + '\n K0 = ' + K0 + '\nExpected = ' + test_vectors[i]['k_bad00'] + '\n\n'; + } else if (K1 != test_vectors[i]['k_bad01']) { + result_txt += 'Failed Test #' + (i + 1) + '\n K1 = ' + K1 + '\nExpected = ' + test_vectors[i]['k_bad01'] + '\n\n'; + } else if (K15 != test_vectors[i]['k_bad15']) { + result_txt += 'Failed Test #' + (i + 1) + '\n K15 = ' + K15 + '\nExpected = ' + test_vectors[i]['k_bad15'] + '\n\n'; + }; + }; + + if (result_txt.length < 60) { + result_txt = 'All Tests OK!'; + }; + + return result_txt; + }; + + /* start of hd functions, thanks bip32.org */ + coinjs.hd = function (data) { + + var r = {}; + + /* some hd value parsing */ + r.parse = function () { + + var bytes = []; + + // some quick validation + if (typeof (data) == 'string') { + var decoded = coinjs.base58decode(data); + if (decoded.length == 82) { + var checksum = decoded.slice(78, 82); + var hash = Crypto.SHA256(Crypto.SHA256(decoded.slice(0, 78), { + asBytes: true + }), { + asBytes: true + }); + if (checksum[0] == hash[0] && checksum[1] == hash[1] && checksum[2] == hash[2] && checksum[3] == hash[3]) { + bytes = decoded.slice(0, 78); + } + } + } + + // actual parsing code + if (bytes && bytes.length > 0) { + r.version = coinjs.uint(bytes.slice(0, 4), 4); + r.depth = coinjs.uint(bytes.slice(4, 5), 1); + r.parent_fingerprint = bytes.slice(5, 9); + r.child_index = coinjs.uint(bytes.slice(9, 13), 4); + r.chain_code = bytes.slice(13, 45); + r.key_bytes = bytes.slice(45, 78); + + var c = coinjs.compressed; // get current default + coinjs.compressed = true; + + if (r.key_bytes[0] == 0x00) { + r.type = 'private'; + var privkey = (r.key_bytes).slice(1, 33); + var privkeyHex = Crypto.util.bytesToHex(privkey); + var pubkey = coinjs.newPubkey(privkeyHex); + + r.keys = { + 'privkey': privkeyHex, + 'pubkey': pubkey, + 'address': coinjs.pubkey2address(pubkey), + 'wif': coinjs.privkey2wif(privkeyHex) + }; + + } else if (r.key_bytes[0] == 0x02 || r.key_bytes[0] == 0x03) { + r.type = 'public'; + var pubkeyHex = Crypto.util.bytesToHex(r.key_bytes); + + r.keys = { + 'pubkey': pubkeyHex, + 'address': coinjs.pubkey2address(pubkeyHex) + }; + } else { + r.type = 'invalid'; + } + + r.keys_extended = r.extend(); + + coinjs.compressed = c; // reset to default + } + + return r; + } + + // extend prv/pub key + r.extend = function () { + var hd = coinjs.hd(); + return hd.make({ + 'depth': (this.depth * 1) + 1, + 'parent_fingerprint': this.parent_fingerprint, + 'child_index': this.child_index, + 'chain_code': this.chain_code, + 'privkey': this.keys.privkey, + 'pubkey': this.keys.pubkey + }); + } + + // derive from path + r.derive_path = function (path) { + + if (path == 'm' || path == 'M' || path == 'm\'' || path == 'M\'') return this; + + var p = path.split('/'); + var hdp = coinjs.clone(this); // clone hd path + + for (var i in p) { + + if (((i == 0) && c != 'm') || i == 'remove') { + continue; + } + + var c = p[i]; + + var use_private = (c.length > 1) && (c[c.length - 1] == '\''); + var child_index = parseInt(use_private ? c.slice(0, c.length - 1) : c) & 0x7fffffff; + if (use_private) + child_index += 0x80000000; + + hdp = hdp.derive(child_index); + var key = ((hdp.keys_extended.privkey) && hdp.keys_extended.privkey != '') ? hdp.keys_extended.privkey : hdp.keys_extended.pubkey; + hdp = coinjs.hd(key); + } + return hdp; + } + + // derive key from index + r.derive = function (i) { + + i = (i) ? i : 0; + var blob = (Crypto.util.hexToBytes(this.keys.pubkey)).concat(coinjs.numToBytes(i, 4).reverse()); + + var j = new jsSHA(Crypto.util.bytesToHex(blob), 'HEX'); + var hash = j.getHMAC(Crypto.util.bytesToHex(r.chain_code), "HEX", "SHA-512", "HEX"); + + var il = new BigInteger(hash.slice(0, 64), 16); + var ir = Crypto.util.hexToBytes(hash.slice(64, 128)); + + var ecparams = EllipticCurve.getSECCurveByName("secp256k1"); + var curve = ecparams.getCurve(); + + var k, key, pubkey, o; + + o = coinjs.clone(this); + o.chain_code = ir; + o.child_index = i; + + if (this.type == 'private') { + // derive key pair from from a xprv key + k = il.add(new BigInteger([0].concat(Crypto.util.hexToBytes(this.keys.privkey)))).mod(ecparams.getN()); + key = Crypto.util.bytesToHex(k.toByteArrayUnsigned()); + + pubkey = coinjs.newPubkey(key); + + o.keys = { + 'privkey': key, + 'pubkey': pubkey, + 'wif': coinjs.privkey2wif(key), + 'address': coinjs.pubkey2address(pubkey) + }; + + } else if (this.type == 'public') { + // derive xpub key from an xpub key + q = ecparams.curve.decodePointHex(this.keys.pubkey); + var curvePt = ecparams.getG().multiply(il).add(q); + + var x = curvePt.getX().toBigInteger(); + var y = curvePt.getY().toBigInteger(); + + var publicKeyBytesCompressed = EllipticCurve.integerToBytes(x, 32) + if (y.isEven()) { + publicKeyBytesCompressed.unshift(0x02) + } else { + publicKeyBytesCompressed.unshift(0x03) + } + pubkey = Crypto.util.bytesToHex(publicKeyBytesCompressed); + + o.keys = { + 'pubkey': pubkey, + 'address': coinjs.pubkey2address(pubkey) + } + } else { + // fail + } + + o.parent_fingerprint = (ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(r.keys.pubkey), { + asBytes: true + }), { + asBytes: true + })).slice(0, 4); + o.keys_extended = o.extend(); + return o; + } + + // make a master hd xprv/xpub + r.master = function (pass) { + var seed = (pass) ? Crypto.SHA256(pass) : coinjs.newPrivkey(); + var hasher = new jsSHA(seed, 'HEX'); + var I = hasher.getHMAC("Bitcoin seed", "TEXT", "SHA-512", "HEX"); + + var privkey = Crypto.util.hexToBytes(I.slice(0, 64)); + var chain = Crypto.util.hexToBytes(I.slice(64, 128)); + + var hd = coinjs.hd(); + return hd.make({ + 'depth': 0, + 'parent_fingerprint': [0, 0, 0, 0], + 'child_index': 0, + 'chain_code': chain, + 'privkey': I.slice(0, 64), + 'pubkey': coinjs.newPubkey(I.slice(0, 64)) + }); + } + + // encode data to a base58 string + r.make = function (data) { // { (int) depth, (array) parent_fingerprint, (int) child_index, (byte array) chain_code, (hex str) privkey, (hex str) pubkey} + var k = []; + + //depth + k.push(data.depth * 1); + + //parent fingerprint + k = k.concat(data.parent_fingerprint); + + //child index + k = k.concat((coinjs.numToBytes(data.child_index, 4)).reverse()); + + //Chain code + k = k.concat(data.chain_code); + + var o = {}; // results + + //encode xprv key + if (data.privkey) { + var prv = (coinjs.numToBytes(coinjs.hdkey.prv, 4)).reverse(); + prv = prv.concat(k); + prv.push(0x00); + prv = prv.concat(Crypto.util.hexToBytes(data.privkey)); + var hash = Crypto.SHA256(Crypto.SHA256(prv, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + var ret = prv.concat(checksum); + o.privkey = coinjs.base58encode(ret); + } + + //encode xpub key + if (data.pubkey) { + var pub = (coinjs.numToBytes(coinjs.hdkey.pub, 4)).reverse(); + pub = pub.concat(k); + pub = pub.concat(Crypto.util.hexToBytes(data.pubkey)); + var hash = Crypto.SHA256(Crypto.SHA256(pub, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + var ret = pub.concat(checksum); + o.pubkey = coinjs.base58encode(ret); + } + return o; + } + + return r.parse(); + } + + + /* start of script functions */ + coinjs.script = function (data) { + var r = {}; + + if (!data) { + r.buffer = []; + } else if ("string" == typeof data) { + r.buffer = Crypto.util.hexToBytes(data); + } else if (coinjs.isArray(data)) { + r.buffer = data; + } else if (data instanceof coinjs.script) { + r.buffer = data.buffer; + } else { + r.buffer = data; + } + + /* parse buffer array */ + r.parse = function () { + + var self = this; + r.chunks = []; + var i = 0; + + function readChunk(n) { + self.chunks.push(self.buffer.slice(i, i + n)); + i += n; + }; + + while (i < this.buffer.length) { + var opcode = this.buffer[i++]; + if (opcode >= 0xF0) { + opcode = (opcode << 8) | this.buffer[i++]; + } + + var len; + if (opcode > 0 && opcode < 76) { //OP_PUSHDATA1 + readChunk(opcode); + } else if (opcode == 76) { //OP_PUSHDATA1 + len = this.buffer[i++]; + readChunk(len); + } else if (opcode == 77) { //OP_PUSHDATA2 + len = (this.buffer[i++] << 8) | this.buffer[i++]; + readChunk(len); + } else if (opcode == 78) { //OP_PUSHDATA4 + len = (this.buffer[i++] << 24) | (this.buffer[i++] << 16) | (this.buffer[i++] << 8) | this.buffer[i++]; + readChunk(len); + } else { + this.chunks.push(opcode); + } + + if (i < 0x00) { + break; + } + } + + return true; + }; + + /* decode the redeemscript of a multisignature transaction */ + r.decodeRedeemScript = function (script) { + var r = false; + try { + var s = coinjs.script(Crypto.util.hexToBytes(script)); + if ((s.chunks.length >= 3) && s.chunks[s.chunks.length - 1] == 174) { //OP_CHECKMULTISIG + r = {}; + r.signaturesRequired = s.chunks[0] - 80; + var pubkeys = []; + for (var i = 1; i < s.chunks.length - 2; i++) { + pubkeys.push(Crypto.util.bytesToHex(s.chunks[i])); + } + r.pubkeys = pubkeys; + var multi = coinjs.pubkeys2MultisigAddress(pubkeys, r.signaturesRequired); + r.address = multi['address']; + r.type = 'multisig__'; // using __ for now to differentiat from the other object .type == "multisig" + var rs = Crypto.util.bytesToHex(s.buffer); + r.redeemscript = rs; + + } else if ((s.chunks.length == 2) && (s.buffer[0] == 0 && s.buffer[1] == 20)) { // SEGWIT + r = {}; + r.type = "segwit__"; + var rs = Crypto.util.bytesToHex(s.buffer); + r.address = coinjs.pubkey2address(rs, coinjs.multisig); + r.redeemscript = rs; + + } else if (s.chunks.length == 5 && s.chunks[1] == 177 && s.chunks[2] == 117 && s.chunks[4] == 172) { + // ^ OP_CHECKLOCKTIMEVERIFY OP_DROP OP_CHECKSIG ^ + r = {} + r.pubkey = Crypto.util.bytesToHex(s.chunks[3]); + r.checklocktimeverify = coinjs.bytesToNum(s.chunks[0].slice()); + r.address = coinjs.simpleHodlAddress(r.pubkey, r.checklocktimeverify).address; + var rs = Crypto.util.bytesToHex(s.buffer); + r.redeemscript = rs; + r.type = "hodl__"; + } + } catch (e) { + // console.log(e); + r = false; + } + return r; + } + + /* decode the redeemscript of a multisignature transaction for Bech32*/ + r.decodeRedeemScriptBech32 = function (script) { + var r = false; + try { + var s = coinjs.script(Crypto.util.hexToBytes(script)); + if ((s.chunks.length >= 3) && s.chunks[s.chunks.length - 1] == 174) { //OP_CHECKMULTISIG + r = {}; + r.signaturesRequired = s.chunks[0] - 80; + var pubkeys = []; + for (var i = 1; i < s.chunks.length - 2; i++) { + pubkeys.push(Crypto.util.bytesToHex(s.chunks[i])); + } + r.pubkeys = pubkeys; + var multi = coinjs.pubkeys2MultisigAddressBech32(pubkeys, r.signaturesRequired); + r.address = multi['address']; + r.type = 'multisig__'; // using __ for now to differentiat from the other object .type == "multisig" + var rs = Crypto.util.bytesToHex(s.buffer); + r.redeemscript = rs; + + } + + } catch (e) { + // console.log(e); + r = false; + } + return r; + } + + /* create output script to spend */ + r.spendToScript = function (address) { + var addr = coinjs.addressDecode(address); + var s = coinjs.script(); + if (addr.type == "bech32" || addr.type == "multisigBech32") { + s.writeOp(0); + s.writeBytes(Crypto.util.hexToBytes(addr.redeemscript)); + } else if (addr.version == coinjs.multisig) { // multisig address + s.writeOp(169); //OP_HASH160 + s.writeBytes(addr.bytes); + s.writeOp(135); //OP_EQUAL + } else { // regular address + s.writeOp(118); //OP_DUP + s.writeOp(169); //OP_HASH160 + s.writeBytes(addr.bytes); + s.writeOp(136); //OP_EQUALVERIFY + s.writeOp(172); //OP_CHECKSIG + } + return s; + } + + /* geneate a (script) pubkey hash of the address - used for when signing */ + r.pubkeyHash = function (address) { + var addr = coinjs.addressDecode(address); + var s = coinjs.script(); + s.writeOp(118); //OP_DUP + s.writeOp(169); //OP_HASH160 + s.writeBytes(addr.bytes); + s.writeOp(136); //OP_EQUALVERIFY + s.writeOp(172); //OP_CHECKSIG + return s; + } + + /* write to buffer */ + r.writeOp = function (op) { + this.buffer.push(op); + this.chunks.push(op); + return true; + } + + /* write bytes to buffer */ + r.writeBytes = function (data) { + if (data.length < 76) { //OP_PUSHDATA1 + this.buffer.push(data.length); + } else if (data.length <= 0xff) { + this.buffer.push(76); //OP_PUSHDATA1 + this.buffer.push(data.length); + } else if (data.length <= 0xffff) { + this.buffer.push(77); //OP_PUSHDATA2 + this.buffer.push(data.length & 0xff); + this.buffer.push((data.length >>> 8) & 0xff); + } else { + this.buffer.push(78); //OP_PUSHDATA4 + this.buffer.push(data.length & 0xff); + this.buffer.push((data.length >>> 8) & 0xff); + this.buffer.push((data.length >>> 16) & 0xff); + this.buffer.push((data.length >>> 24) & 0xff); + } + this.buffer = this.buffer.concat(data); + this.chunks.push(data); + return true; + } + + r.parse(); + return r; + } + + /* start of transaction functions */ + + /* create a new transaction object */ + coinjs.transaction = function () { + + var r = {}; + r.version = 1; + r.lock_time = 0; + r.ins = []; + r.outs = []; + r.witness = false; + r.timestamp = null; + r.block = null; + + /* add an input to a transaction */ + r.addinput = function (txid, index, script, sequence) { + var o = {}; + o.outpoint = { + 'hash': txid, + 'index': index + }; + o.script = coinjs.script(script || []); + o.sequence = sequence || ((r.lock_time == 0) ? 4294967295 : 0); + return this.ins.push(o); + } + + /* add an output to a transaction */ + r.addoutput = function (address, value) { + var o = {}; + o.value = new BigInteger('' + Math.round((value * 1) * 1e8), 10); + var s = coinjs.script(); + o.script = s.spendToScript(address); + + return this.outs.push(o); + } + + /* add two outputs for stealth addresses to a transaction */ + r.addstealth = function (stealth, value) { + var ephemeralKeyBigInt = BigInteger.fromByteArrayUnsigned(Crypto.util.hexToBytes(coinjs.newPrivkey())); + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + + var p = EllipticCurve.fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"); + var a = BigInteger.ZERO; + var b = EllipticCurve.fromHex("7"); + var calccurve = new EllipticCurve.CurveFp(p, a, b); + + var ephemeralPt = curve.getG().multiply(ephemeralKeyBigInt); + var scanPt = calccurve.decodePointHex(stealth.scankey); + var sharedPt = scanPt.multiply(ephemeralKeyBigInt); + var stealthindexKeyBigInt = BigInteger.fromByteArrayUnsigned(Crypto.SHA256(sharedPt.getEncoded(true), { + asBytes: true + })); + + var stealthindexPt = curve.getG().multiply(stealthindexKeyBigInt); + var spendPt = calccurve.decodePointHex(stealth.spendkey); + var addressPt = spendPt.add(stealthindexPt); + + var sendaddress = coinjs.pubkey2address(Crypto.util.bytesToHex(addressPt.getEncoded(true))); + + + var OPRETBytes = [6].concat(Crypto.util.randomBytes(4)).concat(ephemeralPt.getEncoded(true)); // ephemkey data + var q = coinjs.script(); + q.writeOp(106); // OP_RETURN + q.writeBytes(OPRETBytes); + v = {}; + v.value = 0; + v.script = q; + + this.outs.push(v); + + var o = {}; + o.value = new BigInteger('' + Math.round((value * 1) * 1e8), 10); + var s = coinjs.script(); + o.script = s.spendToScript(sendaddress); + + return this.outs.push(o); + } + + /* add data to a transaction */ + r.adddata = function (data) { + var r = false; + if (((data.match(/^[a-f0-9]+$/gi)) && data.length < 160) && (data.length % 2) == 0) { + var s = coinjs.script(); + s.writeOp(106); // OP_RETURN + s.writeBytes(Crypto.util.hexToBytes(data)); + o = {}; + o.value = 0; + o.script = s; + return this.outs.push(o); + } + return r; + } + + /* list unspent transactions */ + r.listUnspent = function (address, callback) { + coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=addresses&request=unspent&address=' + address + '&r=' + Math.random(), callback, "GET"); + } + + /* list transaction data */ + r.getTransaction = function (txid, callback) { + coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=bitcoin&request=gettransaction&txid=' + txid + '&r=' + Math.random(), callback, "GET"); + } + + /* add unspent to transaction */ + r.addUnspent = function (address, callback, script, segwit, sequence) { + var self = this; + this.listUnspent(address, function (data) { + var s = coinjs.script(); + var value = 0; + var total = 0; + var x = {}; + + if (GLOBAL.DOMParser) { + parser = new DOMParser(); + xmlDoc = parser.parseFromString(data, "text/xml"); + } else { + xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); + xmlDoc.async = false; + xmlDoc.loadXML(data); + } + + var unspent = xmlDoc.getElementsByTagName("unspent")[0]; + + if (unspent) { + for (i = 1; i <= unspent.childElementCount; i++) { + var u = xmlDoc.getElementsByTagName("unspent_" + i)[0] + var txhash = (u.getElementsByTagName("tx_hash")[0].childNodes[0].nodeValue).match(/.{1,2}/g).reverse().join("") + ''; + var n = u.getElementsByTagName("tx_output_n")[0].childNodes[0].nodeValue; + var scr = script || u.getElementsByTagName("script")[0].childNodes[0].nodeValue; + + if (segwit) { //also for MULTISIG_BECH32 (p2wsh-multisig)(script = redeemscript; for p2wsh-multisig) + /* this is a small hack to include the value with the redeemscript to make the signing procedure smoother. + It is not standard and removed during the signing procedure. */ + + s = coinjs.script(); + s.writeBytes(Crypto.util.hexToBytes(script)); + s.writeOp(0); + s.writeBytes(coinjs.numToBytes(u.getElementsByTagName("value")[0].childNodes[0].nodeValue * 1, 8)); + scr = Crypto.util.bytesToHex(s.buffer); + } + + var seq = sequence || false; + self.addinput(txhash, n, scr, seq); + value += u.getElementsByTagName("value")[0].childNodes[0].nodeValue * 1; + total++; + } + } + + x.result = xmlDoc.getElementsByTagName("result")[0].childNodes[0].nodeValue; + x.unspent = unspent; + x.value = value; + x.total = total; + x.response = xmlDoc.getElementsByTagName("response")[0].childNodes[0].nodeValue; + + return callback(x); + }); + } + + /* add unspent and sign */ + r.addUnspentAndSign = function (wif, callback) { + var self = this; + var address = coinjs.wif2address(wif); + self.addUnspent(address['address'], function (data) { + self.sign(wif); + return callback(data); + }); + } + + /* broadcast a transaction */ + r.broadcast = function (callback, txhex) { + var tx = txhex || this.serialize(); + coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=bitcoin&request=sendrawtransaction', callback, "POST", ["rawtx=" + tx]); + } + + /* generate the transaction hash to sign from a transaction input */ + r.transactionHash = function (index, sigHashType) { + + var clone = coinjs.clone(this); + var shType = sigHashType || 1; + + /* black out all other ins, except this one */ + for (var i = 0; i < clone.ins.length; i++) { + if (index != i) { + clone.ins[i].script = coinjs.script(); + } + } + + var extract = this.extractScriptKey(index); + clone.ins[index].script = coinjs.script(extract['script']); + + if ((clone.ins) && clone.ins[index]) { + + /* SIGHASH : For more info on sig hashs see https://en.bitcoin.it/wiki/OP_CHECKSIG + and https://bitcoin.org/en/developer-guide#signature-hash-type */ + + if (shType == 1) { + //SIGHASH_ALL 0x01 + + } else if (shType == 2) { + //SIGHASH_NONE 0x02 + clone.outs = []; + for (var i = 0; i < clone.ins.length; i++) { + if (index != i) { + clone.ins[i].sequence = 0; + } + } + + } else if (shType == 3) { + + //SIGHASH_SINGLE 0x03 + clone.outs.length = index + 1; + + for (var i = 0; i < index; i++) { + clone.outs[i].value = -1; + clone.outs[i].script.buffer = []; + } + + for (var i = 0; i < clone.ins.length; i++) { + if (index != i) { + clone.ins[i].sequence = 0; + } + } + + } else if (shType >= 128) { + //SIGHASH_ANYONECANPAY 0x80 + clone.ins = [clone.ins[index]]; + + if (shType == 129) { + // SIGHASH_ALL + SIGHASH_ANYONECANPAY + + } else if (shType == 130) { + // SIGHASH_NONE + SIGHASH_ANYONECANPAY + clone.outs = []; + + } else if (shType == 131) { + // SIGHASH_SINGLE + SIGHASH_ANYONECANPAY + clone.outs.length = index + 1; + for (var i = 0; i < index; i++) { + clone.outs[i].value = -1; + clone.outs[i].script.buffer = []; + } + } + } + + var buffer = Crypto.util.hexToBytes(clone.serialize()); + buffer = buffer.concat(coinjs.numToBytes(parseInt(shType), 4)); + var hash = Crypto.SHA256(buffer, { + asBytes: true + }); + var r = Crypto.util.bytesToHex(Crypto.SHA256(hash, { + asBytes: true + })); + return r; + } else { + return false; + } + } + + /* generate a segwit transaction hash to sign from a transaction input */ + r.transactionHashSegWitV0 = function (index, sigHashType) { + /* + Notice: coinb.in by default, deals with segwit transactions in a non-standard way. + Segwit transactions require that input values are included in the transaction hash. + To save wasting resources and potentially slowing down this service, we include the amount with the + redeem script to generate the transaction hash and remove it after its signed. + */ + + // start redeem script check + var extract = this.extractScriptKey(index); + if (extract['type'] != 'segwit' && extract['type'] != 'multisig_bech32') { + return { + 'result': 0, + 'fail': 'redeemscript', + 'response': 'redeemscript missing or not valid for segwit' + }; + } + + if (extract['value'] == -1) { + return { + 'result': 0, + 'fail': 'value', + 'response': 'unable to generate a valid segwit hash without a value' + }; + } + + var scriptcode = Crypto.util.hexToBytes(extract['script']); + + // end of redeem script check + + /* P2WPKH */ + if (scriptcode.length == 20) { + scriptcode = [0x00, 0x14].concat(scriptcode); + } + + if (scriptcode.length == 22) { + scriptcode = scriptcode.slice(1); + scriptcode.unshift(25, 118, 169); + scriptcode.push(136, 172); + } else if (scriptcode[0] > 80) { + scriptcode.unshift(scriptcode.length) + } + + var value = coinjs.numToBytes(extract['value'], 8); + + // start + + var zero = coinjs.numToBytes(0, 32); + var version = coinjs.numToBytes(parseInt(this.version), 4); + + var bufferTmp = []; + if (!(sigHashType >= 80)) { // not sighash anyonecanpay + for (var i = 0; i < this.ins.length; i++) { + bufferTmp = bufferTmp.concat(Crypto.util.hexToBytes(this.ins[i].outpoint.hash).reverse()); + bufferTmp = bufferTmp.concat(coinjs.numToBytes(this.ins[i].outpoint.index, 4)); + } + } + var hashPrevouts = bufferTmp.length >= 1 ? Crypto.SHA256(Crypto.SHA256(bufferTmp, { + asBytes: true + }), { + asBytes: true + }) : zero; + + var bufferTmp = []; + if (!(sigHashType >= 80) && sigHashType != 2 && sigHashType != 3) { // not sighash anyonecanpay & single & none + for (var i = 0; i < this.ins.length; i++) { + bufferTmp = bufferTmp.concat(coinjs.numToBytes(this.ins[i].sequence, 4)); + } + } + var hashSequence = bufferTmp.length >= 1 ? Crypto.SHA256(Crypto.SHA256(bufferTmp, { + asBytes: true + }), { + asBytes: true + }) : zero; + + var outpoint = Crypto.util.hexToBytes(this.ins[index].outpoint.hash).reverse(); + outpoint = outpoint.concat(coinjs.numToBytes(this.ins[index].outpoint.index, 4)); + + var nsequence = coinjs.numToBytes(this.ins[index].sequence, 4); + var hashOutputs = zero; + var bufferTmp = []; + if (sigHashType != 2 && sigHashType != 3) { // not sighash single & none + for (var i = 0; i < this.outs.length; i++) { + bufferTmp = bufferTmp.concat(coinjs.numToBytes(this.outs[i].value, 8)); + bufferTmp = bufferTmp.concat(coinjs.numToVarInt(this.outs[i].script.buffer.length)); + bufferTmp = bufferTmp.concat(this.outs[i].script.buffer); + } + hashOutputs = Crypto.SHA256(Crypto.SHA256(bufferTmp, { + asBytes: true + }), { + asBytes: true + }); + + } else if ((sigHashType == 2) && index < this.outs.length) { // is sighash single + bufferTmp = bufferTmp.concat(coinjs.numToBytes(this.outs[index].value, 8)); + bufferTmp = bufferTmp.concat(coinjs.numToVarInt(this.outs[i].script.buffer.length)); + bufferTmp = bufferTmp.concat(this.outs[index].script.buffer); + hashOutputs = Crypto.SHA256(Crypto.SHA256(bufferTmp, { + asBytes: true + }), { + asBytes: true + }); + } + + var locktime = coinjs.numToBytes(this.lock_time, 4); + var sighash = coinjs.numToBytes(sigHashType, 4); + + var buffer = []; + buffer = buffer.concat(version); + buffer = buffer.concat(hashPrevouts); + buffer = buffer.concat(hashSequence); + buffer = buffer.concat(outpoint); + buffer = buffer.concat(scriptcode); + buffer = buffer.concat(value); + buffer = buffer.concat(nsequence); + buffer = buffer.concat(hashOutputs); + buffer = buffer.concat(locktime); + buffer = buffer.concat(sighash); + + var hash = Crypto.SHA256(buffer, { + asBytes: true + }); + return { + 'result': 1, + 'hash': Crypto.util.bytesToHex(Crypto.SHA256(hash, { + asBytes: true + })), + 'response': 'hash generated' + }; + } + + /* extract the scriptSig, used in the transactionHash() function */ + r.extractScriptKey = function (index) { + if (this.ins[index]) { + if ((this.ins[index].script.chunks.length == 5) && this.ins[index].script.chunks[4] == 172 && coinjs.isArray(this.ins[index].script.chunks[2])) { //OP_CHECKSIG + // regular scriptPubkey (not signed) + return { + 'type': 'scriptpubkey', + 'signed': 'false', + 'signatures': 0, + 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer) + }; + } else if ((this.ins[index].script.chunks.length == 2) && this.ins[index].script.chunks[0][0] == 48 && this.ins[index].script.chunks[1].length == 5 && this.ins[index].script.chunks[1][1] == 177) { //OP_CHECKLOCKTIMEVERIFY + // hodl script (signed) + return { + 'type': 'hodl', + 'signed': 'true', + 'signatures': 1, + 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer) + }; + } else if ((this.ins[index].script.chunks.length == 2) && this.ins[index].script.chunks[0][0] == 48) { + // regular scriptPubkey (probably signed) + return { + 'type': 'scriptpubkey', + 'signed': 'true', + 'signatures': 1, + 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer) + }; + } else if (this.ins[index].script.chunks.length == 5 && this.ins[index].script.chunks[1] == 177) { //OP_CHECKLOCKTIMEVERIFY + // hodl script (not signed) + return { + 'type': 'hodl', + 'signed': 'false', + 'signatures': 0, + 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer) + }; + } else if ((this.ins[index].script.chunks.length <= 3 && this.ins[index].script.chunks.length > 0) && ((this.ins[index].script.chunks[0].length == 22 && this.ins[index].script.chunks[0][0] == 0) || (this.ins[index].script.chunks[0].length == 20 && this.ins[index].script.chunks[1] == 0))) { + var signed = ((this.witness[index]) && this.witness[index].length == 2) ? 'true' : 'false'; + var sigs = (signed == 'true') ? 1 : 0; + var value = -1; // no value found + if ((this.ins[index].script.chunks[2]) && this.ins[index].script.chunks[2].length == 8) { + value = coinjs.bytesToNum(this.ins[index].script.chunks[2]); // value found encoded in transaction (THIS IS NON STANDARD) + } + return { + 'type': 'segwit', + 'signed': signed, + 'signatures': sigs, + 'script': Crypto.util.bytesToHex(this.ins[index].script.chunks[0]), + 'value': value + }; + } else if (this.ins[index].script.chunks[0] == 0 && this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1][this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1].length - 1] == 174) { // OP_CHECKMULTISIG + // multisig script, with signature(s) included + var sigcount = 0; + for (let i = 1; i < this.ins[index].script.chunks.length - 1; i++) { + if (this.ins[index].script.chunks[i] != 0) { + sigcount++; + } + } + + return { + 'type': 'multisig', + 'signed': 'true', + 'signatures': sigcount, + 'script': Crypto.util.bytesToHex(this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1]) + }; + } else if (this.ins[index].script.chunks[0] >= 80 && this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1] == 174) { // OP_CHECKMULTISIG + // multisig script, without signature! + return { + 'type': 'multisig', + 'signed': 'false', + 'signatures': 0, + 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer) + }; + } else if (this.ins[index].script.chunks.length == 3 && this.ins[index].script.chunks[0][0] >= 80 && this.ins[index].script.chunks[0][this.ins[index].script.chunks[0].length - 1] == 174 && this.ins[index].script.chunks[1] == 0) { //OP_CHECKMULTISIG_BECH32 + // multisig bech32 script + let last_index = this.ins[index].script.chunks.length - 1; + var value = -1; + if (last_index >= 2 && this.ins[index].script.chunks[last_index].length == 8) { + value = coinjs.bytesToNum(this.ins[index].script.chunks[last_index]); // value found encoded in transaction (THIS IS NON STANDARD) + } + var sigcount = (!this.witness[index]) ? 0 : this.witness[index].length - 2; + return { + 'type': 'multisig_bech32', + 'signed': 'false', + 'signatures': sigcount, + 'script': Crypto.util.bytesToHex(this.ins[index].script.chunks[0]), + 'value': value + }; + } else if (this.ins[index].script.chunks.length == 0) { + // empty + //bech32 witness check + var signed = ((this.witness[index]) && this.witness[index].length >= 2) ? 'true' : 'false'; + var sigs = (signed == 'true') ? (!this.witness[index][0] ? this.witness[index].length - 2 : 1) : 0; + return { + 'type': 'empty', + 'signed': signed, + 'signatures': sigs, + 'script': '' + }; + } else { + // something else + return { + 'type': 'unknown', + 'signed': 'false', + 'signatures': 0, + 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer) + }; + } + } else { + return false; + } + } + + /* generate a signature from a transaction hash */ + r.transactionSig = function (index, wif, sigHashType, txhash) { + + function serializeSig(r, s) { + var rBa = r.toByteArraySigned(); + var sBa = s.toByteArraySigned(); + + var sequence = []; + sequence.push(0x02); // INTEGER + sequence.push(rBa.length); + sequence = sequence.concat(rBa); + + sequence.push(0x02); // INTEGER + sequence.push(sBa.length); + sequence = sequence.concat(sBa); + + sequence.unshift(sequence.length); + sequence.unshift(0x30); // SEQUENCE + + return sequence; + } + + var shType = sigHashType || 1; + var hash = txhash || Crypto.util.hexToBytes(this.transactionHash(index, shType)); + + if (hash) { + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + var key = coinjs.wif2privkey(wif); + var priv = BigInteger.fromByteArrayUnsigned(Crypto.util.hexToBytes(key['privkey'])); + var n = curve.getN(); + var e = BigInteger.fromByteArrayUnsigned(hash); + var badrs = 0 + do { + var k = this.deterministicK(wif, hash, badrs); + var G = curve.getG(); + var Q = G.multiply(k); + var r = Q.getX().toBigInteger().mod(n); + var s = k.modInverse(n).multiply(e.add(priv.multiply(r))).mod(n); + badrs++ + } while (r.compareTo(BigInteger.ZERO) <= 0 || s.compareTo(BigInteger.ZERO) <= 0); + + // Force lower s values per BIP62 + var halfn = n.shiftRight(1); + if (s.compareTo(halfn) > 0) { + s = n.subtract(s); + }; + + var sig = serializeSig(r, s); + sig.push(parseInt(shType, 10)); + + return Crypto.util.bytesToHex(sig); + } else { + return false; + } + } + + // https://tools.ietf.org/html/rfc6979#section-3.2 + r.deterministicK = function (wif, hash, badrs) { + // if r or s were invalid when this function was used in signing, + // we do not want to actually compute r, s here for efficiency, so, + // we can increment badrs. explained at end of RFC 6979 section 3.2 + + // wif is b58check encoded wif privkey. + // hash is byte array of transaction digest. + // badrs is used only if the k resulted in bad r or s. + + // some necessary things out of the way for clarity. + badrs = badrs || 0; + var key = coinjs.wif2privkey(wif); + var x = Crypto.util.hexToBytes(key['privkey']) + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + var N = curve.getN(); + + // Step: a + // hash is a byteArray of the message digest. so h1 == hash in our case + + // Step: b + var v = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; + + // Step: c + var k = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + // Step: d + k = Crypto.HMAC(Crypto.SHA256, v.concat([0]).concat(x).concat(hash), k, { + asBytes: true + }); + + // Step: e + v = Crypto.HMAC(Crypto.SHA256, v, k, { + asBytes: true + }); + + // Step: f + k = Crypto.HMAC(Crypto.SHA256, v.concat([1]).concat(x).concat(hash), k, { + asBytes: true + }); + + // Step: g + v = Crypto.HMAC(Crypto.SHA256, v, k, { + asBytes: true + }); + + // Step: h1 + var T = []; + + // Step: h2 (since we know tlen = qlen, just copy v to T.) + v = Crypto.HMAC(Crypto.SHA256, v, k, { + asBytes: true + }); + T = v; + + // Step: h3 + var KBigInt = BigInteger.fromByteArrayUnsigned(T); + + // loop if KBigInt is not in the range of [1, N-1] or if badrs needs incrementing. + var i = 0 + while (KBigInt.compareTo(N) >= 0 || KBigInt.compareTo(BigInteger.ZERO) <= 0 || i < badrs) { + k = Crypto.HMAC(Crypto.SHA256, v.concat([0]), k, { + asBytes: true + }); + v = Crypto.HMAC(Crypto.SHA256, v, k, { + asBytes: true + }); + v = Crypto.HMAC(Crypto.SHA256, v, k, { + asBytes: true + }); + T = v; + KBigInt = BigInteger.fromByteArrayUnsigned(T); + i++ + }; + + return KBigInt; + }; + + /* sign a "standard" input */ + r.signinput = function (index, wif, sigHashType) { + var key = coinjs.wif2pubkey(wif); + var shType = sigHashType || 1; + var signature = this.transactionSig(index, wif, shType); + var s = coinjs.script(); + s.writeBytes(Crypto.util.hexToBytes(signature)); + s.writeBytes(Crypto.util.hexToBytes(key['pubkey'])); + this.ins[index].script = s; + return true; + } + + /* signs a time locked / hodl input */ + r.signhodl = function (index, wif, sigHashType) { + var shType = sigHashType || 1; + var signature = this.transactionSig(index, wif, shType); + var redeemScript = this.ins[index].script.buffer + var s = coinjs.script(); + s.writeBytes(Crypto.util.hexToBytes(signature)); + s.writeBytes(redeemScript); + this.ins[index].script = s; + return true; + } + + r.signmultisig_bech32 = function (index, wif, sigHashType) { + + function scriptListPubkey(redeemScript) { + var r = {}; + for (var i = 1; i < redeemScript.chunks.length - 2; i++) { + r[i] = Crypto.util.hexToBytes(coinjs.pubkeydecompress(Crypto.util.bytesToHex(redeemScript.chunks[i]))); + } + return r; + } + + function scriptListSigs(sigList) { + let r = {}; + var c = 0; + if (Array.isArray(sigList)) { + for (let i = 1; i < sigList.length - 1; i++) { + c++; + r[c] = Crypto.util.hexToBytes(sigList[i]); + } + } + return r; + } + + var redeemScript = Crypto.util.bytesToHex(this.ins[index].script.chunks[0]); //redeemScript + + if (!coinjs.isArray(this.witness)) { + this.witness = new Array(this.ins.length); + this.witness.fill([]); + } + + var pubkeyList = scriptListPubkey(coinjs.script(redeemScript)); + var sigsList = scriptListSigs(this.witness[index]); + let decode_rs = coinjs.script().decodeRedeemScriptBech32(redeemScript); + + var shType = sigHashType || 1; + var txhash = this.transactionHashSegWitV0(index, shType); + + if (txhash.result == 1 && decode_rs.pubkeys.includes(coinjs.wif2pubkey(wif)['pubkey'])) { + + var segwitHash = Crypto.util.hexToBytes(txhash.hash); + var signature = Crypto.util.hexToBytes(this.transactionSig(index, wif, shType, segwitHash)); //CHECK THIS + + sigsList[coinjs.countObject(sigsList) + 1] = signature; + + var w = []; + + for (let x in pubkeyList) { + for (let y in sigsList) { + var sighash = this.transactionHashSegWitV0(index, sigsList[y].slice(-1)[0] * 1).hash + sighash = Crypto.util.hexToBytes(sighash); + if (coinjs.verifySignature(sighash, sigsList[y], pubkeyList[x])) { + w.push((Crypto.util.bytesToHex(sigsList[y]))) + } + } + } + + // when enough signatures collected, remove any non standard data we store, i.e. input value + if (w.length >= decode_rs.signaturesRequired) { + this.ins[index].script = coinjs.script(); + } + w.unshift(0); + w.push(redeemScript); + this.witness[index] = w; + } + } + + /* sign a multisig input */ + r.signmultisig = function (index, wif, sigHashType) { + + function scriptListPubkey(redeemScript) { + var r = {}; + for (var i = 1; i < redeemScript.chunks.length - 2; i++) { + r[i] = Crypto.util.hexToBytes(coinjs.pubkeydecompress(Crypto.util.bytesToHex(redeemScript.chunks[i]))); + } + return r; + } + + function scriptListSigs(scriptSig) { + var r = {}; + var c = 0; + if (scriptSig.chunks[0] == 0 && scriptSig.chunks[scriptSig.chunks.length - 1][scriptSig.chunks[scriptSig.chunks.length - 1].length - 1] == 174) { + for (var i = 1; i < scriptSig.chunks.length - 1; i++) { + if (scriptSig.chunks[i] != 0) { + c++; + r[c] = scriptSig.chunks[i]; + } + } + } + return r; + } + + var redeemScript = (this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1] == 174) ? this.ins[index].script.buffer : this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1]; + + var pubkeyList = scriptListPubkey(coinjs.script(redeemScript)); + var sigsList = scriptListSigs(this.ins[index].script); + + var shType = sigHashType || 1; + var sighash = Crypto.util.hexToBytes(this.transactionHash(index, shType)); + var signature = Crypto.util.hexToBytes(this.transactionSig(index, wif, shType)); + + sigsList[coinjs.countObject(sigsList) + 1] = signature; + + var s = coinjs.script(); + + s.writeOp(0); + + for (let x in pubkeyList) { + for (let y in sigsList) { + this.ins[index].script.buffer = redeemScript; + sighash = Crypto.util.hexToBytes(this.transactionHash(index, sigsList[y].slice(-1)[0] * 1)); + if (coinjs.verifySignature(sighash, sigsList[y], pubkeyList[x])) { + s.writeBytes(sigsList[y]); + } + } + } + + s.writeBytes(redeemScript); + this.ins[index].script = s; + return true; + } + + /* sign segwit input */ + r.signsegwit = function (index, wif, sigHashType) { + var shType = sigHashType || 1; + + var wif2 = coinjs.wif2pubkey(wif); + var segwit = coinjs.segwitAddress(wif2['pubkey']); + var bech32 = coinjs.bech32Address(wif2['pubkey']); + + if ((segwit['redeemscript'] == Crypto.util.bytesToHex(this.ins[index].script.chunks[0])) || (bech32['redeemscript'] == Crypto.util.bytesToHex(this.ins[index].script.chunks[0]))) { + var txhash = this.transactionHashSegWitV0(index, shType); + + if (txhash.result == 1) { + + var segwitHash = Crypto.util.hexToBytes(txhash.hash); + var signature = this.transactionSig(index, wif, shType, segwitHash); + + // remove any non standard data we store, i.e. input value + var script = coinjs.script(); + script.writeBytes(this.ins[index].script.chunks[0]); + this.ins[index].script = script; + + if (!coinjs.isArray(this.witness)) { + this.witness = new Array(this.ins.length); + this.witness.fill([]); + } + + this.witness[index] = ([signature, wif2['pubkey']]); + + // bech32, empty redeemscript + if (bech32['redeemscript'] == Crypto.util.bytesToHex(this.ins[index].script.chunks[0])) { + this.ins[index].script = coinjs.script(); + } + + /* attempt to reorder witness data as best as we can. + data can't be easily validated at this stage as + we dont have access to the inputs value and + making a web call will be too slow. */ + + /* + var witness_order = []; + var witness_used = []; + for (var i = 0; i < this.ins.length; i++) { + for (var y = 0; y < this.witness.length; y++) { + if (!witness_used.includes(y)) { + var sw = coinjs.segwitAddress(this.witness[y][1]); + var b32 = coinjs.bech32Address(this.witness[y][1]); + var rs = ''; + + if (this.ins[i].script.chunks.length >= 1) { + rs = Crypto.util.bytesToHex(this.ins[i].script.chunks[0]); + } else if (this.ins[i].script.chunks.length == 0) { + rs = b32['redeemscript']; + } + + if ((sw['redeemscript'] == rs) || (b32['redeemscript'] == rs)) { + witness_order.push(this.witness[y]); + witness_used.push(y); + + // bech32, empty redeemscript + if (b32['redeemscript'] == rs) { + this.ins[index].script = coinjs.script(); + } + break; + } + } + } + } + + this.witness = witness_order; + */ + } + } + return true; + } + + /* sign inputs */ + r.sign = function (wif, sigHashType) { + var shType = sigHashType || 1; + for (var i = 0; i < this.ins.length; i++) { + var d = this.extractScriptKey(i); + + var w2a = coinjs.wif2address(wif); + var script = coinjs.script(); + var pubkeyHash = script.pubkeyHash(w2a['address']); + + if (((d['type'] == 'scriptpubkey' && d['script'] == Crypto.util.bytesToHex(pubkeyHash.buffer)) || d['type'] == 'empty') && d['signed'] == "false") { + this.signinput(i, wif, shType); + + } else if (d['type'] == 'hodl' && d['signed'] == "false") { + this.signhodl(i, wif, shType); + + } else if (d['type'] == 'multisig') { + this.signmultisig(i, wif, shType); + + } else if (d['type'] == 'multisig_bech32' && d['signed'] == "false") { + this.signmultisig_bech32(i, wif, shType); + + } else if (d['type'] == 'segwit') { + this.signsegwit(i, wif, shType); + + } else { + // could not sign + } + } + return this.serialize(); + } + + /* serialize a transaction */ + r.serialize = function () { + var buffer = []; + buffer = buffer.concat(coinjs.numToBytes(parseInt(this.version), 4)); + + if (coinjs.isArray(this.witness)) { + buffer = buffer.concat([0x00, 0x01]); + } + + buffer = buffer.concat(coinjs.numToVarInt(this.ins.length)); + for (var i = 0; i < this.ins.length; i++) { + var txin = this.ins[i]; + buffer = buffer.concat(Crypto.util.hexToBytes(txin.outpoint.hash).reverse()); + buffer = buffer.concat(coinjs.numToBytes(parseInt(txin.outpoint.index), 4)); + var scriptBytes = txin.script.buffer; + buffer = buffer.concat(coinjs.numToVarInt(scriptBytes.length)); + buffer = buffer.concat(scriptBytes); + buffer = buffer.concat(coinjs.numToBytes(parseInt(txin.sequence), 4)); + } + buffer = buffer.concat(coinjs.numToVarInt(this.outs.length)); + + for (var i = 0; i < this.outs.length; i++) { + var txout = this.outs[i]; + buffer = buffer.concat(coinjs.numToBytes(txout.value, 8)); + var scriptBytes = txout.script.buffer; + buffer = buffer.concat(coinjs.numToVarInt(scriptBytes.length)); + buffer = buffer.concat(scriptBytes); + } + + if ((coinjs.isArray(this.witness)) && this.witness.length >= 1) { + for (var i = 0; i < this.witness.length; i++) { + buffer = buffer.concat(coinjs.numToVarInt(this.witness[i].length)); + for (var x = 0; x < this.witness[i].length; x++) { + buffer = buffer.concat(coinjs.numToVarInt(Crypto.util.hexToBytes(this.witness[i][x]).length)); + buffer = buffer.concat(Crypto.util.hexToBytes(this.witness[i][x])); + } + } + } + + buffer = buffer.concat(coinjs.numToBytes(parseInt(this.lock_time), 4)); + return Crypto.util.bytesToHex(buffer); + } + + //Utility funtion added to directly compute signatures without transaction index + r.transactionSigNoIndex = function (wif, sigHashType, txhash) { + + function serializeSig(r, s) { + var rBa = r.toByteArraySigned(); + var sBa = s.toByteArraySigned(); + + var sequence = []; + sequence.push(0x02); // INTEGER + sequence.push(rBa.length); + sequence = sequence.concat(rBa); + + sequence.push(0x02); // INTEGER + sequence.push(sBa.length); + sequence = sequence.concat(sBa); + + sequence.unshift(sequence.length); + sequence.unshift(0x30); // SEQUENCE + + return sequence; + } + + var shType = sigHashType || 1; + var hash = Crypto.util.hexToBytes(txhash); + + if (hash) { + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + var key = coinjs.wif2privkey(wif); + var priv = BigInteger.fromByteArrayUnsigned(Crypto.util.hexToBytes(key['privkey'])); + var n = curve.getN(); + var e = BigInteger.fromByteArrayUnsigned(hash); + + var badrs = 0 + do { + var k = this.deterministicK(wif, hash, badrs); + var G = curve.getG(); + var Q = G.multiply(k); + var r = Q.getX().toBigInteger().mod(n); + var s = k.modInverse(n).multiply(e.add(priv.multiply(r))).mod(n); + badrs++ + } while (r.compareTo(BigInteger.ZERO) <= 0 || s.compareTo(BigInteger.ZERO) <= 0); + + // Force lower s values per BIP62 + var halfn = n.shiftRight(1); + if (s.compareTo(halfn) > 0) { + s = n.subtract(s); + }; + + var sig = serializeSig(r, s); + sig.push(parseInt(shType, 10)); + + return Crypto.util.bytesToHex(sig); + } else { + return false; + } + } + + /* deserialize a transaction */ + r.deserialize = function (buffer) { + if (typeof buffer == "string") { + buffer = Crypto.util.hexToBytes(buffer) + } + + var pos = 0; + var witness = false; + + var readAsInt = function (bytes) { + if (bytes == 0) return 0; + pos++; + return buffer[pos - 1] + readAsInt(bytes - 1) * 256; + } + + var readVarInt = function () { + pos++; + if (buffer[pos - 1] < 253) { + return buffer[pos - 1]; + } + return readAsInt(buffer[pos - 1] - 251); + } + + var readBytes = function (bytes) { + pos += bytes; + return buffer.slice(pos - bytes, pos); + } + + var readVarString = function () { + var size = readVarInt(); + return readBytes(size); + } + + var obj = new coinjs.transaction(); + obj.version = readAsInt(4); + + if (buffer[pos] == 0x00 && buffer[pos + 1] == 0x01) { + // segwit transaction + witness = true; + obj.witness = []; + pos += 2; + } + + var ins = readVarInt(); + for (var i = 0; i < ins; i++) { + obj.ins.push({ + outpoint: { + hash: Crypto.util.bytesToHex(readBytes(32).reverse()), + index: readAsInt(4) + }, + script: coinjs.script(readVarString()), + sequence: readAsInt(4) + }); + } + + var outs = readVarInt(); + for (var i = 0; i < outs; i++) { + obj.outs.push({ + value: coinjs.bytesToNum(readBytes(8)), + script: coinjs.script(readVarString()) + }); + } + + if (witness == true) { + for (i = 0; i < ins; ++i) { + var count = readVarInt(); + var vector = []; + if (!coinjs.isArray(obj.witness[i])) { + obj.witness[i] = []; + } + for (var y = 0; y < count; y++) { + var slice = readVarInt(); + pos += slice; + obj.witness[i].push(Crypto.util.bytesToHex(buffer.slice(pos - slice, pos))); + } + } + } + + obj.lock_time = readAsInt(4); + return obj; + } + + r.size = function () { + return ((this.serialize()).length / 2).toFixed(0); + } + + return r; + } + + /* start of signature vertification functions */ + + coinjs.verifySignature = function (hash, sig, pubkey) { + + function parseSig(sig) { + var cursor; + if (sig[0] != 0x30) + throw new Error("Signature not a valid DERSequence"); + + cursor = 2; + if (sig[cursor] != 0x02) + throw new Error("First element in signature must be a DERInteger");; + + var rBa = sig.slice(cursor + 2, cursor + 2 + sig[cursor + 1]); + + cursor += 2 + sig[cursor + 1]; + if (sig[cursor] != 0x02) + throw new Error("Second element in signature must be a DERInteger"); + + var sBa = sig.slice(cursor + 2, cursor + 2 + sig[cursor + 1]); + + cursor += 2 + sig[cursor + 1]; + + var r = BigInteger.fromByteArrayUnsigned(rBa); + var s = BigInteger.fromByteArrayUnsigned(sBa); + + return { + r: r, + s: s + }; + } + + var r, s; + + if (coinjs.isArray(sig)) { + var obj = parseSig(sig); + r = obj.r; + s = obj.s; + } else if ("object" === typeof sig && sig.r && sig.s) { + r = sig.r; + s = sig.s; + } else { + throw "Invalid value for signature"; + } + + var Q; + if (coinjs.isArray(pubkey)) { + var ecparams = EllipticCurve.getSECCurveByName("secp256k1"); + Q = EllipticCurve.PointFp.decodeFrom(ecparams.getCurve(), pubkey); + } else { + throw "Invalid format for pubkey value, must be byte array"; + } + var e = BigInteger.fromByteArrayUnsigned(hash); + + return coinjs.verifySignatureRaw(e, r, s, Q); + } + + coinjs.verifySignatureRaw = function (e, r, s, Q) { + var ecparams = EllipticCurve.getSECCurveByName("secp256k1"); + var n = ecparams.getN(); + var G = ecparams.getG(); + + if (r.compareTo(BigInteger.ONE) < 0 || r.compareTo(n) >= 0) + return false; + + if (s.compareTo(BigInteger.ONE) < 0 || s.compareTo(n) >= 0) + return false; + + var c = s.modInverse(n); + + var u1 = e.multiply(c).mod(n); + var u2 = r.multiply(c).mod(n); + + var point = G.multiply(u1).add(Q.multiply(u2)); + + var v = point.getX().toBigInteger().mod(n); + + return v.equals(r); + } + + /* start of privates functions */ + + /* base58 encode function */ + coinjs.base58encode = function (buffer) { + var alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + var base = BigInteger.valueOf(58); + + var bi = BigInteger.fromByteArrayUnsigned(buffer); + var chars = []; + + while (bi.compareTo(base) >= 0) { + var mod = bi.mod(base); + chars.unshift(alphabet[mod.intValue()]); + bi = bi.subtract(mod).divide(base); + } + + chars.unshift(alphabet[bi.intValue()]); + for (var i = 0; i < buffer.length; i++) { + if (buffer[i] == 0x00) { + chars.unshift(alphabet[0]); + } else break; + } + return chars.join(''); + } + + /* base58 decode function */ + coinjs.base58decode = function (buffer) { + var alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + var base = BigInteger.valueOf(58); + var validRegex = /^[1-9A-HJ-NP-Za-km-z]+$/; + + var bi = BigInteger.valueOf(0); + var leadingZerosNum = 0; + for (var i = buffer.length - 1; i >= 0; i--) { + var alphaIndex = alphabet.indexOf(buffer[i]); + if (alphaIndex < 0) { + throw "Invalid character"; + } + bi = bi.add(BigInteger.valueOf(alphaIndex).multiply(base.pow(buffer.length - 1 - i))); + + if (buffer[i] == "1") leadingZerosNum++; + else leadingZerosNum = 0; + } + + var bytes = bi.toByteArrayUnsigned(); + while (leadingZerosNum-- > 0) bytes.unshift(0); + return bytes; + } + + /* raw ajax function to avoid needing bigger frame works like jquery, mootools etc */ + coinjs.ajax = function (u, f, m, a) { + var x = false; + try { + x = new ActiveXObject('Msxml2.XMLHTTP') + } catch (e) { + try { + x = new ActiveXObject('Microsoft.XMLHTTP') + } catch (e) { + x = new XMLHttpRequest() + } + } + + if (x == false) { + return false; + } + + x.open(m, u, true); + x.onreadystatechange = function () { + if ((x.readyState == 4) && f) + f(x.responseText); + }; + + if (m == 'POST') { + x.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + } + + x.send(a); + } + + /* clone an object */ + coinjs.clone = function (obj) { + if (obj == null || typeof (obj) != 'object') return obj; + var temp = new obj.constructor(); + + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + temp[key] = coinjs.clone(obj[key]); + } + } + return temp; + } + + coinjs.numToBytes = function (num, bytes) { + if (typeof bytes === "undefined") bytes = 8; + if (bytes == 0) { + return []; + } else if (num == -1) { + return Crypto.util.hexToBytes("ffffffffffffffff"); + } else { + return [num % 256].concat(coinjs.numToBytes(Math.floor(num / 256), bytes - 1)); + } + } + + function scriptNumSize(i) { + return i > 0x7fffffff ? 5 : + i > 0x7fffff ? 4 : + i > 0x7fff ? 3 : + i > 0x7f ? 2 : + i > 0x00 ? 1 : + 0; + } + + coinjs.numToScriptNumBytes = function (_number) { + var value = Math.abs(_number); + var size = scriptNumSize(value); + var result = []; + for (var i = 0; i < size; ++i) { + result.push(0); + } + var negative = _number < 0; + for (i = 0; i < size; ++i) { + result[i] = value & 0xff; + value = Math.floor(value / 256); + } + if (negative) { + result[size - 1] |= 0x80; + } + return result; + } + + coinjs.numToVarInt = function (num) { + if (num < 253) { + return [num]; + } else if (num < 65536) { + return [253].concat(coinjs.numToBytes(num, 2)); + } else if (num < 4294967296) { + return [254].concat(coinjs.numToBytes(num, 4)); + } else { + return [255].concat(coinjs.numToBytes(num, 8)); + } + } + + coinjs.bytesToNum = function (bytes) { + if (bytes.length == 0) return 0; + else return bytes[0] + 256 * coinjs.bytesToNum(bytes.slice(1)); + } + + coinjs.uint = function (f, size) { + if (f.length < size) + throw new Error("not enough data"); + var n = 0; + for (var i = 0; i < size; i++) { + n *= 256; + n += f[i]; + } + return n; + } + + coinjs.isArray = function (o) { + return Object.prototype.toString.call(o) === '[object Array]'; + } + + coinjs.countObject = function (obj) { + var count = 0; + var i; + for (i in obj) { + if (obj.hasOwnProperty(i)) { + count++; + } + } + return count; + } + + //Nine utility functions added for generating transaction hashes and verification of signatures + coinjs.changeEndianness = (string) => { + const result = []; + let len = string.length - 2; + while (len >= 0) { + result.push(string.substr(len, 2)); + len -= 2; + } + return result.join(''); + } + + coinjs.getTransactionHash = function (transaction_in_hex, changeOutputEndianess) { + var x1, x2, x3, x4, x5; + x1 = Crypto.util.hexToBytes(transaction_in_hex); + x2 = Crypto.SHA256(x1); + x3 = Crypto.util.hexToBytes(x2); + x4 = Crypto.SHA256(x3); + x5 = coinjs.changeEndianness(x4); + if (changeOutputEndianess == true) { x5 = x5 } else if ((typeof changeOutputEndianess == 'undefined') || (changeOutputEndianess == false)) { x5 = x4 }; + return x5; + } + + coinjs.compressedToUncompressed = function (compressed) { + var t1, t2; + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + t1 = curve.curve.decodePointHex(compressed); + t2 = curve.curve.encodePointHex(t1); + return t2; + } + + coinjs.uncompressedToCompressed = function (uncompressed) { + var t1, t2, t3; + t1 = uncompressed.charAt(uncompressed.length - 1) + t2 = parseInt(t1, 10); + //Check if the last digit is odd + if (t2 % 2 == 1) { t3 = "03"; } else { t3 = "02" }; + return t3 + uncompressed.substr(2, 64); + } + + coinjs.verifySignatureHex = function (hashHex, sigHex, pubHexCompressed) { + var h1, s1, p1, p2; + h1 = Crypto.util.hexToBytes(hashHex); + s1 = Crypto.util.hexToBytes(sigHex); + p1 = coinjs.compressedToUncompressed(pubHexCompressed); + p2 = Crypto.util.hexToBytes(p1); + + return coinjs.verifySignature(h1, s1, p2); + } + + coinjs.generateBitcoinSignature = function (private_key, hash, sighash_type_int = 1) { + var wif, tx1; + if (private_key.length < 60) { wif = private_key } else { wif = coinjs.privkey2wif(private_key) }; + tx1 = coinjs.transaction(); + return tx1.transactionSigNoIndex(wif, sighash_type_int, hash); + } + + coinjs.dSHA256 = function (data) { + var t1, t2, t3; + t1 = Crypto.SHA256(Crypto.util.hexToBytes(data)); + t2 = Crypto.util.hexToBytes(t1); + t3 = Crypto.SHA256(t2); + return t3; + } + + coinjs.fromBitcoinAmountFormat = function (data) { + var x1, x2, x3; + x1 = coinjs.changeEndianness(data); + x2 = parseInt(x1, 16); + x3 = x2 / (10 ** 8); + return x3; + } + + coinjs.toBitcoinAmountFormat = function (countBitcoin) { + var t2, t3, t4, t5; + t2 = countBitcoin * 10 ** 8; + t3 = t2.toString(16); + t4 = coinjs.changeEndianness(t3); + t5 = t4.padEnd(16, "0"); + return t5; + } + + coinjs.scriptcodeCreatorBasic = function (scriptpubkey) { + var t1, t2, t3, t4; + if (scriptpubkey.substr(0, 4) == "0014") { + //Scriptpubkey case + t1 = scriptpubkey.slice(2); + t2 = "1976a9" + t1 + "88ac"; + } else { + //Redeemscript case + t3 = (scriptpubkey.length) / 2; + t4 = t3.toString(16); + t2 = t4 + scriptpubkey; + } + return t2; + } + + coinjs.ripemd160sha256 = function (data) { + var t1, t2; + + t1 = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(data), { asBytes: true }), { asBytes: true }); + t2 = Crypto.util.bytesToHex(t1) + return t2; + } + + coinjs.random = function (length) { + var r = ""; + var l = length || 25; + var chars = "!$%^&*()_+{}:@~?><|\./;'#][=-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + for (let x = 0; x < l; x++) { + r += chars.charAt(Math.floor(Math.random() * 62)); + } + return r; + } + + })(); + + //secrets.js + (function () { + //Shamir Secret Share by Alexander Stetsyuk - released under MIT License + + var SecretShare = GLOBAL.shamirSecretShare = {}; + var defaults = { + bits: 8, // default number of bits + radix: 16, // work with HEX by default + minBits: 3, + maxBits: 20, // this permits 1,048,575 shares, though going this high is NOT recommended in JS! + + bytesPerChar: 2, + maxBytesPerChar: 6, // Math.pow(256,7) > Math.pow(2,53) + + // Primitive polynomials (in decimal form) for Galois Fields GF(2^n), for 2 <= n <= 30 + // The index of each term in the array corresponds to the n for that polynomial + // i.e. to get the polynomial for n=16, use primitivePolynomials[16] + primitivePolynomials: [null, null, 1, 3, 3, 5, 3, 3, 29, 17, 9, 5, 83, 27, 43, 3, 45, 9, 39, 39, + 9, 5, 3, 33, 27, 9, 71, 39, 9, 5, 83 + ], + + // warning for insecure PRNG + warning: 'WARNING:\nA secure random number generator was not found.\nUsing securedMathRandom(), which is NOT cryptographically strong!' + }; + + // Protected settings object + var config = {}; + + /** @expose **/ + SecretShare.getConfig = function () { + return { + 'bits': config.bits, + 'unsafePRNG': config.unsafePRNG + }; + }; + + function init(bits) { + if (bits && (typeof bits !== 'number' || bits % 1 !== 0 || bits < defaults.minBits || bits > + defaults.maxBits)) { + throw new Error('Number of bits must be an integer between ' + defaults.minBits + ' and ' + + defaults.maxBits + ', inclusive.') + } + + config.radix = defaults.radix; + config.bits = bits || defaults.bits; + config.size = Math.pow(2, config.bits); + config.max = config.size - 1; + + // Construct the exp and log tables for multiplication. + var logs = [], + exps = [], + x = 1, + primitive = defaults.primitivePolynomials[config.bits]; + for (var i = 0; i < config.size; i++) { + exps[i] = x; + logs[x] = i; + x <<= 1; + if (x >= config.size) { + x ^= primitive; + x &= config.max; + } + } + + config.logs = logs; + config.exps = exps; + }; + + /** @expose **/ + SecretShare.init = init; + + function isInited() { + if (!config.bits || !config.size || !config.max || !config.logs || !config.exps || config.logs.length !== + config.size || config.exps.length !== config.size) { + return false; + } + return true; + }; + + // Returns a pseudo-random number generator of the form function(bits){} + // which should output a random string of 1's and 0's of length `bits` + function getRNG() { + var randomBits, crypto; + + function construct(bits, arr, radix, size) { + var str = '', + i = 0, + len = arr.length - 1; + while (i < len || (str.length < bits)) { + str += padLeft(parseInt(arr[i], radix).toString(2), size); + i++; + } + str = str.substr(-bits); + if ((str.match(/0/g) || []).length === str.length) { // all zeros? + return null; + } else { + return str; + } + } + + // node.js crypto.randomBytes() + if (typeof require === 'function') { + return function (bits) { + var bytes = Math.ceil(bits / 8), + str = null; + + while (str === null) { + str = construct(bits, require('crypto').randomBytes(bytes).toString('hex'), 16, 4); + } + return str; + } + } + + // browsers with window.crypto.getRandomValues() + if (GLOBAL['crypto'] && typeof GLOBAL['crypto']['getRandomValues'] === 'function' && typeof GLOBAL['Uint32Array'] === 'function') { + crypto = GLOBAL['crypto']; + return function (bits) { + var elems = Math.ceil(bits / 32), + str = null, + arr = new GLOBAL['Uint32Array'](elems); + + while (str === null) { + crypto['getRandomValues'](arr); + str = construct(bits, arr, 10, 32); + } + + return str; + } + } + + // A totally insecure RNG!!! (except in Safari) + // Will produce a warning every time it is called. + config.unsafePRNG = true; + warn(); + + var bitsPerNum = 32; + var max = Math.pow(2, bitsPerNum) - 1; + return function (bits) { + var elems = Math.ceil(bits / bitsPerNum); + var arr = [], + str = null; + while (str === null) { + for (var i = 0; i < elems; i++) { + arr[i] = Math.floor(securedMathRandom() * max + 1); + } + str = construct(bits, arr, 10, bitsPerNum); + } + return str; + }; + }; + + // Warn about using insecure rng. + // Called when securedMathRandom() is being used. + function warn() { + GLOBAL['console']['warn'](defaults.warning); + if (typeof GLOBAL['alert'] === 'function' && config.alert) { + GLOBAL['alert'](defaults.warning); + } + } + + // Set the PRNG to use. If no RNG function is supplied, pick a default using getRNG() + /** @expose **/ + SecretShare.setRNG = function (rng, alert) { + if (!isInited()) { + this.init(); + } + config.unsafePRNG = false; + rng = rng || getRNG(); + + // test the RNG (5 times) + if (typeof rng !== 'function' || typeof rng(config.bits) !== 'string' || !parseInt(rng(config.bits), + 2) || rng(config.bits).length > config.bits || rng(config.bits).length < config.bits) { + throw new Error( + "Random number generator is invalid. Supply an RNG of the form function(bits){} that returns a string containing 'bits' number of random 1's and 0's." + ) + } else { + config.rng = rng; + } + config.alert = !!alert; + + return !!config.unsafePRNG; + }; + + function isSetRNG() { + return typeof config.rng === 'function'; + }; + + // Generates a random bits-length number string using the PRNG + /** @expose **/ + SecretShare.random = function (bits) { + if (!isSetRNG()) { + this.setRNG(); + } + + if (typeof bits !== 'number' || bits % 1 !== 0 || bits < 2) { + throw new Error('Number of bits must be an integer greater than 1.') + } + + if (config.unsafePRNG) { + warn(); + } + return bin2hex(config.rng(bits)); + } + + // Divides a `secret` number String str expressed in radix `inputRadix` (optional, default 16) + // into `numShares` shares, each expressed in radix `outputRadix` (optional, default to `inputRadix`), + // requiring `threshold` number of shares to reconstruct the secret. + // Optionally, zero-pads the secret to a length that is a multiple of padLength before sharing. + /** @expose **/ + SecretShare.share = function (secret, numShares, threshold, padLength, withoutPrefix) { + if (!isInited()) { + this.init(); + } + if (!isSetRNG()) { + this.setRNG(); + } + + padLength = padLength || 0; + + if (typeof secret !== 'string') { + throw new Error('Secret must be a string.'); + } + if (typeof numShares !== 'number' || numShares % 1 !== 0 || numShares < 2) { + throw new Error('Number of shares must be an integer between 2 and 2^bits-1 (' + config.max + + '), inclusive.') + } + if (numShares > config.max) { + var neededBits = Math.ceil(Math.log(numShares + 1) / Math.LN2); + throw new Error('Number of shares must be an integer between 2 and 2^bits-1 (' + config.max + + '), inclusive. To create ' + numShares + ' shares, use at least ' + neededBits + + ' bits.') + } + if (typeof threshold !== 'number' || threshold % 1 !== 0 || threshold < 2) { + throw new Error('Threshold number of shares must be an integer between 2 and 2^bits-1 (' + + config.max + '), inclusive.'); + } + if (threshold > config.max) { + var neededBits = Math.ceil(Math.log(threshold + 1) / Math.LN2); + throw new Error('Threshold number of shares must be an integer between 2 and 2^bits-1 (' + + config.max + '), inclusive. To use a threshold of ' + threshold + + ', use at least ' + neededBits + ' bits.'); + } + if (typeof padLength !== 'number' || padLength % 1 !== 0) { + throw new Error('Zero-pad length must be an integer greater than 1.'); + } + + if (config.unsafePRNG) { + warn(); + } + + secret = '1' + hex2bin(secret); // append a 1 so that we can preserve the correct number of leading zeros in our secret + secret = split(secret, padLength); + var x = new Array(numShares), + y = new Array(numShares); + for (var i = 0, len = secret.length; i < len; i++) { + var subShares = this._getShares(secret[i], numShares, threshold); + for (var j = 0; j < numShares; j++) { + x[j] = x[j] || subShares[j].x.toString(config.radix); + y[j] = padLeft(subShares[j].y.toString(2)) + (y[j] ? y[j] : ''); + } + } + var padding = config.max.toString(config.radix).length; + if (withoutPrefix) { + for (var i = 0; i < numShares; i++) { + x[i] = bin2hex(y[i]); + } + } else { + for (var i = 0; i < numShares; i++) { + x[i] = config.bits.toString(36).toUpperCase() + padLeft(x[i], padding) + bin2hex(y[i]); + } + } + + return x; + }; + + // This is the basic polynomial generation and evaluation function + // for a `config.bits`-length secret (NOT an arbitrary length) + // Note: no error-checking at this stage! If `secrets` is NOT + // a NUMBER less than 2^bits-1, the output will be incorrect! + /** @expose **/ + SecretShare._getShares = function (secret, numShares, threshold) { + var shares = []; + var coeffs = [secret]; + + for (var i = 1; i < threshold; i++) { + coeffs[i] = parseInt(config.rng(config.bits), 2); + } + for (var i = 1, len = numShares + 1; i < len; i++) { + shares[i - 1] = { + x: i, + y: horner(i, coeffs) + } + } + return shares; + }; + + // Polynomial evaluation at `x` using Horner's Method + // TODO: this can possibly be sped up using other methods + // NOTE: fx=fx * x + coeff[i] -> exp(log(fx) + log(x)) + coeff[i], + // so if fx===0, just set fx to coeff[i] because + // using the exp/log form will result in incorrect value + function horner(x, coeffs) { + var logx = config.logs[x]; + var fx = 0; + for (var i = coeffs.length - 1; i >= 0; i--) { + if (fx === 0) { + fx = coeffs[i]; + continue; + } + fx = config.exps[(logx + config.logs[fx]) % config.max] ^ coeffs[i]; + } + return fx; + }; + + function inArray(arr, val) { + for (var i = 0, len = arr.length; i < len; i++) { + if (arr[i] === val) { + return true; + } + } + return false; + }; + + function processShare(share) { + + var bits = parseInt(share[0], 36); + if (bits && (typeof bits !== 'number' || bits % 1 !== 0 || bits < defaults.minBits || bits > + defaults.maxBits)) { + throw new Error('Number of bits must be an integer between ' + defaults.minBits + ' and ' + + defaults.maxBits + ', inclusive.') + } + + var max = Math.pow(2, bits) - 1; + var idLength = max.toString(config.radix).length; + + var id = parseInt(share.substr(1, idLength), config.radix); + if (typeof id !== 'number' || id % 1 !== 0 || id < 1 || id > max) { + throw new Error('Share id must be an integer between 1 and ' + config.max + ', inclusive.'); + } + share = share.substr(idLength + 1); + if (!share.length) { + throw new Error('Invalid share: zero-length share.') + } + return { + 'bits': bits, + 'id': id, + 'value': share + }; + }; + + /** @expose **/ + SecretShare._processShare = processShare; + + // Protected method that evaluates the Lagrange interpolation + // polynomial at x=`at` for individual config.bits-length + // segments of each share in the `shares` Array. + // Each share is expressed in base `inputRadix`. The output + // is expressed in base `outputRadix' + function combine(at, shares) { + var setBits, share, x = [], + y = [], + result = '', + idx; + + for (var i = 0, len = shares.length; i < len; i++) { + share = processShare(shares[i]); + if (typeof setBits === 'undefined') { + setBits = share['bits']; + } else if (share['bits'] !== setBits) { + throw new Error('Mismatched shares: Different bit settings.') + } + + if (config.bits !== setBits) { + init(setBits); + } + + if (inArray(x, share['id'])) { // repeated x value? + continue; + } + + idx = x.push(share['id']) - 1; + share = split(hex2bin(share['value'])); + for (var j = 0, len2 = share.length; j < len2; j++) { + y[j] = y[j] || []; + y[j][idx] = share[j]; + } + } + + for (var i = 0, len = y.length; i < len; i++) { + result = padLeft(lagrange(at, x, y[i]).toString(2)) + result; + } + + if (at === 0) { // reconstructing the secret + var idx = result.indexOf('1'); //find the first 1 + return bin2hex(result.slice(idx + 1)); + } else { // generating a new share + return bin2hex(result); + } + }; + + // Combine `shares` Array into the original secret + /** @expose **/ + SecretShare.combine = function (shares) { + return combine(0, shares); + }; + + // Generate a new share with id `id` (a number between 1 and 2^bits-1) + // `id` can be a Number or a String in the default radix (16) + /** @expose **/ + SecretShare.newShare = function (id, shares) { + if (typeof id === 'string') { + id = parseInt(id, config.radix); + } + + var share = processShare(shares[0]); + var max = Math.pow(2, share['bits']) - 1; + + if (typeof id !== 'number' || id % 1 !== 0 || id < 1 || id > max) { + throw new Error('Share id must be an integer between 1 and ' + config.max + ', inclusive.'); + } + + var padding = max.toString(config.radix).length; + return config.bits.toString(36).toUpperCase() + padLeft(id.toString(config.radix), padding) + + combine(id, shares); + }; + + // Evaluate the Lagrange interpolation polynomial at x = `at` + // using x and y Arrays that are of the same length, with + // corresponding elements constituting points on the polynomial. + function lagrange(at, x, y) { + var sum = 0, + product, + i, j; + + for (var i = 0, len = x.length; i < len; i++) { + if (!y[i]) { + continue; + } + + product = config.logs[y[i]]; + for (var j = 0; j < len; j++) { + if (i === j) { + continue; + } + if (at === x[j]) { // happens when computing a share that is in the list of shares used to compute it + product = -1; // fix for a zero product term, after which the sum should be sum^0 = sum, not sum^1 + break; + } + product = (product + config.logs[at ^ x[j]] - config.logs[x[i] ^ x[j]] + config.max /* to make sure it's not negative */) % + config.max; + } + + sum = product === -1 ? sum : sum ^ config.exps[product]; // though exps[-1]= undefined and undefined ^ anything = anything in chrome, this behavior may not hold everywhere, so do the check + } + return sum; + }; + + /** @expose **/ + SecretShare._lagrange = lagrange; + + // Splits a number string `bits`-length segments, after first + // optionally zero-padding it to a length that is a multiple of `padLength. + // Returns array of integers (each less than 2^bits-1), with each element + // representing a `bits`-length segment of the input string from right to left, + // i.e. parts[0] represents the right-most `bits`-length segment of the input string. + function split(str, padLength) { + if (padLength) { + str = padLeft(str, padLength) + } + var parts = []; + for (var i = str.length; i > config.bits; i -= config.bits) { + parts.push(parseInt(str.slice(i - config.bits, i), 2)); + } + parts.push(parseInt(str.slice(0, i), 2)); + return parts; + }; + + // Pads a string `str` with zeros on the left so that its length is a multiple of `bits` + function padLeft(str, bits) { + bits = bits || config.bits + var missing = str.length % bits; + return (missing ? new Array(bits - missing + 1).join('0') : '') + str; + }; + + function hex2bin(str) { + var bin = '', + num; + for (var i = str.length - 1; i >= 0; i--) { + num = parseInt(str[i], 16) + if (isNaN(num)) { + throw new Error('Invalid hex character.') + } + bin = padLeft(num.toString(2), 4) + bin; + } + return bin; + } + + function bin2hex(str) { + var hex = '', + num; + str = padLeft(str, 4); + for (var i = str.length; i >= 4; i -= 4) { + num = parseInt(str.slice(i - 4, i), 2); + if (isNaN(num)) { + throw new Error('Invalid binary character.') + } + hex = num.toString(16) + hex; + } + return hex; + } + + // Converts a given UTF16 character string to the HEX representation. + // Each character of the input string is represented by + // `bytesPerChar` bytes in the output string. + /** @expose **/ + SecretShare.str2hex = function (str, bytesPerChar) { + if (typeof str !== 'string') { + throw new Error('Input must be a character string.'); + } + bytesPerChar = bytesPerChar || defaults.bytesPerChar; + + if (typeof bytesPerChar !== 'number' || bytesPerChar % 1 !== 0 || bytesPerChar < 1 || + bytesPerChar > defaults.maxBytesPerChar) { + throw new Error('Bytes per character must be an integer between 1 and ' + defaults.maxBytesPerChar + + ', inclusive.') + } + + var hexChars = 2 * bytesPerChar; + var max = Math.pow(16, hexChars) - 1; + var out = '', + num; + for (var i = 0, len = str.length; i < len; i++) { + num = str[i].charCodeAt(); + if (isNaN(num)) { + throw new Error('Invalid character: ' + str[i]); + } else if (num > max) { + var neededBytes = Math.ceil(Math.log(num + 1) / Math.log(256)); + throw new Error('Invalid character code (' + num + + '). Maximum allowable is 256^bytes-1 (' + max + + '). To convert this character, use at least ' + neededBytes + ' bytes.') + } else { + out = padLeft(num.toString(16), hexChars) + out; + } + } + return out; + }; + + // Converts a given HEX number string to a UTF16 character string. + /** @expose **/ + SecretShare.hex2str = function (str, bytesPerChar) { + if (typeof str !== 'string') { + throw new Error('Input must be a hexadecimal string.'); + } + bytesPerChar = bytesPerChar || defaults.bytesPerChar; + + if (typeof bytesPerChar !== 'number' || bytesPerChar % 1 !== 0 || bytesPerChar < 1 || + bytesPerChar > defaults.maxBytesPerChar) { + throw new Error('Bytes per character must be an integer between 1 and ' + defaults.maxBytesPerChar + + ', inclusive.') + } + + var hexChars = 2 * bytesPerChar; + var out = ''; + str = padLeft(str, hexChars); + for (var i = 0, len = str.length; i < len; i += hexChars) { + out = String.fromCharCode(parseInt(str.slice(i, i + hexChars), 16)) + out; + } + return out; + }; + + // by default, initialize without an RNG + SecretShare.init(); + })(); + + //kbucket.js + (function () { + // Kademlia DHT K-bucket implementation as a binary tree. + // by 'Tristan Slominski' under 'MIT License' + GLOBAL.BuildKBucket = function KBucket(options = {}) { + if (!(this instanceof KBucket)) + return new KBucket(options); + this.localNodeId = options.localNodeId || getRandomBytes(new Uint8Array(20)) + this.numberOfNodesPerKBucket = options.numberOfNodesPerKBucket || 20 + this.numberOfNodesToPing = options.numberOfNodesToPing || 3 + this.distance = options.distance || this.distance + this.arbiter = options.arbiter || this.arbiter + this.metadata = Object.assign({}, options.metadata) + + this.createNode = function () { + return { + contacts: [], + dontSplit: false, + left: null, + right: null + } + } + + this.ensureInt8 = function (name, val) { + if (!(val instanceof Uint8Array)) + throw new TypeError(name + ' is not a Uint8Array') + } + + this.arrayEquals = function (array1, array2) { + if (array1 === array2) + return true + if (array1.length !== array2.length) + return false + for (let i = 0, length = array1.length; i < length; ++i) + if (array1[i] !== array2[i]) + return false + return true + } + + this.ensureInt8('option.localNodeId as parameter 1', this.localNodeId) + this.root = this.createNode() + + this.arbiter = function (incumbent, candidate) { + return incumbent.vectorClock > candidate.vectorClock ? incumbent : candidate + } + + this.distance = function (firstId, secondId) { + let distance = 0 + let i = 0 + const min = Math.min(firstId.length, secondId.length) + const max = Math.max(firstId.length, secondId.length) + for (; i < min; ++i) + distance = distance * 256 + (firstId[i] ^ secondId[i]) + for (; i < max; ++i) distance = distance * 256 + 255 + return distance + } + + this.add = function (contact) { + this.ensureInt8('contact.id', (contact || {}).id) + let bitIndex = 0 + let node = this.root + while (node.contacts === null) + node = this._determineNode(node, contact.id, bitIndex++) + const index = this._indexOf(node, contact.id) + if (index >= 0) { + this._update(node, index, contact) + return this + } + if (node.contacts.length < this.numberOfNodesPerKBucket) { + node.contacts.push(contact) + return this + } + if (node.dontSplit) + return this + this._split(node, bitIndex) + return this.add(contact) + } + + this.closest = function (id, n = Infinity) { + this.ensureInt8('id', id) + if ((!Number.isInteger(n) && n !== Infinity) || n <= 0) + throw new TypeError('n is not positive number') + let contacts = [] + for (let nodes = [this.root], bitIndex = 0; nodes.length > 0 && contacts.length < n;) { + const node = nodes.pop() + if (node.contacts === null) { + const detNode = this._determineNode(node, id, bitIndex++) + nodes.push(node.left === detNode ? node.right : node.left) + nodes.push(detNode) + } else + contacts = contacts.concat(node.contacts) + } + return contacts + .map(a => [this.distance(a.id, id), a]) + .sort((a, b) => a[0] - b[0]) + .slice(0, n) + .map(a => a[1]) + } + + this.count = function () { + let count = 0 + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + if (node.contacts === null) + nodes.push(node.right, node.left) + else + count += node.contacts.length + } + return count + } + + this._determineNode = function (node, id, bitIndex) { + const bytesDescribedByBitIndex = bitIndex >> 3 + const bitIndexWithinByte = bitIndex % 8 + if ((id.length <= bytesDescribedByBitIndex) && (bitIndexWithinByte !== 0)) + return node.left + const byteUnderConsideration = id[bytesDescribedByBitIndex] + if (byteUnderConsideration & (1 << (7 - bitIndexWithinByte))) + return node.right + return node.left + } + + this.get = function (id) { + this.ensureInt8('id', id) + let bitIndex = 0 + let node = this.root + while (node.contacts === null) + node = this._determineNode(node, id, bitIndex++) + const index = this._indexOf(node, id) + return index >= 0 ? node.contacts[index] : null + } + + this._indexOf = function (node, id) { + for (let i = 0; i < node.contacts.length; ++i) + if (this.arrayEquals(node.contacts[i].id, id)) + return i + return -1 + } + + this.remove = function (id) { + this.ensureInt8('the id as parameter 1', id) + let bitIndex = 0 + let node = this.root + while (node.contacts === null) + node = this._determineNode(node, id, bitIndex++) + const index = this._indexOf(node, id) + if (index >= 0) + node.contacts.splice(index, 1)[0] + return this + } + + this._split = function (node, bitIndex) { + node.left = this.createNode() + node.right = this.createNode() + for (const contact of node.contacts) + this._determineNode(node, contact.id, bitIndex).contacts.push(contact) + node.contacts = null + const detNode = this._determineNode(node, this.localNodeId, bitIndex) + const otherNode = node.left === detNode ? node.right : node.left + otherNode.dontSplit = true + } + + this.toArray = function () { + let result = [] + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + if (node.contacts === null) + nodes.push(node.right, node.left) + else + result = result.concat(node.contacts) + } + return result + } + + this._update = function (node, index, contact) { + if (!this.arrayEquals(node.contacts[index].id, contact.id)) + throw new Error('wrong index for _update') + const incumbent = node.contacts[index] + const selection = this.arbiter(incumbent, contact) + if (selection === incumbent && incumbent !== contact) return + node.contacts.splice(index, 1) + node.contacts.push(selection) + } + } + })(); + +})(typeof global !== "undefined" ? global : window); \ No newline at end of file diff --git a/messenger/scripts/messenger.js b/messenger/scripts/messenger.js new file mode 100644 index 0000000..e10663d --- /dev/null +++ b/messenger/scripts/messenger.js @@ -0,0 +1,1654 @@ +(function () { + const messenger = window.messenger = {}; + + const user = { + get id() { + return floDapps.user.id + }, + get public() { + return floDapps.user.public + } + } + + const expiredKeys = {}; + + const UI = { + group: (d, e) => console.log(d, e), + pipeline: (d, e) => console.log(d, e), + direct: (d, e) => console.log(d, e), + chats: (c) => console.log(c), + mails: (m) => console.log(m), + marked: (r) => console.log(r) + }; + messenger.renderUI = {}; + Object.defineProperties(messenger.renderUI, { + chats: { + set: ui_fn => UI.chats = ui_fn + }, + directChat: { + set: ui_fn => UI.direct = ui_fn + }, + groupChat: { + set: ui_fn => UI.group = ui_fn + }, + pipeline: { + set: ui_fn => UI.pipeline = ui_fn + }, + mails: { + set: ui_fn => UI.mails = ui_fn + }, + marked: { + set: ui_fn => UI.marked = ui_fn + } + }); + + const _loaded = {}; + Object.defineProperties(messenger, { + chats: { + get: () => _loaded.chats + }, + groups: { + get: () => _loaded.groups + }, + pipeline: { + get: () => _loaded.pipeline + }, + blocked: { + get: () => _loaded.blocked + }, + marked: { + get: () => _loaded.marked + } + }); + + var directConnID = [], groupConnID = {}, + pipeConnID = {}; + messenger.conn = {}; + Object.defineProperties(messenger.conn, { + direct: { + get: () => directConnID + }, + group: { + get: () => Object.assign({}, groupConnID), + // value: g_id => groupConnID[g_id] + } + }); + + function sendRaw(message, recipient, type, encrypt = null, comment = undefined) { + return new Promise((resolve, reject) => { + if (!floCrypto.validateAddr(recipient)) + return reject("Invalid Recipient"); + + if ([true, null].includes(encrypt)) { + let r_pubKey = floDapps.user.get_pubKey(recipient); + if (r_pubKey) + message = floCrypto.encryptData(message, r_pubKey); + else if (encrypt === true) + return reject("recipient's pubKey not found") + } + let options = { + receiverID: recipient, + } + if (comment) + options.comment = comment + floCloudAPI.sendApplicationData(message, type, options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + messenger.sendRaw = sendRaw; + + function encrypt(value, key = _loaded.appendix.AESKey) { + return Crypto.AES.encrypt(value, key) + } + messenger.encrypt = encrypt; + + function decrypt(value, key = _loaded.appendix.AESKey) { + return Crypto.AES.decrypt(value, key) + } + + function addMark(key, mark) { + return new Promise((resolve, reject) => { + compactIDB.readData("marked", key).then(result => { + if (!result) + result = [mark]; + else if (!result.includes(mark)) + result.push(mark); + else + return resolve("Mark already exist"); + compactIDB.writeData("marked", result, key) + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + function removeMark(key, mark) { + return new Promise((resolve, reject) => { + compactIDB.readData("marked", key).then(result => { + if (!result || !result.includes(mark)) + return resolve("Mark doesnot exist") + else { + result.splice(result.indexOf(mark), 1); //remove the mark from the list of marks + compactIDB.writeData("marked", result, key) + .then(result => resolve("Mark removed")) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + } + + const initUserDB = function () { + return new Promise((resolve, reject) => { + var obj = { + messages: {}, + mails: {}, + marked: {}, + chats: {}, + groups: {}, + gkeys: {}, + blocked: {}, + pipeline: {}, + request_sent: {}, + request_received: {}, + response_sent: {}, + response_received: {}, + flodata: {}, + appendix: {}, + userSettings: {}, + multisigLabels: {} + } + let user_db = `${floGlobals.application}_${floCrypto.toFloID(user.id)}`; + compactIDB.initDB(user_db, obj).then(result => { + console.info(result) + compactIDB.setDefaultDB(user_db); + resolve("Messenger UserDB Initated Successfully") + }).catch(error => reject(error)); + }) + } + + messenger.blockUser = function (floID) { + return new Promise((resolve, reject) => { + if (_loaded.blocked.has(floID)) + return resolve("User is already blocked"); + compactIDB.addData("blocked", true, floID).then(result => { + _loaded.blocked.add(floID); + resolve("Blocked User: " + floID); + }).catch(error => reject(error)) + }) + } + + messenger.unblockUser = function (floID) { + return new Promise((resolve, reject) => { + if (!_loaded.blocked.has(floID)) + return resolve("User is not blocked"); + compactIDB.removeData("blocked", floID).then(result => { + _loaded.blocked.delete(floID); + resolve("Unblocked User: " + floID); + }).catch(error => reject(error)) + }) + } + + messenger.sendMessage = function (message, receiver) { + return new Promise((resolve, reject) => { + sendRaw(message, receiver, "MESSAGE").then(result => { + let vc = result.vectorClock; + let data = { + floID: receiver, + time: result.time, + category: 'sent', + message: encrypt(message) + } + _loaded.chats[receiver] = parseInt(vc) + compactIDB.writeData("chats", parseInt(vc), receiver) + compactIDB.addData("messages", Object.assign({}, data), `${receiver}|${vc}`) + data.message = message; + resolve({ + [vc]: data + }); + }).catch(error => reject(error)) + }) + } + + messenger.sendMail = function (subject, content, recipients, prev = null) { + return new Promise((resolve, reject) => { + if (!Array.isArray(recipients)) + recipients = [recipients] + let mail = { + subject: subject, + content: content, + ref: Date.now() + floCrypto.randString(8, true), + prev: prev + } + let promises = recipients.map(r => sendRaw(JSON.stringify(mail), r, "MAIL")) + Promise.allSettled(promises).then(results => { + mail.time = Date.now(); + mail.from = user.id + mail.to = [] + results.forEach(r => { + if (r.status === "fulfilled") + mail.to.push(r.value.receiverID) + }); + if (mail.to.length === 0) + return reject(results) + mail.content = encrypt(content) + compactIDB.addData("mails", Object.assign({}, mail), mail.ref) + mail.content = content + resolve({ + [mail.ref]: mail + }); + }) + }) + } + + function listRequests(obs, options = null) { + return new Promise((resolve, reject) => { + compactIDB.readAllData(obs).then(result => { + if (!options || typeof options !== 'object') + return resolve(result); + let filtered = {}; + for (let k in result) { + let val = result[k]; + if (options.type && options.type == val.type) continue; + else if (options.floID && options.floID == val.floID) continue; + else if (typeof options.completed !== 'undefined' && options.completed == !(val.completed)) + continue; + filtered[k] = val; + } + resolve(filtered); + }).catch(error => reject(error)) + }) + } + + messenger.list_request_sent = (options = null) => listRequests('request_sent', options); + messenger.list_request_received = (options = null) => listRequests('request_received', options); + messenger.list_response_sent = (options = null) => listRequests('response_sent', options); + messenger.list_response_received = (options = null) => listRequests('response_received', options); + + function sendRequest(receiver, type, message, encrypt = null) { + return new Promise((resolve, reject) => { + sendRaw(message, receiver, "REQUEST", encrypt, type).then(result => { + let vc = result.vectorClock; + let data = { + floID: receiver, + time: result.time, + message: message, + type: type + } + compactIDB.addData("request_sent", data, vc); + resolve({ + [vc]: data + }); + }).catch(error => reject(error)) + }) + } + + messenger.request_pubKey = (receiver, message = '') => sendRequest(receiver, "PUBLIC_KEY", message, false); + + function sendResponse(req_id, message, encrypt = null) { + return new Promise((resolve, reject) => { + compactIDB.readData("request_received", req_id).then(request => { + let _message = JSON.stringify({ + value: message, + reqID: req_id + }); + sendRaw(_message, request.floID, "RESPONSE", encrypt, request.type).then(result => { + let vc = result.vectorClock; + let data = { + floID: request.floID, + time: result.time, + message: message, + type: request.type, + reqID: req_id + } + compactIDB.addData("response_sent", data, vc); + request.completed = vc; + compactIDB.writeData("request_received", request, req_id); + resolve({ + [vc]: data + }); + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + messenger.respond_pubKey = (req_id, message = '') => sendResponse(req_id, message, false); + + const processData = {}; + processData.direct = function () { + return (unparsed, newInbox) => { + //store the pubKey if not stored already + floDapps.storePubKey(unparsed.senderID, unparsed.pubKey); + if (_loaded.blocked.has(unparsed.senderID) && unparsed.type !== "REVOKE_KEY") + throw "blocked-user"; + if (unparsed.message instanceof Object && "secret" in unparsed.message) + unparsed.message = floDapps.user.decrypt(unparsed.message); + let vc = unparsed.vectorClock; + switch (unparsed.type) { + case "MESSAGE": { //process as message + let dm = { + time: unparsed.time, + floID: unparsed.senderID, + category: "received", + message: encrypt(unparsed.message) + } + console.debug(dm, `${dm.floID}|${vc}`); + compactIDB.addData("messages", Object.assign({}, dm), `${dm.floID}|${vc}`) + _loaded.chats[dm.floID] = parseInt(vc) + compactIDB.writeData("chats", parseInt(vc), dm.floID) + dm.message = unparsed.message; + newInbox.messages[vc] = dm; + addMark(dm.floID, "unread"); + break; + } + case "REQUEST": { + let req = { + floID: unparsed.senderID, + time: unparsed.time, + message: unparsed.message, + type: unparsed.comment + } + compactIDB.addData("request_received", req, vc); + newInbox.requests[vc] = req; + break; + } + case "RESPONSE": { + let data = JSON.parse(unparsed.message); + let res = { + floID: unparsed.senderID, + time: unparsed.time, + message: data.value, + type: unparsed.comment, + reqID: data.reqID + } + compactIDB.addData("response_received", res, vc); + compactIDB.readData("request_sent", data.reqID).then(req => { + req.completed = vc; + compactIDB.writeData("request_sent", req, data.reqID) + }); + newInbox.responses[vc] = res; + break; + } + case "MAIL": { //process as mail + let data = JSON.parse(unparsed.message); + let mail = { + time: unparsed.time, + from: unparsed.senderID, + to: [unparsed.receiverID], + subject: data.subject, + content: encrypt(data.content), + ref: data.ref, + prev: data.prev + } + compactIDB.addData("mails", Object.assign({}, mail), mail.ref); + mail.content = data.content; + newInbox.mails[mail.ref] = mail; + addMark(mail.ref, "unread"); + break; + } + case "CREATE_GROUP": { //process create group + let groupInfo = JSON.parse(unparsed.message); + let h = ["groupID", "created", "admin"].map(x => groupInfo[x]).join('|') + if (groupInfo.admin === unparsed.senderID && + floCrypto.verifySign(h, groupInfo.hash, groupInfo.pubKey) && + floCrypto.getFloID(groupInfo.pubKey) === groupInfo.groupID) { + let eKey = groupInfo.eKey + groupInfo.eKey = encrypt(eKey) + compactIDB.writeData("groups", Object.assign({}, groupInfo), groupInfo.groupID) + groupInfo.eKey = eKey + _loaded.groups[groupInfo.groupID] = groupInfo + requestGroupInbox(groupInfo.groupID) + newInbox.newgroups.push(groupInfo.groupID) + } + break; + } + case "REVOKE_KEY": { //revoke group key + let r = JSON.parse(unparsed.message); + let groupInfo = _loaded.groups[r.groupID] + if (unparsed.senderID === groupInfo.admin) { + if (typeof expiredKeys[r.groupID] !== "object") + expiredKeys[r.groupID] = {} + expiredKeys[r.groupID][vc] = groupInfo.eKey + let eKey = r.newKey + groupInfo.eKey = encrypt(eKey); + compactIDB.writeData("groups", Object.assign({}, groupInfo), groupInfo.groupID) + groupInfo.eKey = eKey + newInbox.keyrevoke.push(groupInfo.groupID) + } + break; + } + case "CREATE_PIPELINE": { //add pipeline + let pipelineInfo = JSON.parse(unparsed.message); + let eKey = pipelineInfo.eKey; + pipelineInfo.eKey = encrypt(eKey) + compactIDB.addData("pipeline", Object.assign({}, pipelineInfo), pipelineInfo.id); + pipelineInfo.eKey = eKey; + _loaded.pipeline[pipelineInfo.id] = pipelineInfo + requestPipelineInbox(pipelineInfo.id, pipelineInfo.model); + newInbox.pipeline[pipelineInfo.id] = pipelineInfo.model; + } + } + } + } + + function requestDirectInbox() { + if (directConnID.length) { //close existing request connection (if any) + directConnID.forEach(id => floCloudAPI.closeRequest(id)); + directConnID = []; + } + const parseData = processData.direct(); + let callbackFn = function (dataSet, error) { + if (error) + return console.error(error) + let newInbox = { + messages: {}, + requests: {}, + responses: {}, + mails: {}, + newgroups: [], + keyrevoke: [], + pipeline: {} + } + for (let vc in dataSet) { + try { + parseData(dataSet[vc], newInbox); + } catch (error) { + //if (error !== "blocked-user") + console.log(error); + } finally { + if (_loaded.appendix.lastReceived < vc) + _loaded.appendix.lastReceived = vc; + } + } + compactIDB.writeData("appendix", _loaded.appendix.lastReceived, "lastReceived"); + console.debug(newInbox); + UI.direct(newInbox) + } + return new Promise((resolve, reject) => { + const promises = [ + floCloudAPI.requestApplicationData(null, { + receiverID: user.id, + lowerVectorClock: _loaded.appendix.lastReceived + 1, + callback: callbackFn + }), + floCloudAPI.requestApplicationData(null, { + receiverID: floEthereum.ethAddressFromCompressedPublicKey(user.public), + lowerVectorClock: _loaded.appendix.lastReceived + 1, + callback: callbackFn + }) + ] + Promise.all(promises).then(connectionIds => { + directConnID = [...directConnID, ...connectionIds]; + resolve("Direct Inbox connected"); + }).catch(error => reject(error)) + }) + } + + messenger.getMail = function (mailRef) { + return new Promise((resolve, reject) => { + compactIDB.readData("mails", mailRef).then(mail => { + mail.content = decrypt(mail.content) + resolve(mail) + }).catch(error => reject(error)) + }); + } + + const getChatOrder = messenger.getChatOrder = function (separate = false) { + let result; + if (separate) { + result = {}; + result.direct = Object.keys(_loaded.chats).map(a => [_loaded.chats[a], a]) + .sort((a, b) => b[0] - a[0]).map(a => a[1]); + result.group = Object.keys(_loaded.groups).map(a => [parseInt(_loaded.appendix[`lastReceived_${a}`]), a]) + .sort((a, b) => b[0] - a[0]).map(a => a[1]); + result.pipeline = Object.keys(_loaded.pipeline).map(a => [parseInt(_loaded.appendix[`lastReceived_${a}`]), a]) + .sort((a, b) => b[0] - a[0]).map(a => a[1]); + } else { + result = Object.keys(_loaded.chats).map(a => [_loaded.chats[a], a]) + .concat(Object.keys(_loaded.groups).map(a => [parseInt(_loaded.appendix[`lastReceived_${a}`]), a])) + .concat(Object.keys(_loaded.pipeline).map(a => [parseInt(_loaded.appendix[`lastReceived_${a}`]), a])) + .sort((a, b) => b[0] - a[0]).map(a => a[1]) + } + return result; + } + + messenger.storeContact = function (floID, name) { + return floDapps.storeContact(floID, name) + } + + const loadDataFromIDB = function (defaultList = true) { + return new Promise((resolve, reject) => { + if (defaultList) + dataList = ["mails", "marked", "groups", "pipeline", "chats", "blocked", "appendix"] + else + dataList = ["messages", "mails", "marked", "chats", "groups", "gkeys", "pipeline", "blocked", "appendix"] + let promises = [] + for (var i = 0; i < dataList.length; i++) + promises[i] = compactIDB.readAllData(dataList[i]) + Promise.all(promises).then(results => { + let data = {} + for (var i = 0; i < dataList.length; i++) + data[dataList[i]] = results[i] + data.appendix.lastReceived = data.appendix.lastReceived || '0'; + if (data.appendix.AESKey) { + try { + let AESKey = floDapps.user.decrypt(data.appendix.AESKey); + data.appendix.AESKey = AESKey; + if (dataList.includes("messages")) + for (let m in data.messages) + if (data.messages[m].message) + data.messages[m].message = decrypt(data.messages[m].message, AESKey); + if (dataList.includes("mails")) + for (let m in data.mails) + data.mails[m].content = decrypt(data.mails[m].content, AESKey); + if (dataList.includes("groups")) + for (let g in data.groups) + data.groups[g].eKey = decrypt(data.groups[g].eKey, AESKey); + if (dataList.includes("gkeys")) + for (let k in data.gkeys) + data.gkeys[k] = decrypt(data.gkeys[k], AESKey); + if (dataList.includes("pipeline")) + for (let p in data.pipeline) + data.pipeline[p].eKey = decrypt(data.pipeline[p].eKey, AESKey); + resolve(data) + } catch (error) { + console.error(error) + reject("Corrupted AES Key"); + } + } else { + if (Object.keys(data.mails).length) + return reject("AES Key not Found") + let AESKey = floCrypto.randString(32, false); + let encryptedKey = floCrypto.encryptData(AESKey, user.public); + compactIDB.addData("appendix", encryptedKey, "AESKey").then(result => { + data.appendix.AESKey = AESKey; + resolve(data); + }).catch(error => reject("Unable to Generate AES Key")) + } + }).catch(error => reject(error)) + }) + } + + messenger.addMark = function (key, mark) { + if (_loaded.marked.hasOwnProperty(key) && !_loaded.marked[key].includes(mark)) + _loaded.marked[key].push(mark) + return addMark(key, mark) + } + + messenger.removeMark = function (key, mark) { + if (_loaded.marked.hasOwnProperty(key)) + _loaded.marked[key] = _loaded.marked[key].filter(v => v !== mark) + return removeMark(key, mark) + } + + messenger.addChat = function (chatID) { + return new Promise((resolve, reject) => { + compactIDB.addData("chats", 0, chatID) + .then(result => resolve("Added chat")) + .catch(error => reject(error)) + }) + } + + messenger.rmChat = function (chatID) { + return new Promise((resolve, reject) => { + compactIDB.removeData("chats", chatID) + .then(result => resolve("Chat removed")) + .catch(error => reject(error)) + }) + } + + messenger.clearChat = function (chatID) { + return new Promise((resolve, reject) => { + let options = { + lowerKey: `${chatID}|`, + upperKey: `${chatID}||` + } + compactIDB.searchData("messages", options).then(result => { + let promises = [] + for (let i in result) + promises.push(compactIDB.removeData("messages", i)) + Promise.all(promises) + .then(result => resolve("Chat cleared")) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + const getChat = messenger.getChat = function (chatID) { + return new Promise((resolve, reject) => { + let options = { + lowerKey: `${chatID}|`, + upperKey: `${chatID}||` + } + compactIDB.searchData("messages", options).then(result => { + for (let i in result) + if (result[i].message) + result[i].message = decrypt(result[i].message) + resolve(result) + }).catch(error => reject(error)) + }) + } + + messenger.backupData = function () { + return new Promise((resolve, reject) => { + loadDataFromIDB(false).then(data => { + delete data.appendix.AESKey; + data.contacts = floGlobals.contacts; + data.pubKeys = floGlobals.pubKeys; + data = btoa(unescape(encodeURIComponent(JSON.stringify(data)))) + let blobData = { + floID: user.id, + pubKey: user.public, + data: floDapps.user.encipher(data), + } + blobData.sign = floDapps.user.sign(blobData.data); + resolve(new Blob([JSON.stringify(blobData)], { + type: 'application/json' + })); + }).catch(error => reject(error)) + }) + } + + const parseBackup = messenger.parseBackup = function (blob) { + return new Promise((resolve, reject) => { + if (blob instanceof Blob || blob instanceof File) { + let reader = new FileReader(); + reader.onload = evt => { + var blobData = JSON.parse(evt.target.result); + if (!floCrypto.verifySign(blobData.data, blobData.sign, blobData.pubKey)) + reject("Corrupted Backup file: Signature verification failed"); + else if (user.id !== blobData.floID || user.public !== blobData.pubKey) + reject("Invalid Backup file: Incorrect floID"); + else { + try { + let data = floDapps.user.decipher(blobData.data); + try { + data = JSON.parse(decodeURIComponent(escape(atob(data)))); + resolve(data) + } catch (e) { + reject("Corrupted Backup file: Parse failed"); + } + } catch (e) { + reject("Corrupted Backup file: Decryption failed"); + } + } + } + reader.readAsText(blob); + } else + reject("Backup is not a valid File (or) Blob") + }) + } + + messenger.restoreData = function (arg) { + return new Promise((resolve, reject) => { + if (arg instanceof Blob || arg instanceof File) + var parseData = parseBackup + else + var parseData = data => new Promise((res, rej) => res(data)) + parseData(arg).then(data => { + for (let m in data.messages) + if (data.messages[m].message) + data.messages[m].message = encrypt(data.messages[m].message) + for (let m in data.mails) + data.mails[m].content = encrypt(data.mails[m].content) + for (let k in data.gkeys) + data.gkeys[k] = encrypt(data.gkeys[k]) + for (let g in data.groups) + data.groups[g].eKey = encrypt(data.groups[g].eKey) + for (let p in data.pipeline) + data.pipeline[p].eKey = encrypt(data.pipeline[p].eKey) + for (let c in data.chats) + if (data.chats[c] <= _loaded.chats[c]) + delete data.chats[c] + for (let l in data.appendix) + if (l.startsWith('lastReceived') && data.appendix[l] <= _loaded.appendix[l]) + delete data.appendix[l] + for (let c in data.contacts) + if (c in floGlobals.contacts) + delete data.contacts[c] + for (let p in data.pubKeys) + if (p in floGlobals.pubKeys) + delete data.pubKeys[p] + let promises = []; + for (let obs in data) { + let writeFn; + switch (obs) { + case "contacts": + writeFn = (k, v) => floDapps.storeContact(k, v); + break; + case "pubKeys": + writeFn = (k, v) => floDapps.storePubKey(k, v); + break; + default: + writeFn = (k, v) => compactIDB.writeData(obs, v, k); + break; + } + for (let k in data[obs]) + promises.push(writeFn(k, data[obs][k])); + } + + Promise.all(promises) + .then(results => resolve("Restore Successful")) + .catch(error => reject("Restore Failed: Unable to write to IDB")) + }).catch(error => reject(error)) + }) + } + + messenger.clearUserData = function () { + return new Promise((resolve, reject) => { + let user_floID = floCrypto.toFloID(user.id); + let promises = [ + compactIDB.deleteDB(`${floGlobals.application}_${user_floID}`), + compactIDB.removeData('lastTx', `${floGlobals.application}|${user_floID}`, floDapps.root), + floDapps.clearCredentials() + ] + Promise.all(promises) + .then(result => resolve("User Data cleared")) + .catch(error => reject(error)) + }) + } + + //group feature + + messenger.createGroup = function (groupname, description = '') { + return new Promise((resolve, reject) => { + if (!groupname) return reject("Invalid Group Name") + let id = floCrypto.generateNewID(); + let groupInfo = { + groupID: id.floID, + pubKey: id.pubKey, + admin: user.id, + name: groupname, + description: description, + created: Date.now(), + members: [user.id] + } + let h = ["groupID", "created", "admin"].map(x => groupInfo[x]).join('|') + groupInfo.hash = floCrypto.signData(h, id.privKey) + let eKey = floCrypto.randString(16, false) + groupInfo.eKey = encrypt(eKey) + let p1 = compactIDB.addData("groups", groupInfo, id.floID) + let p2 = compactIDB.addData("gkeys", encrypt(id.privKey), id.floID) + Promise.all([p1, p2]).then(r => { + groupInfo.eKey = eKey + _loaded.groups[id.floID] = groupInfo; + requestGroupInbox(id.floID) + resolve(groupInfo) + }).catch(e => reject(e)) + }) + } + + messenger.changeGroupName = function (groupID, name) { + return new Promise((resolve, reject) => { + let groupInfo = _loaded.groups[groupID] + if (user.id !== groupInfo.admin) + return reject("Access denied: Admin only!") + let message = encrypt(name, groupInfo.eKey) + sendRaw(message, groupID, "UP_NAME", false) + .then(result => resolve('Name updated')) + .catch(error => reject(error)) + }) + } + + messenger.changeGroupDescription = function (groupID, description) { + return new Promise((resolve, reject) => { + let groupInfo = _loaded.groups[groupID] + if (user.id !== groupInfo.admin) + return reject("Access denied: Admin only!") + let message = encrypt(description, groupInfo.eKey) + sendRaw(message, groupID, "UP_DESCRIPTION", false) + .then(result => resolve('Description updated')) + .catch(error => reject(error)) + }) + } + + messenger.addGroupMembers = function (groupID, newMem, note = undefined) { + return new Promise((resolve, reject) => { + if (!Array.isArray(newMem) && typeof newMem === "string") + newMem = [newMem] + //check for validity + let imem1 = [], + imem2 = [] + newMem.forEach(m => + !floCrypto.validateAddr(m) ? imem1.push(m) : + m in floGlobals.pubKeys ? null : imem2.push(m) + ); + if (imem1.length) + return reject(`Invalid Members(floIDs): ${imem1}`) + else if (imem2.length) + return reject(`Invalid Members (pubKey not available): ${imem2}`) + //send new newMem list to existing members + let groupInfo = _loaded.groups[groupID] + if (user.id !== groupInfo.admin) + return reject("Access denied: Admin only!") + let k = groupInfo.eKey; + //send groupInfo to new newMem + groupInfo = JSON.stringify(groupInfo) + let promises = newMem.map(m => sendRaw(groupInfo, m, "CREATE_GROUP", true)); + Promise.allSettled(promises).then(results => { + let success = [], + failed = []; + for (let i in results) + if (results[i].status === "fulfilled") + success.push(newMem[i]) + else if (results[i].status === "rejected") + failed.push(newMem[i]) + let message = encrypt(success.join("|"), k) + sendRaw(message, groupID, "ADD_MEMBERS", false, note) + .then(r => resolve(`Members added: ${success}`)) + .catch(e => reject(e)) + }) + }) + } + + messenger.rmGroupMembers = function (groupID, rmMem, note = undefined) { + return new Promise((resolve, reject) => { + if (!Array.isArray(rmMem) && typeof rmMem === "string") + rmMem = [rmMem] + let groupInfo = _loaded.groups[groupID] + let imem = rmMem.filter(m => !groupInfo.members.includes(m)) + if (imem.length) + return reject(`Invalid members: ${imem}`) + if (user.id !== groupInfo.admin) + return reject("Access denied: Admin only!") + let message = encrypt(rmMem.join("|"), groupInfo.eKey) + let p1 = sendRaw(message, groupID, "RM_MEMBERS", false, note) + groupInfo.members = groupInfo.members.filter(m => !rmMem.includes(m)) + let p2 = revokeKey(groupID) + Promise.all([p1, p2]) + .then(r => resolve(`Members removed: ${rmMem}`)) + .catch(e => reject(e)) + }) + } + + const revokeKey = messenger.revokeKey = function (groupID) { + return new Promise((resolve, reject) => { + let groupInfo = _loaded.groups[groupID] + if (user.id !== groupInfo.admin) + return reject("Access denied: Admin only!") + let newKey = floCrypto.randString(16, false); + Promise.all(groupInfo.members.map(m => sendRaw(JSON.stringify({ + newKey, + groupID + }), m, "REVOKE_KEY", true))).then(result => { + resolve("Group key revoked") + }).catch(error => reject(error)) + }) + } + + messenger.sendGroupMessage = function (message, groupID) { + return new Promise((resolve, reject) => { + let k = _loaded.groups[groupID].eKey + message = encrypt(message, k) + sendRaw(message, groupID, "GROUP_MSG", false) + .then(result => resolve(`${groupID}: ${message}`)) + .catch(error => reject(error)) + }) + } + + const disableGroup = messenger.disableGroup = function (groupID) { + return new Promise((resolve, reject) => { + if (!_loaded.groups[groupID]) + return reject("Group not found"); + let groupInfo = Object.assign({}, _loaded.groups[groupID]); + if (groupInfo.disabled) + return resolve("Group already diabled"); + groupInfo.disabled = true; + groupInfo.eKey = encrypt(groupInfo.eKey) + compactIDB.writeData("groups", groupInfo, groupID).then(result => { + floCloudAPI.closeRequest(groupConnID[groupID]); + delete groupConnID[groupID]; + resolve("Group diabled"); + }).catch(error => reject(error)) + }) + } + + processData.group = function (groupID) { + return (unparsed, newInbox) => { + if (!_loaded.groups[groupID].members.includes(unparsed.senderID)) + return; + //store the pubKey if not stored already + floDapps.storePubKey(unparsed.senderID, unparsed.pubKey) + let data = { + time: unparsed.time, + sender: unparsed.senderID, + groupID: unparsed.receiverID + } + let vc = unparsed.vectorClock, + k = _loaded.groups[groupID].eKey; + if (expiredKeys[groupID]) { + var ex = Object.keys(expiredKeys[groupID]).sort() + while (ex.length && vc > ex[0]) ex.shift() + if (ex.length) + k = expiredKeys[groupID][ex.shift()] + } + unparsed.message = decrypt(unparsed.message, k); + var infoChange = false; + if (unparsed.type === "GROUP_MSG") + data.message = encrypt(unparsed.message); + else if (data.sender === _loaded.groups[groupID].admin) { + let groupInfo = _loaded.groups[groupID] + data.admin = true; + switch (unparsed.type) { + case "ADD_MEMBERS": { + data.newMembers = unparsed.message.split("|") + data.note = unparsed.comment + groupInfo.members = Array.from(new Set(groupInfo.members.concat(data.newMembers))) + break; + } + case "UP_DESCRIPTION": { + data.description = unparsed.message; + groupInfo.description = data.description; + break; + } + case "RM_MEMBERS": { + data.rmMembers = unparsed.message.split("|") + data.note = unparsed.comment + groupInfo.members = groupInfo.members.filter(m => !data.rmMembers.includes(m)) + if (data.rmMembers.includes(user.id)) { + disableGroup(groupID); + return; + } + break; + } + case "UP_NAME": { + data.name = unparsed.message + groupInfo.name = data.name; + break; + } + } + infoChange = true; + } + compactIDB.addData("messages", Object.assign({}, data), `${groupID}|${vc}`) + if (data.message) + data.message = decrypt(data.message); + newInbox.messages[vc] = data; + if (!floCrypto.isSameAddr(data.sender, user.id)) + addMark(data.groupID, "unread"); + return infoChange; + } + } + + function requestGroupInbox(groupID, _async = true) { + if (groupConnID[groupID]) { //close existing request connection (if any) + floCloudAPI.closeRequest(groupConnID[groupID]); + delete groupConnID[groupID]; + } + + const parseData = processData.group(groupID); + let callbackFn = function (dataSet, error) { + if (error) + return console.error(error) + console.info(dataSet) + let newInbox = { + messages: {} + } + let infoChange = false; + for (let vc in dataSet) { + if (groupID !== dataSet[vc].receiverID) + continue; + try { + infoChange = parseData(dataSet[vc], newInbox) || infoChange; + if (!_loaded.appendix[`lastReceived_${groupID}`] || + _loaded.appendix[`lastReceived_${groupID}`] < vc) + _loaded.appendix[`lastReceived_${groupID}`] = vc; + } catch (error) { + console.log(error) + } + } + compactIDB.writeData("appendix", _loaded.appendix[`lastReceived_${groupID}`], `lastReceived_${groupID}`); + if (infoChange) { + let newInfo = Object.assign({}, _loaded.groups[groupID]); + newInfo.eKey = encrypt(newInfo.eKey) + compactIDB.writeData("groups", newInfo, groupID) + } + console.debug(newInbox); + UI.group(newInbox); + } + let fn = floCloudAPI.requestApplicationData(null, { + receiverID: groupID, + lowerVectorClock: _loaded.appendix[`lastReceived_${groupID}`] + 1, + callback: callbackFn + }); + if (_async) { + fn.then(conn_id => groupConnID[groupID] = conn_id) + .catch(error => console.error(`request-group(${groupID}):`, error)) + } else { + return new Promise((resolve, reject) => { + fn.then(conn_id => { + groupConnID[groupID] = conn_id; + resolve(`Connected to group ${groupID}`); + }).catch(error => reject(error)) + }); + } + } + + //messenger startups + messenger.init = function () { + return new Promise((resolve, reject) => { + initUserDB().then(result => { + console.debug(result); + loadDataFromIDB().then(data => { + console.debug(data); + //load data to memory + _loaded.appendix = data.appendix; + _loaded.groups = data.groups; + _loaded.pipeline = data.pipeline; + _loaded.chats = data.chats; + _loaded.marked = data.marked; + _loaded.blocked = new Set(Object.keys(data.blocked)); + //call UI render functions + UI.chats(getChatOrder()); + UI.mails(data.mails); + UI.marked(data.marked); + resolve('Loaded local data') + //request data from cloud + let promises = []; + promises.push(requestDirectInbox()); + for (let g in data.groups) + if (data.groups[g].disabled !== true) + promises.push(requestGroupInbox(g, false)); + for (let p in data.pipeline) + if (data.pipeline[p].disabled !== true) + promises.push(requestPipelineInbox(p, data.pipeline[p].model, false)); + loadDataFromBlockchain().then(result => { + Promise.all(promises) + .then(result => resolve("Messenger initiated")) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)); + }) + }) + } + + const loadDataFromBlockchain = messenger.loadDataFromBlockchain = function () { + return new Promise((resolve, reject) => { + let user_floID = floCrypto.toFloID(user.id); + if (!user_floID) + return reject("Not an valid address"); + let last_key = `${floGlobals.application}|${user_floID}`; + compactIDB.readData("lastTx", last_key, floDapps.root).then(lastTx => { + var query_options = { pattern: floGlobals.application, tx: true }; + if (typeof lastTx == 'number') //lastTx is tx count (*backward support) + query_options.ignoreOld = lastTx; + else if (typeof lastTx == 'string') //lastTx is txid of last tx + query_options.after = lastTx; + floBlockchainAPI.readData(user_floID, query_options).then(result => { + for (var i = result.items.length - 1; i >= 0; i--) { + let tx = result.items[i], + content = JSON.parse(tx.data)[floGlobals.application]; + if (!(content instanceof Object)) + continue; + let key = (content.type ? content.type + "|" : "") + tx.txid.substr(0, 16); + compactIDB.writeData("flodata", { + time: tx.time, + txid: tx.txid, + data: content + }, key); + } + compactIDB.writeData("lastTx", result.lastItem, last_key, floDapps.root); + resolve(true); + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + //BTC multisig application + const MultiSig = messenger.multisig = {} + const TYPE_BTC_MULTISIG = "btc_multisig", //used for both pipeline and multisig address creation + TYPE_FLO_MULTISIG = "flo_multisig"; //only used for pipeline + + MultiSig.createAddress = function (pubKeys, minRequired) { + return new Promise(async (resolve, reject) => { + let co_owners = pubKeys.map(p => floCrypto.getFloID(p)); + if (co_owners.includes(null)) + return reject("Invalid public key: " + pubKeys[co_owners.indexOf(null)]); + let privateKey = await floDapps.user.private; + let multisig = btcOperator.multiSigAddress(pubKeys, minRequired) //TODO: change to correct function + if (typeof multisig !== 'object') + return reject("Unable to create multisig address"); + let content = { + type: TYPE_BTC_MULTISIG, + address: multisig.address, //TODO: maybe encrypt the address + redeemScript: multisig.redeemScript + }; + console.debug(content.address, content.redeemScript); + debugger; + floBlockchainAPI.writeDataMultiple([privateKey], JSON.stringify({ + [floGlobals.application]: content + }), co_owners).then(txid => { + console.info(txid); + let key = TYPE_BTC_MULTISIG + "|" + txid.substr(0, 16); + compactIDB.writeData("flodata", { + time: null, //time will be overwritten when confirmed on blockchain + txid: txid, + data: content + }, key); + resolve(multisig.address); + }).catch(error => reject(error)) + }) + } + + MultiSig.listAddress = function () { + return new Promise((resolve, reject) => { + let options = { + lowerKey: `${TYPE_BTC_MULTISIG}|`, + upperKey: `${TYPE_BTC_MULTISIG}||` + } + compactIDB.searchData("flodata", options).then(result => { + let multsigs = {}; + for (let i in result) { + let addr = result[i].data.address, + addr_type = btcOperator.validateAddress(addr); + let decode = (addr_type == "multisig" ? + coinjs.script().decodeRedeemScript : coinjs.script().decodeRedeemScriptBech32) + (result[i].data.redeemScript); + if (addr_type != "multisig" && addr_type != "multisigBech32") + console.warn("Invalid multi-sig address:", addr); + else if (!decode || decode.address !== addr) + console.warn("Invalid redeem-script:", addr); + else if (decode.type !== "multisig__") + console.warn("Redeem-script is not of a multisig:", addr); + else if (!decode.pubkeys.includes(user.public.toLowerCase()) && !decode.pubkeys.includes(user.public.toUpperCase())) + console.warn("User is not a part of this multisig:", addr); + else if (decode.pubkeys.length < decode.signaturesRequired) + console.warn("Invalid multisig (required is greater than users):", addr); + else + multsigs[addr] = { + redeemScript: decode.redeemscript, + pubKeys: decode.pubkeys, + minRequired: decode.signaturesRequired, + time: result[i].time, + txid: result[i].txid + } + } + resolve(multsigs); + }).catch(error => reject(error)) + }) + } + + //create multisig tx for BTC + MultiSig.createTx_BTC = function (address, redeemScript, receivers, amounts, fee = null, options = {}) { + return new Promise(async (resolve, reject) => { + let addr_type = btcOperator.validateAddress(address); + if (addr_type != "multisig" && addr_type != "multisigBech32") + return reject("Sender address is not a multisig"); + let decode = (addr_type == "multisig" ? + coinjs.script().decodeRedeemScript : coinjs.script().decodeRedeemScriptBech32)(redeemScript); + if (!decode || decode.address !== address || decode.type !== "multisig__") + return reject("Invalid redeem-script"); + else if (!decode.pubkeys.includes(user.public.toLowerCase()) && !decode.pubkeys.includes(user.public.toUpperCase())) + return reject("User is not a part of this multisig"); + else if (decode.pubkeys.length < decode.signaturesRequired) + return reject("Invalid multisig (required is greater than users)"); + let co_owners = decode.pubkeys.map(p => floCrypto.getFloID(p)); + let privateKey = await floDapps.user.private; + btcOperator.createMultiSigTx(address, redeemScript, receivers, amounts, fee, options).then(({ tx_hex }) => { + tx_hex = btcOperator.signTx(tx_hex, privateKey); + createPipeline(TYPE_BTC_MULTISIG, co_owners, 32, decode.pubkeys).then(pipeline => { + let message = encrypt(tx_hex, pipeline.eKey); + sendRaw(message, pipeline.id, "TRANSACTION", false) + .then(result => resolve(pipeline.id)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + //sign multisig tx for BTC + MultiSig.signTx_BTC = function (pipeID) { + return new Promise((resolve, reject) => { + if (_loaded.pipeline[pipeID].model !== TYPE_BTC_MULTISIG) + return reject('Incorrect pipeline model. Only works for BTC-multisig'); + if (_loaded.pipeline[pipeID].disabled) + return reject("Pipeline is already closed"); + getChat(pipeID).then(async result => { + let pipeline = _loaded.pipeline[pipeID], + tx_hex_latest = Object.keys(result).sort().map(i => result[i].tx_hex).filter(x => x).pop(); + let privateKey = await floDapps.user.private; + let tx_hex_signed = btcOperator.signTx(tx_hex_latest, privateKey); + let message = encrypt(tx_hex_signed, pipeline.eKey); + sendRaw(message, pipeline.id, "TRANSACTION", false).then(result => { + if (!btcOperator.checkSigned(tx_hex_signed)) + return resolve({ + tx_hex: tx_hex_signed + }); + debugger; + btcOperator.broadcastTx(tx_hex_signed).then(txid => { + console.debug(txid); + sendRaw(encrypt(txid, pipeline.eKey), pipeline.id, "BROADCAST", false) + .then(result => resolve({ + tx_hex: tx_hex_signed, + txid: txid + })).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => console.error(error)) + }) + } + + //create multisig tx for FLO + MultiSig.createTx_FLO = function (address, redeemScript, receivers, amounts, floData = '', options = {}) { + return new Promise(async (resolve, reject) => { + if (!floCrypto.validateFloID(address)) { //not a flo multisig, but maybe btc multisig address + let addr_type = btcOperator.validateAddress(address); + if (addr_type != "multisig" && addr_type != "multisigBech32") + return reject("Sender address is not a multisig"); + address = floCrypto.toMultisigFloID(address); + } + let decode = floCrypto.decodeRedeemScript(redeemScript); + if (!decode || decode.address !== address) + return reject("Invalid redeem-script"); + else if (!decode.pubkeys.includes(user.public.toLowerCase()) && !decode.pubkeys.includes(user.public.toUpperCase())) + return reject("User is not a part of this multisig"); + else if (decode.pubkeys.length < decode.required) + return reject("Invalid multisig (required is greater than users)"); + let co_owners = decode.pubkeys.map(p => floCrypto.getFloID(p)); + let privateKey = await floDapps.user.private; + floBlockchainAPI.createMultisigTx(redeemScript, receivers, amounts, floData).then(tx_hex => { + tx_hex = floBlockchainAPI.signTx(tx_hex, privateKey); + createPipeline(TYPE_FLO_MULTISIG, co_owners, 32, decode.pubkeys).then(pipeline => { + let message = encrypt(tx_hex, pipeline.eKey); + sendRaw(message, pipeline.id, "TRANSACTION", false) + .then(result => resolve(pipeline.id)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + //sign multisig tx for FLO + MultiSig.signTx_FLO = function (pipeID) { + return new Promise((resolve, reject) => { + if (_loaded.pipeline[pipeID].model !== TYPE_FLO_MULTISIG) + return reject('Incorrect pipeline model. Only works for FLO-multisig'); + if (_loaded.pipeline[pipeID].disabled) + return reject("Pipeline is already closed"); + getChat(pipeID).then(async result => { + let pipeline = _loaded.pipeline[pipeID], + tx_hex_latest = Object.keys(result).sort().map(i => result[i].tx_hex).filter(x => x).pop(); + let privateKey = await floDapps.user.private; + let tx_hex_signed = floBlockchainAPI.signTx(tx_hex_latest, privateKey); + let message = encrypt(tx_hex_signed, pipeline.eKey); + sendRaw(message, pipeline.id, "TRANSACTION", false).then(result => { + if (!floBlockchainAPI.checkSigned(tx_hex_signed)) + return resolve({ tx_hex: tx_hex_signed }); + debugger; + floBlockchainAPI.broadcastTx(tx_hex_signed).then(txid => { + console.debug(txid); + sendRaw(encrypt(txid, pipeline.eKey), pipeline.id, "BROADCAST", false) + .then(result => resolve({ + tx_hex: tx_hex_signed, + txid: txid + })).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => console.error(error)) + }) + } + + //Pipelines + const createPipeline = messenger.createPipeline = function (model, members, ekeySize = 16, pubkeys = null) { + return new Promise((resolve, reject) => { + //optional pubkey parameter + if (pubkeys !== null) { + if (!Array.isArray(pubkeys)) + return reject('pubkeys must be an array (if passed)'); + else if (pubkeys.length !== members.length) + return reject('pubkey length doesnot match members length'); + } + + //validate members + let imem1 = [], + imem2 = [] + members.forEach((m, i) => { + if (!floCrypto.validateAddr(m)) + imem1.push(m); + else if (!(m in floGlobals.pubKeys) && !floCrypto.isSameAddr(user.id, m)) { + if (pubkeys !== null && floCrypto.verifyPubKey(pubkeys[i], m)) + floGlobals.pubKeys[m] = pubkeys[i]; + else + imem2.push(m); + } + }); + if (imem1.length) + return reject(`Invalid Members(floIDs): ${imem1}`); + else if (imem2.length) + return reject(`Invalid Members (pubKey not available): ${imem2}`); + //create pipeline info + const id = floCrypto.tmpID; + let pipeline = { + id, + model, + members + } + if (ekeySize) + pipeline.eKey = floCrypto.randString(ekeySize); + //send pipeline info to members + let pipelineInfo = JSON.stringify(pipeline); + let promises = members.filter(m => !floCrypto.isSameAddr(m, user.id)).map(m => sendRaw(pipelineInfo, m, "CREATE_PIPELINE", true)); + Promise.allSettled(promises).then(results => { + console.debug(results.filter(r => r.status === "rejected").map(r => r.reason)); + _loaded.pipeline[pipeline.id] = Object.assign({}, pipeline); + if (pipeline.eKey) + pipeline.eKey = encrypt(pipeline.eKey); + compactIDB.addData("pipeline", pipeline, pipeline.id).then(result => { + requestPipelineInbox(pipeline.id, pipeline.model); + resolve(_loaded.pipeline[pipeline.id]) + }).catch(error => reject(error)) + }) + }) + } + + function requestPipelineInbox(pipeID, model, _async = true) { + if (pipeConnID[pipeID]) { //close existing request connection (if any) + floCloudAPI.closeRequest(pipeConnID[pipeID]); + delete pipeConnID[pipeID]; + } + + let parseData = processData.pipeline[model](pipeID); + let callbackFn = function (dataSet, error) { + if (error) + return console.error(error); + console.info(dataSet) + let newInbox = { + messages: {} + } + for (let vc in dataSet) { + if (pipeID !== dataSet[vc].receiverID) + continue; + try { + parseData(dataSet[vc], newInbox); + if (!floCrypto.isSameAddr(dataSet[vc].senderID, user.id)) + addMark(pipeID, "unread") + if (!_loaded.appendix[`lastReceived_${pipeID}`] || + _loaded.appendix[`lastReceived_${pipeID}`] < vc) + _loaded.appendix[`lastReceived_${pipeID}`] = vc; + } catch (error) { + console.log(error) + } + } + compactIDB.writeData("appendix", _loaded.appendix[`lastReceived_${pipeID}`], `lastReceived_${pipeID}`); + console.debug(newInbox); + UI.pipeline(model, newInbox); + } + + let fn = floCloudAPI.requestApplicationData(null, { + receiverID: pipeID, + lowerVectorClock: _loaded.appendix[`lastReceived_${pipeID}`] + 1, + callback: callbackFn + }); + if (_async) { + fn.then(conn_id => pipeConnID[pipeID] = conn_id) + .catch(error => console.error(`request-pipeline(${pipeID}):`, error)) + } else { + return new Promise((resolve, reject) => { + fn.then(conn_id => { + pipeConnID[pipeID] = conn_id; + resolve(`Connected to pipeline ${pipeID}`); + }).catch(error => reject(error)) + }); + } + } + + const disablePipeline = messenger.disablePipeline = function (pipeID) { + console.debug(JSON.stringify(pipeConnID), pipeConnID[pipeID]) + return new Promise((resolve, reject) => { + if (!_loaded.pipeline[pipeID]) + return reject("Pipeline not found"); + if (_loaded.pipeline[pipeID].disabled) + return resolve("Pipeline already diabled"); + _loaded.pipeline[pipeID].disabled = true; + let pipelineInfo = Object.assign({}, _loaded.pipeline[pipeID]); + pipelineInfo.eKey = encrypt(pipelineInfo.eKey) + compactIDB.writeData("pipeline", pipelineInfo, pipeID).then(result => { + floCloudAPI.closeRequest(pipeConnID[pipeID]); + delete pipeConnID[pipeID]; + resolve("Pipeline diabled"); + }).catch(error => reject(error)) + }) + } + + messenger.sendPipelineMessage = function (message, pipeID) { + return new Promise((resolve, reject) => { + let k = _loaded.pipeline[pipeID].eKey; + if (k) message = encrypt(message, k); + sendRaw(message, pipeID, "MESSAGE", false) + .then(result => resolve(`${pipeID}: ${message}`)) + .catch(error => reject(error)) + }) + } + + messenger.editFee = function (tx_id, new_fee, private_keys, change_only = true) { + return new Promise(async (resolve, reject) => { + //1. FIND REDEEMSCRIPT + //2. CHANGE OUTPUT VALUES + //3. Call modified version of MultiSig.createTx_BTC_1 where the input taken is txhex rather than senders etc + //4. MultiSig.createTx_BTC_1 will in turn call btcOperator.createMultiSigTx_1(tx_hex). Check that Redeemscript information is present + var address; + + if (!Array.isArray(private_keys)) + private_keys = [private_keys]; + try { + let tx, tx_parsed; + tx = await btcOperator.tx_fetch_for_editing(tx_id) + tx_parsed = await btcOperator.parseTransaction(tx) + if (tx_parsed.fee >= new_fee) + return reject("Fees can only be increased"); + + //editable addresses in output values (for fee increase) + var edit_output_address = new Set(); + if (change_only === true) //allow only change values (ie, sender address) to be edited to inc fee + tx_parsed.inputs.forEach(inp => edit_output_address.add(inp.address)); + else if (change_only === false) //allow all output values to be edited + tx_parsed.outputs.forEach(out => edit_output_address.add(out.address)); + else if (typeof change_only == 'string') // allow only given receiver id output to be edited + edit_output_address.add(change_only); + else if (Array.isArray(change_only)) //allow only given set of receiver id outputs to be edited + change_only.forEach(id => edit_output_address.add(id)); + + //edit output values to increase fee + let inc_fee = btcOperator.util.BTC_to_Sat(new_fee - tx_parsed.fee); + if (inc_fee < MIN_FEE_UPDATE) + return reject(`Insufficient additional fee. Minimum increment: ${MIN_FEE_UPDATE}`); + for (let i = tx.outs.length - 1; i >= 0 && inc_fee > 0; i--) //reduce in reverse order + if (edit_output_address.has(tx_parsed.outputs[i].address)) { + let current_value = tx.outs[i].value; + if (current_value instanceof BigInteger) //convert BigInteger class to inv value + current_value = current_value.intValue(); + //edit the value as required + if (current_value > inc_fee) { + tx.outs[i].value = current_value - inc_fee; + inc_fee = 0; + } else { + inc_fee -= current_value; + tx.outs[i].value = 0; + } + } + if (inc_fee > 0) { + let max_possible_fee = btcOperator.util.BTC_to_Sat(new_fee) - inc_fee; //in satoshi + return reject(`Insufficient output values to increase fee. Maximum fee possible: ${btcOperator.util.Sat_to_BTC(max_possible_fee)}`); + } + tx.outs = tx.outs.filter(o => o.value >= DUST_AMT); //remove all output with value less than DUST amount + + //remove existing signatures and reset the scripts + let wif_keys = []; + let witness_position = 0; + for (let i in tx.ins) { + var addr = tx_parsed.inputs[i].address, + value = btcOperator.util.BTC_to_Sat(tx_parsed.inputs[i].value); + let addr_decode = coinjs.addressDecode(addr); + //find the correct key for addr + var privKey = private_keys.find(pk => verifyKey(addr, pk)); + if (!privKey) + return reject(`Private key missing for ${addr}`); + //find redeemScript (if any) + const rs = _redeemScript(addr, privKey); + rs === false ? wif_keys.unshift(privKey) : wif_keys.push(privKey); //sorting private-keys (wif) + //reset the script for re-signing + var script; + if (!rs || !rs.length) { + //legacy script (derive from address) + let s = coinjs.script(); + s.writeOp(118); //OP_DUP + s.writeOp(169); //OP_HASH160 + s.writeBytes(addr_decode.bytes); + s.writeOp(136); //OP_EQUALVERIFY + s.writeOp(172); //OP_CHECKSIG + script = Crypto.util.bytesToHex(s.buffer); + } else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi))) { + //redeemScript for segwit/bech32 + if (addr_decode == "bech32") { witness_position = witness_position + 1; } //bech32 has witness + let s = coinjs.script(); + s.writeBytes(Crypto.util.hexToBytes(rs)); + s.writeOp(0); + s.writeBytes(coinjs.numToBytes(value.toFixed(0), 8)); + script = Crypto.util.bytesToHex(s.buffer); + } else if (addr_decode.type === 'multisigBech32') { + //redeemScript multisig (bech32) + address = addr; + var rs_array = []; + rs_array = btcOperator.extractLastHexStrings(tx.witness); + let redeemScript = rs_array[witness_position]; + witness_position = witness_position + 1; //this permits mixing witness and non witness based inputs + + let s = coinjs.script(); + s.writeBytes(Crypto.util.hexToBytes(redeemScript)); + s.writeOp(0); + s.writeBytes(coinjs.numToBytes(value.toFixed(0), 8)); + script = Crypto.util.bytesToHex(s.buffer); + } else //redeemScript for multisig (segwit) + script = rs; + tx.ins[i].script = coinjs.script(script); + } + tx.witness = false; //remove all witness signatures + console.debug("Unsigned:", tx.serialize()); + //re-sign the transaction + new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF + let tx_hex = tx.serialize(); + + //Call MultiSig.createTx_BTC_editFee(tx.serialize()); + let addr_type = btcOperator.validateAddress(address); + if (addr_type != "multisig" && addr_type != "multisigBech32") + return reject("Sender address is not a multisig"); + let decode = (addr_type == "multisig" ? + coinjs.script().decodeRedeemScript : coinjs.script().decodeRedeemScriptBech32)(redeemScript); + if (!decode || decode.address !== address || decode.type !== "multisig__") + return reject("Invalid redeem-script"); + else if (!decode.pubkeys.includes(user.public.toLowerCase()) && !decode.pubkeys.includes(user.public.toUpperCase())) + return reject("User is not a part of this multisig"); + else if (decode.pubkeys.length < decode.signaturesRequired) + return reject("Invalid multisig (required is greater than users)"); + let co_owners = decode.pubkeys.map(p => floCrypto.getFloID(p)); + //let privateKey = await floDapps.user.private; + + // let tx_hex = btcOperator.signTx_1(tx, privateKey); + + createPipeline(TYPE_BTC_MULTISIG, co_owners, 32, decode.pubkeys).then(pipeline => { + let message = encrypt(tx_hex, pipeline.eKey); + sendRaw(message, pipeline.id, "TRANSACTION", false) + .then(result => resolve(pipeline.id)) + .catch(error => reject(error)) //SENDRAW + }).catch(error => reject(error)) //CREATE PIPELINE + // resolve(tx.serialize()); //CHECK THIS -- NOT NEEDED + } catch (error) { + reject(error); + } + }) + } + + + processData.pipeline = {}; + + //pipeline model for btc multisig + processData.pipeline[TYPE_BTC_MULTISIG] = function (pipeID) { + return (unparsed, newInbox) => { + if (!_loaded.pipeline[pipeID].members.includes(floCrypto.toFloID(unparsed.senderID))) + return; + let data = { + time: unparsed.time, + sender: unparsed.senderID, + pipeID: unparsed.receiverID + } + let vc = unparsed.vectorClock, + k = _loaded.pipeline[pipeID].eKey; + unparsed.message = decrypt(unparsed.message, k) + //store the pubKey if not stored already + floDapps.storePubKey(unparsed.senderID, unparsed.pubKey); + data.type = unparsed.type; + switch (unparsed.type) { + case "TRANSACTION": { + data.tx_hex = unparsed.message; + break; + } + case "BROADCAST": { + data.txid = unparsed.message; + //the following check is done on parallel (in background) instead of sync + btcOperator.getTx.hex(data.txid).then(tx_hex_final => { + getChat(pipeID).then(result => { + let tx_hex_inital = Object.keys(result).sort().map(i => result[i].tx_hex).filter(x => x).shift(); + if (btcOperator.checkIfSameTx(tx_hex_inital, tx_hex_final)) + disablePipeline(pipeID); + }).catch(error => console.error(error)) + }).catch(error => console.error(error)) + break; + } + case "MESSAGE": { + data.message = encrypt(unparsed.message); + break; + } + } + compactIDB.addData("messages", Object.assign({}, data), `${pipeID}|${vc}`); + if (data.message) + data.message = decrypt(data.message); + newInbox.messages[vc] = data; + } + } + + //pipeline model for flo multisig + processData.pipeline[TYPE_FLO_MULTISIG] = function (pipeID) { + return (unparsed, newInbox) => { + if (!_loaded.pipeline[pipeID].members.includes(floCrypto.toFloID(unparsed.senderID))) + return; + let data = { + time: unparsed.time, + sender: unparsed.senderID, + pipeID: unparsed.receiverID + } + let vc = unparsed.vectorClock, + k = _loaded.pipeline[pipeID].eKey; + unparsed.message = decrypt(unparsed.message, k) + //store the pubKey if not stored already + floDapps.storePubKey(unparsed.senderID, unparsed.pubKey); + data.type = unparsed.type; + switch (unparsed.type) { + case "TRANSACTION": { + data.tx_hex = unparsed.message; + break; + } + case "BROADCAST": { + data.txid = unparsed.message; + //the following check is done on parallel (in background) instead of sync + getChat(pipeID).then(result => { + var tx_hex_list = Object.keys(result).sort().map(i => result[i].tx_hex).filter(x => x); + let tx_hex_inital = tx_hex_list[0], + tx_hex_final = tx_hex_list.pop(); + if (floBlockchainAPI.checkIfSameTx(tx_hex_inital, tx_hex_final) && + floBlockchainAPI.transactionID(tx_hex_final) == data.txid) //compare the txHex and txid + disablePipeline(pipeID); + }).catch(error => console.error(error)) + break; + } + case "MESSAGE": { + data.message = encrypt(unparsed.message); + break; + } + } + compactIDB.addData("messages", Object.assign({}, data), `${pipeID}|${vc}`); + if (data.message) + data.message = decrypt(data.message); + newInbox.messages[vc] = data; + } + } + +})(); diff --git a/messenger/scripts/messenger.min.js b/messenger/scripts/messenger.min.js new file mode 100644 index 0000000..f3c9739 --- /dev/null +++ b/messenger/scripts/messenger.min.js @@ -0,0 +1 @@ +!function(){const messenger=window.messenger={},user={get id(){return floDapps.user.id},get public(){return floDapps.user.public}},expiredKeys={},UI={group:(d,e)=>console.log(d,e),pipeline:(d,e)=>console.log(d,e),direct:(d,e)=>console.log(d,e),chats:c=>console.log(c),mails:m=>console.log(m),marked:r=>console.log(r)};messenger.renderUI={},Object.defineProperties(messenger.renderUI,{chats:{set:ui_fn=>UI.chats=ui_fn},directChat:{set:ui_fn=>UI.direct=ui_fn},groupChat:{set:ui_fn=>UI.group=ui_fn},pipeline:{set:ui_fn=>UI.pipeline=ui_fn},mails:{set:ui_fn=>UI.mails=ui_fn},marked:{set:ui_fn=>UI.marked=ui_fn}});const _loaded={};Object.defineProperties(messenger,{chats:{get:()=>_loaded.chats},groups:{get:()=>_loaded.groups},pipeline:{get:()=>_loaded.pipeline},blocked:{get:()=>_loaded.blocked},marked:{get:()=>_loaded.marked}});var directConnID=[],groupConnID={},pipeConnID={};function sendRaw(message,recipient,type,encrypt=null,comment=void 0){return new Promise(((resolve,reject)=>{if(!floCrypto.validateAddr(recipient))return reject("Invalid Recipient");if([!0,null].includes(encrypt)){let r_pubKey=floDapps.user.get_pubKey(recipient);if(r_pubKey)message=floCrypto.encryptData(message,r_pubKey);else if(!0===encrypt)return reject("recipient's pubKey not found")}let options={receiverID:recipient};comment&&(options.comment=comment),floCloudAPI.sendApplicationData(message,type,options).then((result=>resolve(result))).catch((error=>reject(error)))}))}function encrypt(value,key=_loaded.appendix.AESKey){return Crypto.AES.encrypt(value,key)}function decrypt(value,key=_loaded.appendix.AESKey){return Crypto.AES.decrypt(value,key)}function addMark(key,mark){return new Promise(((resolve,reject)=>{compactIDB.readData("marked",key).then((result=>{if(result){if(result.includes(mark))return resolve("Mark already exist");result.push(mark)}else result=[mark];compactIDB.writeData("marked",result,key).then((result=>resolve(result))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))}messenger.conn={},Object.defineProperties(messenger.conn,{direct:{get:()=>directConnID},group:{get:()=>Object.assign({},groupConnID)}}),messenger.sendRaw=sendRaw,messenger.encrypt=encrypt;function listRequests(obs,options=null){return new Promise(((resolve,reject)=>{compactIDB.readAllData(obs).then((result=>{if(!options||"object"!=typeof options)return resolve(result);let filtered={};for(let k in result){let val=result[k];options.type&&options.type==val.type||(options.floID&&options.floID==val.floID||void 0!==options.completed&&options.completed==!val.completed||(filtered[k]=val))}resolve(filtered)})).catch((error=>reject(error)))}))}messenger.blockUser=function(floID){return new Promise(((resolve,reject)=>{if(_loaded.blocked.has(floID))return resolve("User is already blocked");compactIDB.addData("blocked",!0,floID).then((result=>{_loaded.blocked.add(floID),resolve("Blocked User: "+floID)})).catch((error=>reject(error)))}))},messenger.unblockUser=function(floID){return new Promise(((resolve,reject)=>{if(!_loaded.blocked.has(floID))return resolve("User is not blocked");compactIDB.removeData("blocked",floID).then((result=>{_loaded.blocked.delete(floID),resolve("Unblocked User: "+floID)})).catch((error=>reject(error)))}))},messenger.sendMessage=function(message,receiver){return new Promise(((resolve,reject)=>{sendRaw(message,receiver,"MESSAGE").then((result=>{let vc=result.vectorClock,data={floID:receiver,time:result.time,category:"sent",message:encrypt(message)};_loaded.chats[receiver]=parseInt(vc),compactIDB.writeData("chats",parseInt(vc),receiver),compactIDB.addData("messages",Object.assign({},data),`${receiver}|${vc}`),data.message=message,resolve({[vc]:data})})).catch((error=>reject(error)))}))},messenger.sendMail=function(subject,content,recipients,prev=null){return new Promise(((resolve,reject)=>{Array.isArray(recipients)||(recipients=[recipients]);let mail={subject:subject,content:content,ref:Date.now()+floCrypto.randString(8,!0),prev:prev},promises=recipients.map((r=>sendRaw(JSON.stringify(mail),r,"MAIL")));Promise.allSettled(promises).then((results=>{if(mail.time=Date.now(),mail.from=user.id,mail.to=[],results.forEach((r=>{"fulfilled"===r.status&&mail.to.push(r.value.receiverID)})),0===mail.to.length)return reject(results);mail.content=encrypt(content),compactIDB.addData("mails",Object.assign({},mail),mail.ref),mail.content=content,resolve({[mail.ref]:mail})}))}))},messenger.list_request_sent=(options=null)=>listRequests("request_sent",options),messenger.list_request_received=(options=null)=>listRequests("request_received",options),messenger.list_response_sent=(options=null)=>listRequests("response_sent",options),messenger.list_response_received=(options=null)=>listRequests("response_received",options),messenger.request_pubKey=(receiver,message="")=>function(receiver,type,message,encrypt=null){return new Promise(((resolve,reject)=>{sendRaw(message,receiver,"REQUEST",encrypt,type).then((result=>{let vc=result.vectorClock,data={floID:receiver,time:result.time,message:message,type:type};compactIDB.addData("request_sent",data,vc),resolve({[vc]:data})})).catch((error=>reject(error)))}))}(receiver,"PUBLIC_KEY",message,!1),messenger.respond_pubKey=(req_id,message="")=>function(req_id,message,encrypt=null){return new Promise(((resolve,reject)=>{compactIDB.readData("request_received",req_id).then((request=>{sendRaw(JSON.stringify({value:message,reqID:req_id}),request.floID,"RESPONSE",encrypt,request.type).then((result=>{let vc=result.vectorClock,data={floID:request.floID,time:result.time,message:message,type:request.type,reqID:req_id};compactIDB.addData("response_sent",data,vc),request.completed=vc,compactIDB.writeData("request_received",request,req_id),resolve({[vc]:data})})).catch((error=>reject(error)))})).catch((error=>reject(error)))}))}(req_id,message,!1);const processData={};processData.direct=function(){return(unparsed,newInbox)=>{if(floDapps.storePubKey(unparsed.senderID,unparsed.pubKey),_loaded.blocked.has(unparsed.senderID)&&"REVOKE_KEY"!==unparsed.type)throw"blocked-user";unparsed.message instanceof Object&&"secret"in unparsed.message&&(unparsed.message=floDapps.user.decrypt(unparsed.message));let vc=unparsed.vectorClock;switch(unparsed.type){case"MESSAGE":{let dm={time:unparsed.time,floID:unparsed.senderID,category:"received",message:encrypt(unparsed.message)};console.debug(dm,`${dm.floID}|${vc}`),compactIDB.addData("messages",Object.assign({},dm),`${dm.floID}|${vc}`),_loaded.chats[dm.floID]=parseInt(vc),compactIDB.writeData("chats",parseInt(vc),dm.floID),dm.message=unparsed.message,newInbox.messages[vc]=dm,addMark(dm.floID,"unread");break}case"REQUEST":{let req={floID:unparsed.senderID,time:unparsed.time,message:unparsed.message,type:unparsed.comment};compactIDB.addData("request_received",req,vc),newInbox.requests[vc]=req;break}case"RESPONSE":{let data=JSON.parse(unparsed.message),res={floID:unparsed.senderID,time:unparsed.time,message:data.value,type:unparsed.comment,reqID:data.reqID};compactIDB.addData("response_received",res,vc),compactIDB.readData("request_sent",data.reqID).then((req=>{req.completed=vc,compactIDB.writeData("request_sent",req,data.reqID)})),newInbox.responses[vc]=res;break}case"MAIL":{let data=JSON.parse(unparsed.message),mail={time:unparsed.time,from:unparsed.senderID,to:[unparsed.receiverID],subject:data.subject,content:encrypt(data.content),ref:data.ref,prev:data.prev};compactIDB.addData("mails",Object.assign({},mail),mail.ref),mail.content=data.content,newInbox.mails[mail.ref]=mail,addMark(mail.ref,"unread");break}case"CREATE_GROUP":{let groupInfo=JSON.parse(unparsed.message),h=["groupID","created","admin"].map((x=>groupInfo[x])).join("|");if(groupInfo.admin===unparsed.senderID&&floCrypto.verifySign(h,groupInfo.hash,groupInfo.pubKey)&&floCrypto.getFloID(groupInfo.pubKey)===groupInfo.groupID){let eKey=groupInfo.eKey;groupInfo.eKey=encrypt(eKey),compactIDB.writeData("groups",Object.assign({},groupInfo),groupInfo.groupID),groupInfo.eKey=eKey,_loaded.groups[groupInfo.groupID]=groupInfo,requestGroupInbox(groupInfo.groupID),newInbox.newgroups.push(groupInfo.groupID)}break}case"REVOKE_KEY":{let r=JSON.parse(unparsed.message),groupInfo=_loaded.groups[r.groupID];if(unparsed.senderID===groupInfo.admin){"object"!=typeof expiredKeys[r.groupID]&&(expiredKeys[r.groupID]={}),expiredKeys[r.groupID][vc]=groupInfo.eKey;let eKey=r.newKey;groupInfo.eKey=encrypt(eKey),compactIDB.writeData("groups",Object.assign({},groupInfo),groupInfo.groupID),groupInfo.eKey=eKey,newInbox.keyrevoke.push(groupInfo.groupID)}break}case"CREATE_PIPELINE":{let pipelineInfo=JSON.parse(unparsed.message),eKey=pipelineInfo.eKey;pipelineInfo.eKey=encrypt(eKey),compactIDB.addData("pipeline",Object.assign({},pipelineInfo),pipelineInfo.id),pipelineInfo.eKey=eKey,_loaded.pipeline[pipelineInfo.id]=pipelineInfo,requestPipelineInbox(pipelineInfo.id,pipelineInfo.model),newInbox.pipeline[pipelineInfo.id]=pipelineInfo.model}}}},messenger.getMail=function(mailRef){return new Promise(((resolve,reject)=>{compactIDB.readData("mails",mailRef).then((mail=>{mail.content=decrypt(mail.content),resolve(mail)})).catch((error=>reject(error)))}))};const getChatOrder=messenger.getChatOrder=function(separate=!1){let result;return separate?(result={},result.direct=Object.keys(_loaded.chats).map((a=>[_loaded.chats[a],a])).sort(((a,b)=>b[0]-a[0])).map((a=>a[1])),result.group=Object.keys(_loaded.groups).map((a=>[parseInt(_loaded.appendix[`lastReceived_${a}`]),a])).sort(((a,b)=>b[0]-a[0])).map((a=>a[1])),result.pipeline=Object.keys(_loaded.pipeline).map((a=>[parseInt(_loaded.appendix[`lastReceived_${a}`]),a])).sort(((a,b)=>b[0]-a[0])).map((a=>a[1]))):result=Object.keys(_loaded.chats).map((a=>[_loaded.chats[a],a])).concat(Object.keys(_loaded.groups).map((a=>[parseInt(_loaded.appendix[`lastReceived_${a}`]),a]))).concat(Object.keys(_loaded.pipeline).map((a=>[parseInt(_loaded.appendix[`lastReceived_${a}`]),a]))).sort(((a,b)=>b[0]-a[0])).map((a=>a[1])),result};messenger.storeContact=function(floID,name){return floDapps.storeContact(floID,name)};const loadDataFromIDB=function(defaultList=!0){return new Promise(((resolve,reject)=>{dataList=defaultList?["mails","marked","groups","pipeline","chats","blocked","appendix"]:["messages","mails","marked","chats","groups","gkeys","pipeline","blocked","appendix"];let promises=[];for(var i=0;i{let data={};for(var i=0;i{data.appendix.AESKey=AESKey,resolve(data)})).catch((error=>reject("Unable to Generate AES Key")))}})).catch((error=>reject(error)))}))};messenger.addMark=function(key,mark){return _loaded.marked.hasOwnProperty(key)&&!_loaded.marked[key].includes(mark)&&_loaded.marked[key].push(mark),addMark(key,mark)},messenger.removeMark=function(key,mark){return _loaded.marked.hasOwnProperty(key)&&(_loaded.marked[key]=_loaded.marked[key].filter((v=>v!==mark))),function(key,mark){return new Promise(((resolve,reject)=>{compactIDB.readData("marked",key).then((result=>{if(!result||!result.includes(mark))return resolve("Mark doesnot exist");result.splice(result.indexOf(mark),1),compactIDB.writeData("marked",result,key).then((result=>resolve("Mark removed"))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))}(key,mark)},messenger.addChat=function(chatID){return new Promise(((resolve,reject)=>{compactIDB.addData("chats",0,chatID).then((result=>resolve("Added chat"))).catch((error=>reject(error)))}))},messenger.rmChat=function(chatID){return new Promise(((resolve,reject)=>{compactIDB.removeData("chats",chatID).then((result=>resolve("Chat removed"))).catch((error=>reject(error)))}))},messenger.clearChat=function(chatID){return new Promise(((resolve,reject)=>{let options={lowerKey:`${chatID}|`,upperKey:`${chatID}||`};compactIDB.searchData("messages",options).then((result=>{let promises=[];for(let i in result)promises.push(compactIDB.removeData("messages",i));Promise.all(promises).then((result=>resolve("Chat cleared"))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))};const getChat=messenger.getChat=function(chatID){return new Promise(((resolve,reject)=>{let options={lowerKey:`${chatID}|`,upperKey:`${chatID}||`};compactIDB.searchData("messages",options).then((result=>{for(let i in result)result[i].message&&(result[i].message=decrypt(result[i].message));resolve(result)})).catch((error=>reject(error)))}))};messenger.backupData=function(){return new Promise(((resolve,reject)=>{loadDataFromIDB(!1).then((data=>{delete data.appendix.AESKey,data.contacts=floGlobals.contacts,data.pubKeys=floGlobals.pubKeys,data=btoa(unescape(encodeURIComponent(JSON.stringify(data))));let blobData={floID:user.id,pubKey:user.public,data:floDapps.user.encipher(data)};blobData.sign=floDapps.user.sign(blobData.data),resolve(new Blob([JSON.stringify(blobData)],{type:"application/json"}))})).catch((error=>reject(error)))}))};const parseBackup=messenger.parseBackup=function(blob){return new Promise(((resolve,reject)=>{if(blob instanceof Blob||blob instanceof File){let reader=new FileReader;reader.onload=evt=>{var blobData=JSON.parse(evt.target.result);if(floCrypto.verifySign(blobData.data,blobData.sign,blobData.pubKey))if(user.id!==blobData.floID||user.public!==blobData.pubKey)reject("Invalid Backup file: Incorrect floID");else try{let data=floDapps.user.decipher(blobData.data);try{data=JSON.parse(decodeURIComponent(escape(atob(data)))),resolve(data)}catch(e){reject("Corrupted Backup file: Parse failed")}}catch(e){reject("Corrupted Backup file: Decryption failed")}else reject("Corrupted Backup file: Signature verification failed")},reader.readAsText(blob)}else reject("Backup is not a valid File (or) Blob")}))};messenger.restoreData=function(arg){return new Promise(((resolve,reject)=>{if(arg instanceof Blob||arg instanceof File)var parseData=parseBackup;else parseData=data=>new Promise(((res,rej)=>res(data)));parseData(arg).then((data=>{for(let m in data.messages)data.messages[m].message&&(data.messages[m].message=encrypt(data.messages[m].message));for(let m in data.mails)data.mails[m].content=encrypt(data.mails[m].content);for(let k in data.gkeys)data.gkeys[k]=encrypt(data.gkeys[k]);for(let g in data.groups)data.groups[g].eKey=encrypt(data.groups[g].eKey);for(let p in data.pipeline)data.pipeline[p].eKey=encrypt(data.pipeline[p].eKey);for(let c in data.chats)data.chats[c]<=_loaded.chats[c]&&delete data.chats[c];for(let l in data.appendix)l.startsWith("lastReceived")&&data.appendix[l]<=_loaded.appendix[l]&&delete data.appendix[l];for(let c in data.contacts)c in floGlobals.contacts&&delete data.contacts[c];for(let p in data.pubKeys)p in floGlobals.pubKeys&&delete data.pubKeys[p];let promises=[];for(let obs in data){let writeFn;switch(obs){case"contacts":writeFn=(k,v)=>floDapps.storeContact(k,v);break;case"pubKeys":writeFn=(k,v)=>floDapps.storePubKey(k,v);break;default:writeFn=(k,v)=>compactIDB.writeData(obs,v,k)}for(let k in data[obs])promises.push(writeFn(k,data[obs][k]))}Promise.all(promises).then((results=>resolve("Restore Successful"))).catch((error=>reject("Restore Failed: Unable to write to IDB")))})).catch((error=>reject(error)))}))},messenger.clearUserData=function(){return new Promise(((resolve,reject)=>{let user_floID=floCrypto.toFloID(user.id),promises=[compactIDB.deleteDB(`${floGlobals.application}_${user_floID}`),compactIDB.removeData("lastTx",`${floGlobals.application}|${user_floID}`,floDapps.root),floDapps.clearCredentials()];Promise.all(promises).then((result=>resolve("User Data cleared"))).catch((error=>reject(error)))}))},messenger.createGroup=function(groupname,description=""){return new Promise(((resolve,reject)=>{if(!groupname)return reject("Invalid Group Name");let id=floCrypto.generateNewID(),groupInfo={groupID:id.floID,pubKey:id.pubKey,admin:user.id,name:groupname,description:description,created:Date.now(),members:[user.id]},h=["groupID","created","admin"].map((x=>groupInfo[x])).join("|");groupInfo.hash=floCrypto.signData(h,id.privKey);let eKey=floCrypto.randString(16,!1);groupInfo.eKey=encrypt(eKey);let p1=compactIDB.addData("groups",groupInfo,id.floID),p2=compactIDB.addData("gkeys",encrypt(id.privKey),id.floID);Promise.all([p1,p2]).then((r=>{groupInfo.eKey=eKey,_loaded.groups[id.floID]=groupInfo,requestGroupInbox(id.floID),resolve(groupInfo)})).catch((e=>reject(e)))}))},messenger.changeGroupName=function(groupID,name){return new Promise(((resolve,reject)=>{let groupInfo=_loaded.groups[groupID];if(user.id!==groupInfo.admin)return reject("Access denied: Admin only!");sendRaw(encrypt(name,groupInfo.eKey),groupID,"UP_NAME",!1).then((result=>resolve("Name updated"))).catch((error=>reject(error)))}))},messenger.changeGroupDescription=function(groupID,description){return new Promise(((resolve,reject)=>{let groupInfo=_loaded.groups[groupID];if(user.id!==groupInfo.admin)return reject("Access denied: Admin only!");sendRaw(encrypt(description,groupInfo.eKey),groupID,"UP_DESCRIPTION",!1).then((result=>resolve("Description updated"))).catch((error=>reject(error)))}))},messenger.addGroupMembers=function(groupID,newMem,note=void 0){return new Promise(((resolve,reject)=>{Array.isArray(newMem)||"string"!=typeof newMem||(newMem=[newMem]);let imem1=[],imem2=[];if(newMem.forEach((m=>floCrypto.validateAddr(m)?m in floGlobals.pubKeys?null:imem2.push(m):imem1.push(m))),imem1.length)return reject(`Invalid Members(floIDs): ${imem1}`);if(imem2.length)return reject(`Invalid Members (pubKey not available): ${imem2}`);let groupInfo=_loaded.groups[groupID];if(user.id!==groupInfo.admin)return reject("Access denied: Admin only!");let k=groupInfo.eKey;groupInfo=JSON.stringify(groupInfo);let promises=newMem.map((m=>sendRaw(groupInfo,m,"CREATE_GROUP",!0)));Promise.allSettled(promises).then((results=>{let success=[],failed=[];for(let i in results)"fulfilled"===results[i].status?success.push(newMem[i]):"rejected"===results[i].status&&failed.push(newMem[i]);sendRaw(encrypt(success.join("|"),k),groupID,"ADD_MEMBERS",!1,note).then((r=>resolve(`Members added: ${success}`))).catch((e=>reject(e)))}))}))},messenger.rmGroupMembers=function(groupID,rmMem,note=void 0){return new Promise(((resolve,reject)=>{Array.isArray(rmMem)||"string"!=typeof rmMem||(rmMem=[rmMem]);let groupInfo=_loaded.groups[groupID],imem=rmMem.filter((m=>!groupInfo.members.includes(m)));if(imem.length)return reject(`Invalid members: ${imem}`);if(user.id!==groupInfo.admin)return reject("Access denied: Admin only!");let p1=sendRaw(encrypt(rmMem.join("|"),groupInfo.eKey),groupID,"RM_MEMBERS",!1,note);groupInfo.members=groupInfo.members.filter((m=>!rmMem.includes(m)));let p2=revokeKey(groupID);Promise.all([p1,p2]).then((r=>resolve(`Members removed: ${rmMem}`))).catch((e=>reject(e)))}))};const revokeKey=messenger.revokeKey=function(groupID){return new Promise(((resolve,reject)=>{let groupInfo=_loaded.groups[groupID];if(user.id!==groupInfo.admin)return reject("Access denied: Admin only!");let newKey=floCrypto.randString(16,!1);Promise.all(groupInfo.members.map((m=>sendRaw(JSON.stringify({newKey:newKey,groupID:groupID}),m,"REVOKE_KEY",!0)))).then((result=>{resolve("Group key revoked")})).catch((error=>reject(error)))}))};messenger.sendGroupMessage=function(message,groupID){return new Promise(((resolve,reject)=>{let k=_loaded.groups[groupID].eKey;sendRaw(message=encrypt(message,k),groupID,"GROUP_MSG",!1).then((result=>resolve(`${groupID}: ${message}`))).catch((error=>reject(error)))}))};const disableGroup=messenger.disableGroup=function(groupID){return new Promise(((resolve,reject)=>{if(!_loaded.groups[groupID])return reject("Group not found");let groupInfo=Object.assign({},_loaded.groups[groupID]);if(groupInfo.disabled)return resolve("Group already diabled");groupInfo.disabled=!0,groupInfo.eKey=encrypt(groupInfo.eKey),compactIDB.writeData("groups",groupInfo,groupID).then((result=>{floCloudAPI.closeRequest(groupConnID[groupID]),delete groupConnID[groupID],resolve("Group diabled")})).catch((error=>reject(error)))}))};function requestGroupInbox(groupID,_async=!0){groupConnID[groupID]&&(floCloudAPI.closeRequest(groupConnID[groupID]),delete groupConnID[groupID]);const parseData=processData.group(groupID);let fn=floCloudAPI.requestApplicationData(null,{receiverID:groupID,lowerVectorClock:_loaded.appendix[`lastReceived_${groupID}`]+1,callback:function(dataSet,error){if(error)return console.error(error);console.info(dataSet);let newInbox={messages:{}},infoChange=!1;for(let vc in dataSet)if(groupID===dataSet[vc].receiverID)try{infoChange=parseData(dataSet[vc],newInbox)||infoChange,(!_loaded.appendix[`lastReceived_${groupID}`]||_loaded.appendix[`lastReceived_${groupID}`]{fn.then((conn_id=>{groupConnID[groupID]=conn_id,resolve(`Connected to group ${groupID}`)})).catch((error=>reject(error)))}));fn.then((conn_id=>groupConnID[groupID]=conn_id)).catch((error=>console.error(`request-group(${groupID}):`,error)))}processData.group=function(groupID){return(unparsed,newInbox)=>{if(!_loaded.groups[groupID].members.includes(unparsed.senderID))return;floDapps.storePubKey(unparsed.senderID,unparsed.pubKey);let data={time:unparsed.time,sender:unparsed.senderID,groupID:unparsed.receiverID},vc=unparsed.vectorClock,k=_loaded.groups[groupID].eKey;if(expiredKeys[groupID]){for(var ex=Object.keys(expiredKeys[groupID]).sort();ex.length&&vc>ex[0];)ex.shift();ex.length&&(k=expiredKeys[groupID][ex.shift()])}unparsed.message=decrypt(unparsed.message,k);var infoChange=!1;if("GROUP_MSG"===unparsed.type)data.message=encrypt(unparsed.message);else if(data.sender===_loaded.groups[groupID].admin){let groupInfo=_loaded.groups[groupID];switch(data.admin=!0,unparsed.type){case"ADD_MEMBERS":data.newMembers=unparsed.message.split("|"),data.note=unparsed.comment,groupInfo.members=Array.from(new Set(groupInfo.members.concat(data.newMembers)));break;case"UP_DESCRIPTION":data.description=unparsed.message,groupInfo.description=data.description;break;case"RM_MEMBERS":if(data.rmMembers=unparsed.message.split("|"),data.note=unparsed.comment,groupInfo.members=groupInfo.members.filter((m=>!data.rmMembers.includes(m))),data.rmMembers.includes(user.id))return void disableGroup(groupID);break;case"UP_NAME":data.name=unparsed.message,groupInfo.name=data.name}infoChange=!0}return compactIDB.addData("messages",Object.assign({},data),`${groupID}|${vc}`),data.message&&(data.message=decrypt(data.message)),newInbox.messages[vc]=data,floCrypto.isSameAddr(data.sender,user.id)||addMark(data.groupID,"unread"),infoChange}},messenger.init=function(){return new Promise(((resolve,reject)=>{new Promise(((resolve,reject)=>{let user_db=`${floGlobals.application}_${floCrypto.toFloID(user.id)}`;compactIDB.initDB(user_db,{messages:{},mails:{},marked:{},chats:{},groups:{},gkeys:{},blocked:{},pipeline:{},request_sent:{},request_received:{},response_sent:{},response_received:{},flodata:{},appendix:{},userSettings:{},multisigLabels:{}}).then((result=>{console.info(result),compactIDB.setDefaultDB(user_db),resolve("Messenger UserDB Initated Successfully")})).catch((error=>reject(error)))})).then((result=>{console.debug(result),loadDataFromIDB().then((data=>{console.debug(data),_loaded.appendix=data.appendix,_loaded.groups=data.groups,_loaded.pipeline=data.pipeline,_loaded.chats=data.chats,_loaded.marked=data.marked,_loaded.blocked=new Set(Object.keys(data.blocked)),UI.chats(getChatOrder()),UI.mails(data.mails),UI.marked(data.marked),resolve("Loaded local data");let promises=[];promises.push(function(){directConnID.length&&(directConnID.forEach((id=>floCloudAPI.closeRequest(id))),directConnID=[]);const parseData=processData.direct();let callbackFn=function(dataSet,error){if(error)return console.error(error);let newInbox={messages:{},requests:{},responses:{},mails:{},newgroups:[],keyrevoke:[],pipeline:{}};for(let vc in dataSet)try{parseData(dataSet[vc],newInbox)}catch(error){console.log(error)}finally{_loaded.appendix.lastReceived{const promises=[floCloudAPI.requestApplicationData(null,{receiverID:user.id,lowerVectorClock:_loaded.appendix.lastReceived+1,callback:callbackFn}),floCloudAPI.requestApplicationData(null,{receiverID:floEthereum.ethAddressFromCompressedPublicKey(user.public),lowerVectorClock:_loaded.appendix.lastReceived+1,callback:callbackFn})];Promise.all(promises).then((connectionIds=>{directConnID=[...directConnID,...connectionIds],resolve("Direct Inbox connected")})).catch((error=>reject(error)))}))}());for(let g in data.groups)!0!==data.groups[g].disabled&&promises.push(requestGroupInbox(g,!1));for(let p in data.pipeline)!0!==data.pipeline[p].disabled&&promises.push(requestPipelineInbox(p,data.pipeline[p].model,!1));loadDataFromBlockchain().then((result=>{Promise.all(promises).then((result=>resolve("Messenger initiated"))).catch((error=>reject(error)))})).catch((error=>reject(error)))})).catch((error=>reject(error)))}))}))};const loadDataFromBlockchain=messenger.loadDataFromBlockchain=function(){return new Promise(((resolve,reject)=>{let user_floID=floCrypto.toFloID(user.id);if(!user_floID)return reject("Not an valid address");let last_key=`${floGlobals.application}|${user_floID}`;compactIDB.readData("lastTx",last_key,floDapps.root).then((lastTx=>{var query_options={pattern:floGlobals.application,tx:!0};"number"==typeof lastTx?query_options.ignoreOld=lastTx:"string"==typeof lastTx&&(query_options.after=lastTx),floBlockchainAPI.readData(user_floID,query_options).then((result=>{for(var i=result.items.length-1;i>=0;i--){let tx=result.items[i],content=JSON.parse(tx.data)[floGlobals.application];if(!(content instanceof Object))continue;let key=(content.type?content.type+"|":"")+tx.txid.substr(0,16);compactIDB.writeData("flodata",{time:tx.time,txid:tx.txid,data:content},key)}compactIDB.writeData("lastTx",result.lastItem,last_key,floDapps.root),resolve(!0)})).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},MultiSig=messenger.multisig={};MultiSig.createAddress=function(pubKeys,minRequired){return new Promise((async(resolve,reject)=>{let co_owners=pubKeys.map((p=>floCrypto.getFloID(p)));if(co_owners.includes(null))return reject("Invalid public key: "+pubKeys[co_owners.indexOf(null)]);let privateKey=await floDapps.user.private,multisig=btcOperator.multiSigAddress(pubKeys,minRequired);if("object"!=typeof multisig)return reject("Unable to create multisig address");let content={type:"btc_multisig",address:multisig.address,redeemScript:multisig.redeemScript};console.debug(content.address,content.redeemScript),floBlockchainAPI.writeDataMultiple([privateKey],JSON.stringify({[floGlobals.application]:content}),co_owners).then((txid=>{console.info(txid);let key="btc_multisig|"+txid.substr(0,16);compactIDB.writeData("flodata",{time:null,txid:txid,data:content},key),resolve(multisig.address)})).catch((error=>reject(error)))}))},MultiSig.listAddress=function(){return new Promise(((resolve,reject)=>{let options={lowerKey:"btc_multisig|",upperKey:"btc_multisig||"};compactIDB.searchData("flodata",options).then((result=>{let multsigs={};for(let i in result){let addr=result[i].data.address,addr_type=btcOperator.validateAddress(addr),decode=("multisig"==addr_type?coinjs.script().decodeRedeemScript:coinjs.script().decodeRedeemScriptBech32)(result[i].data.redeemScript);"multisig"!=addr_type&&"multisigBech32"!=addr_type?console.warn("Invalid multi-sig address:",addr):decode&&decode.address===addr?"multisig__"!==decode.type?console.warn("Redeem-script is not of a multisig:",addr):decode.pubkeys.includes(user.public.toLowerCase())||decode.pubkeys.includes(user.public.toUpperCase())?decode.pubkeys.lengthreject(error)))}))},MultiSig.createTx_BTC=function(address,redeemScript,receivers,amounts,fee=null,options={}){return new Promise((async(resolve,reject)=>{let addr_type=btcOperator.validateAddress(address);if("multisig"!=addr_type&&"multisigBech32"!=addr_type)return reject("Sender address is not a multisig");let decode=("multisig"==addr_type?coinjs.script().decodeRedeemScript:coinjs.script().decodeRedeemScriptBech32)(redeemScript);if(!decode||decode.address!==address||"multisig__"!==decode.type)return reject("Invalid redeem-script");if(!decode.pubkeys.includes(user.public.toLowerCase())&&!decode.pubkeys.includes(user.public.toUpperCase()))return reject("User is not a part of this multisig");if(decode.pubkeys.lengthfloCrypto.getFloID(p))),privateKey=await floDapps.user.private;btcOperator.createMultiSigTx(address,redeemScript,receivers,amounts,fee,options).then((({tx_hex:tx_hex})=>{tx_hex=btcOperator.signTx(tx_hex,privateKey),createPipeline("btc_multisig",co_owners,32,decode.pubkeys).then((pipeline=>{sendRaw(encrypt(tx_hex,pipeline.eKey),pipeline.id,"TRANSACTION",!1).then((result=>resolve(pipeline.id))).catch((error=>reject(error)))})).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},MultiSig.signTx_BTC=function(pipeID){return new Promise(((resolve,reject)=>"btc_multisig"!==_loaded.pipeline[pipeID].model?reject("Incorrect pipeline model. Only works for BTC-multisig"):_loaded.pipeline[pipeID].disabled?reject("Pipeline is already closed"):void getChat(pipeID).then((async result=>{let pipeline=_loaded.pipeline[pipeID],tx_hex_latest=Object.keys(result).sort().map((i=>result[i].tx_hex)).filter((x=>x)).pop(),privateKey=await floDapps.user.private,tx_hex_signed=btcOperator.signTx(tx_hex_latest,privateKey);sendRaw(encrypt(tx_hex_signed,pipeline.eKey),pipeline.id,"TRANSACTION",!1).then((result=>{if(!btcOperator.checkSigned(tx_hex_signed))return resolve({tx_hex:tx_hex_signed});btcOperator.broadcastTx(tx_hex_signed).then((txid=>{console.debug(txid),sendRaw(encrypt(txid,pipeline.eKey),pipeline.id,"BROADCAST",!1).then((result=>resolve({tx_hex:tx_hex_signed,txid:txid}))).catch((error=>reject(error)))})).catch((error=>reject(error)))})).catch((error=>reject(error)))})).catch((error=>console.error(error)))))},MultiSig.createTx_FLO=function(address,redeemScript,receivers,amounts,floData="",options={}){return new Promise((async(resolve,reject)=>{if(!floCrypto.validateFloID(address)){let addr_type=btcOperator.validateAddress(address);if("multisig"!=addr_type&&"multisigBech32"!=addr_type)return reject("Sender address is not a multisig");address=floCrypto.toMultisigFloID(address)}let decode=floCrypto.decodeRedeemScript(redeemScript);if(!decode||decode.address!==address)return reject("Invalid redeem-script");if(!decode.pubkeys.includes(user.public.toLowerCase())&&!decode.pubkeys.includes(user.public.toUpperCase()))return reject("User is not a part of this multisig");if(decode.pubkeys.lengthfloCrypto.getFloID(p))),privateKey=await floDapps.user.private;floBlockchainAPI.createMultisigTx(redeemScript,receivers,amounts,floData).then((tx_hex=>{tx_hex=floBlockchainAPI.signTx(tx_hex,privateKey),createPipeline("flo_multisig",co_owners,32,decode.pubkeys).then((pipeline=>{sendRaw(encrypt(tx_hex,pipeline.eKey),pipeline.id,"TRANSACTION",!1).then((result=>resolve(pipeline.id))).catch((error=>reject(error)))})).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},MultiSig.signTx_FLO=function(pipeID){return new Promise(((resolve,reject)=>"flo_multisig"!==_loaded.pipeline[pipeID].model?reject("Incorrect pipeline model. Only works for FLO-multisig"):_loaded.pipeline[pipeID].disabled?reject("Pipeline is already closed"):void getChat(pipeID).then((async result=>{let pipeline=_loaded.pipeline[pipeID],tx_hex_latest=Object.keys(result).sort().map((i=>result[i].tx_hex)).filter((x=>x)).pop(),privateKey=await floDapps.user.private,tx_hex_signed=floBlockchainAPI.signTx(tx_hex_latest,privateKey);sendRaw(encrypt(tx_hex_signed,pipeline.eKey),pipeline.id,"TRANSACTION",!1).then((result=>{if(!floBlockchainAPI.checkSigned(tx_hex_signed))return resolve({tx_hex:tx_hex_signed});floBlockchainAPI.broadcastTx(tx_hex_signed).then((txid=>{console.debug(txid),sendRaw(encrypt(txid,pipeline.eKey),pipeline.id,"BROADCAST",!1).then((result=>resolve({tx_hex:tx_hex_signed,txid:txid}))).catch((error=>reject(error)))})).catch((error=>reject(error)))})).catch((error=>reject(error)))})).catch((error=>console.error(error)))))};const createPipeline=messenger.createPipeline=function(model,members,ekeySize=16,pubkeys=null){return new Promise(((resolve,reject)=>{if(null!==pubkeys){if(!Array.isArray(pubkeys))return reject("pubkeys must be an array (if passed)");if(pubkeys.length!==members.length)return reject("pubkey length doesnot match members length")}let imem1=[],imem2=[];if(members.forEach(((m,i)=>{floCrypto.validateAddr(m)?m in floGlobals.pubKeys||floCrypto.isSameAddr(user.id,m)||(null!==pubkeys&&floCrypto.verifyPubKey(pubkeys[i],m)?floGlobals.pubKeys[m]=pubkeys[i]:imem2.push(m)):imem1.push(m)})),imem1.length)return reject(`Invalid Members(floIDs): ${imem1}`);if(imem2.length)return reject(`Invalid Members (pubKey not available): ${imem2}`);let pipeline={id:floCrypto.tmpID,model:model,members:members};ekeySize&&(pipeline.eKey=floCrypto.randString(ekeySize));let pipelineInfo=JSON.stringify(pipeline),promises=members.filter((m=>!floCrypto.isSameAddr(m,user.id))).map((m=>sendRaw(pipelineInfo,m,"CREATE_PIPELINE",!0)));Promise.allSettled(promises).then((results=>{console.debug(results.filter((r=>"rejected"===r.status)).map((r=>r.reason))),_loaded.pipeline[pipeline.id]=Object.assign({},pipeline),pipeline.eKey&&(pipeline.eKey=encrypt(pipeline.eKey)),compactIDB.addData("pipeline",pipeline,pipeline.id).then((result=>{requestPipelineInbox(pipeline.id,pipeline.model),resolve(_loaded.pipeline[pipeline.id])})).catch((error=>reject(error)))}))}))};function requestPipelineInbox(pipeID,model,_async=!0){pipeConnID[pipeID]&&(floCloudAPI.closeRequest(pipeConnID[pipeID]),delete pipeConnID[pipeID]);let parseData=processData.pipeline[model](pipeID),fn=floCloudAPI.requestApplicationData(null,{receiverID:pipeID,lowerVectorClock:_loaded.appendix[`lastReceived_${pipeID}`]+1,callback:function(dataSet,error){if(error)return console.error(error);console.info(dataSet);let newInbox={messages:{}};for(let vc in dataSet)if(pipeID===dataSet[vc].receiverID)try{parseData(dataSet[vc],newInbox),floCrypto.isSameAddr(dataSet[vc].senderID,user.id)||addMark(pipeID,"unread"),(!_loaded.appendix[`lastReceived_${pipeID}`]||_loaded.appendix[`lastReceived_${pipeID}`]{fn.then((conn_id=>{pipeConnID[pipeID]=conn_id,resolve(`Connected to pipeline ${pipeID}`)})).catch((error=>reject(error)))}));fn.then((conn_id=>pipeConnID[pipeID]=conn_id)).catch((error=>console.error(`request-pipeline(${pipeID}):`,error)))}const disablePipeline=messenger.disablePipeline=function(pipeID){return console.debug(JSON.stringify(pipeConnID),pipeConnID[pipeID]),new Promise(((resolve,reject)=>{if(!_loaded.pipeline[pipeID])return reject("Pipeline not found");if(_loaded.pipeline[pipeID].disabled)return resolve("Pipeline already diabled");_loaded.pipeline[pipeID].disabled=!0;let pipelineInfo=Object.assign({},_loaded.pipeline[pipeID]);pipelineInfo.eKey=encrypt(pipelineInfo.eKey),compactIDB.writeData("pipeline",pipelineInfo,pipeID).then((result=>{floCloudAPI.closeRequest(pipeConnID[pipeID]),delete pipeConnID[pipeID],resolve("Pipeline diabled")})).catch((error=>reject(error)))}))};messenger.sendPipelineMessage=function(message,pipeID){return new Promise(((resolve,reject)=>{let k=_loaded.pipeline[pipeID].eKey;k&&(message=encrypt(message,k)),sendRaw(message,pipeID,"MESSAGE",!1).then((result=>resolve(`${pipeID}: ${message}`))).catch((error=>reject(error)))}))},messenger.editFee=function(tx_id,new_fee,private_keys,change_only=!0){return new Promise((async(resolve,reject)=>{var address;Array.isArray(private_keys)||(private_keys=[private_keys]);try{let tx,tx_parsed;if(tx=await btcOperator.tx_fetch_for_editing(tx_id),tx_parsed=await btcOperator.parseTransaction(tx),tx_parsed.fee>=new_fee)return reject("Fees can only be increased");var edit_output_address=new Set;!0===change_only?tx_parsed.inputs.forEach((inp=>edit_output_address.add(inp.address))):!1===change_only?tx_parsed.outputs.forEach((out=>edit_output_address.add(out.address))):"string"==typeof change_only?edit_output_address.add(change_only):Array.isArray(change_only)&&change_only.forEach((id=>edit_output_address.add(id)));let inc_fee=btcOperator.util.BTC_to_Sat(new_fee-tx_parsed.fee);if(inc_fee=0&&inc_fee>0;i--)if(edit_output_address.has(tx_parsed.outputs[i].address)){let current_value=tx.outs[i].value;current_value instanceof BigInteger&&(current_value=current_value.intValue()),current_value>inc_fee?(tx.outs[i].value=current_value-inc_fee,inc_fee=0):(inc_fee-=current_value,tx.outs[i].value=0)}if(inc_fee>0){let max_possible_fee=btcOperator.util.BTC_to_Sat(new_fee)-inc_fee;return reject(`Insufficient output values to increase fee. Maximum fee possible: ${btcOperator.util.Sat_to_BTC(max_possible_fee)}`)}tx.outs=tx.outs.filter((o=>o.value>=DUST_AMT));let wif_keys=[],witness_position=0;for(let i in tx.ins){var addr=tx_parsed.inputs[i].address,value=btcOperator.util.BTC_to_Sat(tx_parsed.inputs[i].value);let addr_decode=coinjs.addressDecode(addr);var privKey=private_keys.find((pk=>verifyKey(addr,pk)));if(!privKey)return reject(`Private key missing for ${addr}`);const rs=_redeemScript(addr,privKey);var script;if(!1===rs?wif_keys.unshift(privKey):wif_keys.push(privKey),rs&&rs.length)if(rs.match(/^00/)&&44==rs.length||40==rs.length&&rs.match(/^[a-f0-9]+$/gi)){"bech32"==addr_decode&&(witness_position+=1);let s=coinjs.script();s.writeBytes(Crypto.util.hexToBytes(rs)),s.writeOp(0),s.writeBytes(coinjs.numToBytes(value.toFixed(0),8)),script=Crypto.util.bytesToHex(s.buffer)}else if("multisigBech32"===addr_decode.type){address=addr;let redeemScript=btcOperator.extractLastHexStrings(tx.witness)[witness_position];witness_position+=1;let s=coinjs.script();s.writeBytes(Crypto.util.hexToBytes(redeemScript)),s.writeOp(0),s.writeBytes(coinjs.numToBytes(value.toFixed(0),8)),script=Crypto.util.bytesToHex(s.buffer)}else script=rs;else{let s=coinjs.script();s.writeOp(118),s.writeOp(169),s.writeBytes(addr_decode.bytes),s.writeOp(136),s.writeOp(172),script=Crypto.util.bytesToHex(s.buffer)}tx.ins[i].script=coinjs.script(script)}tx.witness=!1,console.debug("Unsigned:",tx.serialize()),new Set(wif_keys).forEach((key=>tx.sign(key,1)));let tx_hex=tx.serialize(),addr_type=btcOperator.validateAddress(address);if("multisig"!=addr_type&&"multisigBech32"!=addr_type)return reject("Sender address is not a multisig");let decode=("multisig"==addr_type?coinjs.script().decodeRedeemScript:coinjs.script().decodeRedeemScriptBech32)(redeemScript);if(!decode||decode.address!==address||"multisig__"!==decode.type)return reject("Invalid redeem-script");if(!decode.pubkeys.includes(user.public.toLowerCase())&&!decode.pubkeys.includes(user.public.toUpperCase()))return reject("User is not a part of this multisig");if(decode.pubkeys.lengthfloCrypto.getFloID(p)));createPipeline("btc_multisig",co_owners,32,decode.pubkeys).then((pipeline=>{sendRaw(encrypt(tx_hex,pipeline.eKey),pipeline.id,"TRANSACTION",!1).then((result=>resolve(pipeline.id))).catch((error=>reject(error)))})).catch((error=>reject(error)))}catch(error){reject(error)}}))},processData.pipeline={},processData.pipeline.btc_multisig=function(pipeID){return(unparsed,newInbox)=>{if(!_loaded.pipeline[pipeID].members.includes(floCrypto.toFloID(unparsed.senderID)))return;let data={time:unparsed.time,sender:unparsed.senderID,pipeID:unparsed.receiverID},vc=unparsed.vectorClock,k=_loaded.pipeline[pipeID].eKey;switch(unparsed.message=decrypt(unparsed.message,k),floDapps.storePubKey(unparsed.senderID,unparsed.pubKey),data.type=unparsed.type,unparsed.type){case"TRANSACTION":data.tx_hex=unparsed.message;break;case"BROADCAST":data.txid=unparsed.message,btcOperator.getTx.hex(data.txid).then((tx_hex_final=>{getChat(pipeID).then((result=>{let tx_hex_inital=Object.keys(result).sort().map((i=>result[i].tx_hex)).filter((x=>x)).shift();btcOperator.checkIfSameTx(tx_hex_inital,tx_hex_final)&&disablePipeline(pipeID)})).catch((error=>console.error(error)))})).catch((error=>console.error(error)));break;case"MESSAGE":data.message=encrypt(unparsed.message)}compactIDB.addData("messages",Object.assign({},data),`${pipeID}|${vc}`),data.message&&(data.message=decrypt(data.message)),newInbox.messages[vc]=data}},processData.pipeline.flo_multisig=function(pipeID){return(unparsed,newInbox)=>{if(!_loaded.pipeline[pipeID].members.includes(floCrypto.toFloID(unparsed.senderID)))return;let data={time:unparsed.time,sender:unparsed.senderID,pipeID:unparsed.receiverID},vc=unparsed.vectorClock,k=_loaded.pipeline[pipeID].eKey;switch(unparsed.message=decrypt(unparsed.message,k),floDapps.storePubKey(unparsed.senderID,unparsed.pubKey),data.type=unparsed.type,unparsed.type){case"TRANSACTION":data.tx_hex=unparsed.message;break;case"BROADCAST":data.txid=unparsed.message,getChat(pipeID).then((result=>{var tx_hex_list=Object.keys(result).sort().map((i=>result[i].tx_hex)).filter((x=>x));let tx_hex_inital=tx_hex_list[0],tx_hex_final=tx_hex_list.pop();floBlockchainAPI.checkIfSameTx(tx_hex_inital,tx_hex_final)&&floBlockchainAPI.transactionID(tx_hex_final)==data.txid&&disablePipeline(pipeID)})).catch((error=>console.error(error)));break;case"MESSAGE":data.message=encrypt(unparsed.message)}compactIDB.addData("messages",Object.assign({},data),`${pipeID}|${vc}`),data.message&&(data.message=decrypt(data.message)),newInbox.messages[vc]=data}}}(); \ No newline at end of file diff --git a/messenger/scripts/messengerEthereum.js b/messenger/scripts/messengerEthereum.js new file mode 100644 index 0000000..775ad40 --- /dev/null +++ b/messenger/scripts/messengerEthereum.js @@ -0,0 +1,43 @@ +(function (EXPORTS) { //floEthereum v1.0.1a + /* FLO Ethereum Operators */ + /* Make sure you added Taproot, Keccak, FLO and BTC Libraries before */ + 'use strict'; + const floEthereum = EXPORTS; + +// onlyEvenY is usually false. It is needed to be true only when taproot private keys are input +const ethAddressFromPrivateKey = floEthereum.ethAddressFromPrivateKey = function(privateKey, onlyEvenY = false){ + var t1,t1_x,t1_y,t1_y_BigInt,t2,t3,t4; + var groupOrder = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"); + + t1 = bitjs.newPubkey(privateKey); + t1_x = t1.slice(2, 66); t1_y = t1.slice(-64); + if (onlyEvenY) { + t1_y_BigInt = BigInt("0x"+t1_y); + if (t1_y_BigInt % 2n !== 0n) { t1_y_BigInt = (groupOrder-t1_y_BigInt)%groupOrder; t1_y=t1_y_BigInt.toString(16)} + }; + + t2 = t1_x.toString(16) + t1_y.toString(16); + t3 = keccak.keccak_256(Crypto.util.hexToBytes(t2)); + t4 = keccak.extractLast20Bytes(t3); + return "0x" + t4; +} + +const ethAddressFromCompressedPublicKey = floEthereum.ethAddressFromCompressedPublicKey = function(compressedPublicKey){ + var t1,t2,t3,t4; + t1 = coinjs.compressedToUncompressed(compressedPublicKey); + t2 = t1.slice(2); + t3 = keccak.keccak_256(Crypto.util.hexToBytes(t2)); + t4 = keccak.extractLast20Bytes(t3); + return "0x" + t4; +} + +const ethAddressFromUncompressedPublicKey = floEthereum.ethAddressFromUncompressedPublicKey = function(unCompressedPublicKey){ + var t1,t2,t3,t4; + t1 = unCompressedPublicKey; + t2 = t1.slice(2); + t3 = keccak.keccak_256(Crypto.util.hexToBytes(t2)); + t4 = keccak.extractLast20Bytes(t3); + return "0x" + t4; +} + +})('object' === typeof module ? module.exports : window.floEthereum = {}); diff --git a/messenger/test.html b/messenger/test.html new file mode 100644 index 0000000..2e17b1b --- /dev/null +++ b/messenger/test.html @@ -0,0 +1,57 @@ + + + + + + Document + + + + + + +
+ +
Playback
+
quality
+
About
+ +
0.25
+
0.5
+
0.75
+
Normal
+
1.25
+
1.5
+
1.75
+ +
1044p
+
720p
+
480p
+
360p
+
240p
+
144p
+
auto
+ - +
Profile
+
Hobbies
+
Education
+
Family
+
Career
+
+
+ + + \ No newline at end of file