diff --git a/.env b/.env new file mode 100755 index 00000000..2d4b189e --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +# fallback file for env-cmd +# do not edit this file. for your local environment +# create your own file called '.env.local' in the same dir and add entries like +# CONTRACTUS_TESTLOGS=true +# of course drop the '# ' comment prefix in your file ;) diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0a7780f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +bundles/* +discify/* +dist/* +node_modules/* +.env.local +ipfs-contractus +package-lock.json.* +package-lock.json +docs/_build/* diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..0b45f10e --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +.env +bundles/bcc/bcc.js.map +bundles/bcc/dbcpPath.json +discify/ +docu/ +scripts/ +src/ +tsconfig.json +tslint.json \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..9cf94950 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..be3f7b28 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 00000000..64a9fcf2 --- /dev/null +++ b/README.md @@ -0,0 +1,1074 @@ +# blockchain-core + +## Table of Contents + + +- [About](#about) +- [Tests](#tests) +- [Module Initialization](#module-initialization) +- [Handling Profiles](#handling-profiles) + - [Structure](#structure) + - [Example](#example) +- [BaseContract](#basecontract) + - [Create Contracts](#create-contracts) + - [Managing Contract Members](#managing-contract-members) + - [Change Contract State](#change-contract-state) + - [Updating a Members State](#updating-a-members-state) +- [DataContract](#datacontract) + - [Creating DataContracts](#creating-datacontracts) + - [Entries](#entries) + - [List Entries](#list-entries) +- [Encryption](#encryption) + - [Usage](#usage) + - [Examples](#examples) +- [Sharings / Multikeys](#sharings--multikeys) + - [The Sharing Concept](#the-sharing-concept) + - [How to use](#how-to-use) + - [Example](#example-1) +- [Rights and Roles](#rights-and-roles) + - [Add Accounts to Role](#add-accounts-to-role) + - [Get Members of Roles a Contract](#get-members-of-roles-a-contract) + - [Setting Function Permissions](#setting-function-permissions) + - [Setting Operation Permissions](#setting-operation-permissions) +- [IPLD](#ipld) + - [Adding Trees to IPLD](#adding-trees-to-ipld) + - [Retrieving Trees from IPLD](#retrieving-trees-from-ipld) + - [Subtrees](#subtrees) + - [Extending trees](#extending-trees) + - [Getting Values from Subtrees](#getting-values-from-subtrees) + - [About Encryption in IPLD Trees](#about-encryption-in-ipld-trees) +- [Mailbox](#mailbox) + - [Send a Mail](#send-a-mail) + - [Retrieving bmails](#retrieving-bmails) + - [Answering to a bmail](#answering-to-a-bmail) + - [Bmails and EVEs](#bmails-and-eves) + - [Checking a bmails balance](#checking-a-bmails-balance) + - [Withdrawing funds from bmails](#withdrawing-funds-from-bmails) +- [Key Exchange](#key-exchange) + - [Start the Key Exchange Process](#start-the-key-exchange-process) + - [Finishing the Key Exchange Process](#finishing-the-key-exchange-process) +- [Onboarding](#onboarding) + + + + +## About +The blockchain core is a helper library, that offers helpers for interacting with the blockchain. It is written in TypeScript and offers several (up to a certain degree) stand-alone modules, that can be used for +- creating and updating contracts +- managing user profiles +- en- and decryption +- distributed filesystem file handling +- key exchange and key handling +- ENS domain handling +- sending and receiving bmails + + +## Tests +The tests are written with mocha and chai and the files (`*.spec.js`) are located next to the files, they contain tests for. +The tests are in between unit tests and integration tests. They each cover a single class but do not mock external dependencies and use the live blockchain for its contract and transaction related components. They act as a living documentation and examples for using the modules can be found in them. + +As the modules depend on each other, most tests require some repeating initialization steps. To speed things up a bit, the [`TestUtils`](./src/test/test-utils.ts) class is used for creating the modules, this class initializes the required modules, but creates multiple instances of the same modules. This pattern can be used for tests, but when writing code intended for productive use, modules should be re-used instead of creating new ones repeatedly. + +There are multiple scripts for running tests: +- `npm run test` - runs all tests, only recommended when running during CI, takes really long by now +- `npm run testunit ${PATH_TO_SPEC_FILE}` - runs a single `*.spec.js` file, your best friend when writing new modules or upating them +- `npm run testunitbail ${PATH_TO_SPEC_FILE}` - runs a single `*.spec.js` file, breaks on first error without waiting for all tests in this file to finish +- `npm run testunitbrk ${PATH_TO_SPEC_FILE}` - runs a single `*.spec.js` file, steps into breakpoint on first line, can be used when facing startup issues + +All tests are run with the `--inspect` flag for debugging. + + +## Module Initialization +The modules take 1 argument in their constructor, that is (in most modules) an interface with the required dependencies, for example in the [`BaseContract`](./src/contracts/base-contract/base-contract.ts) class: + +```typescript +// ... + +/** + * options for BaseContract constructor + */ +export interface BaseContractOptions { + executor: Executor, + loader: ContractLoader, + log?: Function, + nameResolver: NameResolver, +} + + +/** + * wrapper for BaseContract interactions + * + * @class BaseContract (name) + */ +export class BaseContract extends Logger { + protected options: BaseContractOptions; + + constructor(optionsInput: BaseContractOptions) { + super(optionsInput); + this.options = optionsInput; + } + + // ... +} +``` + +Some of the modules have circular depenencies, as many modules require basic modules like the `Executor` or the `NameResolver` (from [DBCP](https://github.com/evannetwork/dbcp)[+]) and in reverse those two modules need functionalities from their dependents. For example the `Executor` from the sample above needs the `EventHub` (which requires the `Executor` itself) for transactions, that use an events for returning results. These modules +need further initialization steps before they can be used, which are described in their constructors comment and can be seen in their tests. + + +## Handling Profiles +### Structure +A users profile is its personal storage for +- contacts +- encryption keys exchanged with contacts +- an own public key for exchanging keys with new contacts +- bookmarked ÐAPPs +- created contracts + +This data is stored as an [IPLD Graphs](https://github.com/ipld/ipld)[+] per type and stored in a users profile contract. These graphs are independant from each other and have to be saved separately. + +This contract is a [`DataContract`](./src/contracts/database-contract/data-contract.ts) and can be created via the factory at `profile.factory.evan` and looked up at the global profile index `profile.evan`. The creation process and landmap looks like this: + +![profile landmap](https://user-images.githubusercontent.com/1394421/38298221-1938d006-37f7-11e8-9a84-abfd311c97f0.png) + +### Example +This example shows how to create a profile and store a bookmark in it. For abbreviation the creation of the profile helper has been omitted and an existing instance called `profile` is used. +```typescript +// the bookmark we want to store +const sampleDesc = { + title: 'sampleTest', + description: 'desc', + img: 'img', + primaryColor: '#FFFFFF', +}; + +// create new profile, set private key and keyexchange partial key +await profile.createProfile(keyExchange.getDiffieHellmanKeys()); + +// add a bookmark +await profile.addDappBookmark('sample1.test', sampleDesc); + +// store tree to contract +await profile.storeForAccount(profile.treeLabels.bookmarkedDapps); +``` + + +## BaseContract +The [`BaseContract`](./src/contracts/base-contract/base-contract.ts) is the base contract class used for +- [DataContracts](#datacontract) +- [ServiceContractss](#servicecontract) + +Contracts, that inherit from `BaseContracts`, are able to: +- manage a list of contract participants (called "members") +- manage the own state (a flag, that indicate its own life cycle status) +- manage members state (a flag, that indicate the members state in the contract) + +What members can do, what non-members cannot do depends of the + + +### Create Contracts +The API supports creating contracts, that inhering from `BaseContract`. This is done by calling the respective factory. The factory calls are done via a function with this interface: +```solidity +/// @notice create new contract instance +/// @param businessCenter address of the BusinessCenter to use or 0x0 +/// @param provider future owner of the contract +/// @param _contractDescription DBCP definition of the contract +/// @param ensAddress address of the ENS contract +function createContract( + address businessCenter, + address provider, + bytes32 _contractDescription, + address ensAddress) public returns (address); +``` + +The API supports creating contracts with this function. Contracts created this way may not be ready to use and require an additional function at the contract to be called before usage. This function is usually called `init` and its arguments and implementation depends of the specific contract. + +The `createUninitialized` function performs a lookup for the respective factory contract and calls the `createContract` function at it. + +```typescript +const contractOwner = '0x...'; +const businessCenterDomain = 'testbc.evan'; +const contractId = await baseContract.createUninitialized( + 'testdatacontract', // factory name + contractOwner, // account, that will be owner of the new contract + businessCenterDomain, // business center, where the new contract will be created +); +``` + + +### Managing Contract Members +To allow accounts to work with contract resources, they have to be added as members to the contract. This can be can be done with: +```typescript +const contractOwner = '0x0000000000000000000000000000000000000001'; +const invitee = '0x0000000000000000000000000000000000000002'; +const businessCenterDomain = 'testbc.evan'; +const contract = loader.loadContract('BaseContractInterface', contractId); +await baseContract.inviteToContract( + businessCenterDomain, + contractId, + contractOwner, + invitee, +); +``` + +To check if an account is a member of a contract, the contract function `isMember` can be used: +```typescript +const isMember = await executor.executeContractCall(contract, 'isConsumer', invitee); +console.log(isMember); +// Output: +// true +``` + + +### Change Contract State +The contracts state reflects the current state and how other members may be able to interact with it. So for example, a contract for tasks cannot have its tasks resolved, when the contract is still in Draft state. State transitions are limited to configured roles and allow going from one state to another only if configured for this role. + +The contract state can be set via: `function changeContractState(ContractState newState);`. +```typescript +await baseContract.changeContractState(contractId, contractOwner, ContractState.Active); +``` + +`ContractState` is an enum in the BaseContract class, that holds the same state values as the `[BaseContract.sol`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/BaseContract.sol). Alternatively integer values matching the enums in [`BaseContractInterface.sol`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/BaseContractInterface.sol) can be used. + + +### Updating a Members State +A members state reflects this members status in the contract. These status values can for example be be Active, Draft or Terminated. + +Consumer state can be set via: +```solidity +function changeConsumerState(address consumer, ConsumerState state); +``` + +`ConsumerState` is an enum in the BaseContract class, that holds the same state values as the [`BaseContract.sol`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/BaseContract.sol). Alternatively integer values matching the enums in [`BaseContractInterface.sol`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/BaseContractInterface.sol) can be used. + + +## DataContract +The DataContract is a secured data storage contract for single properties and lists. If created on its own, DataContracts cannot do very much. They rely on their authority to check which entries or lists can be used. + +For more information about DataContracts purpose and their authorities see [Data Contract](https://evannetwork.github.io/dev/data-contract)[+] in the evan.network wiki. + +For abbreviation the creation of the data contract helper has been omitted and an existing instance called `dc` is used. + +### Creating DataContracts +Let's say, we want to create a DataContract for a business center at the domain "samplebc.evan" and this business center has a DataContractFactory named "testdatacontract". We want to have two users working in our DataContract, so we get these sample values: +```typescript +const factoryName = 'testdatacontract'; +const businessCenterDomain = 'samplebc.evan'; +const accounts = [ + '0x0000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000002', +]; +``` + +Now create a contract with: +```typescript +const contract = await dc.create(factoryName, accounts[0], businessCenterDomain); +``` + +Okay, that was pretty anticlimatic. And boring. And does not provide a description for the contract. Let's add a description to the process. The definition is a [DBCP](https://github.com/evannetwork/dbcp/wiki)[+] contract definition and is stored in an `Envelope` (see section "Encryption"): +```typescript +const definition: Envelope = { + "public": { + "name": "Data Contract Sample", + "description": "reiterance oxynitrate sat alternize acurative", + "version": "0.1.0", + "author": "contractus", + "dataSchema": { + "list_settable_by_member": { + "$id": "list_settable_by_member_schema", + "type": "object", + "additionalProperties": false, + "properties": { + "foo": { "type": "string" }, + "bar": { "type": "integer" } + } + }, + "entry_settable_by_member": { + "$id": "entry_settable_by_member_schema", + "type": "integer", + } + } + } +}; +definition.cryptoInfo = cryptoProvider.getCryptorByCryptoAlgo('aes').getCryptoInfo(accounts[0]); +const contract = await dc.create('testdatacontract', accounts[0], businessCenterDomain, definition); +``` + +Now we have a nice little DataContract with a description. This contract is now able to be understood by other components, that understand the dbcp. And on top of that, we provided data schemas for the two properties `list_settable_by_member` and `entry_settable_by_member` (written for [ajv](https://github.com/epoberezkin/ajv)[+]). This means, that when someone adds or sets entries to or in those properties, the incoming data is validated before actually encrypting and storing it. + +To allow other users to work on the contract, they have to be invited with: +```typescript +await dc.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[1]); +``` + +Now the user `accounts[1]` can use functions from the contract, but to actually store data, the user needs access to the data key for the DataContract. This can be done via updating the contracts sharing: +```typescript +const blockNr = await web3.eth.getBlockNumber(); +const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', blockNr); +await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', blockNr, contentKey); +``` + +This function, used in `data-contract.spec.js` test file, combines the last steps and will be used in later examples in this section. +```typescript +async function createContract(addSharing = false, schema?) { + let definition; + if (schema) { + definition = JSON.parse(JSON.stringify(sampleDefinition)); + definition.public.dataSchema = schema; + definition.cryptoInfo = cryptoProvider.getCryptorByCryptoAlgo('aes').getCryptoInfo(accounts[0]); + } + const contract = await dc.create('testdatacontract', accounts[0], businessCenterDomain, definition); + await dc.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[1]); + if (addSharing) { + const blockNr = await web3.eth.getBlockNumber(); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', blockNr); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', blockNr, contentKey); + } + return contract; +} +``` + + +### Entries +Entries can be set with: +```typescript +const sampleValue = 123; +await dc.setEntry(contract, 'entry_settable_by_owner', sampleValue, accounts[0]); +``` + +Entries are automatically encrypted before setting it in the contract. If you want to use values as is, without encrypting them, you can add them in raw mode, which sets them as `bytes32` values: +```typescript +const sampleValue = '0x000000000000000000000000000000000000007b'; +await dc.setEntry(contract, 'entry_settable_by_owner', sampleValue, accounts[0], true); +``` + +Entries can be retrieved with: +```typescript +const retrieved = await dc.getEntry(contract, 'entry_settable_by_owner', accounts[0]); +``` + +Raw values can be retrieved in the same way: +```typescript +const retrieved = await dc.getEntry(contract, 'entry_settable_by_owner', accounts[0], true); +``` + + +### List Entries +List entries support the raw mode as well. To use raw values, pass `true` in the same way as wehn using the entries functions. + +List entries can be added in bulk, so the value argument is an array with values. This array can be arbitrarily large **up to a certain degree**. Values are inserted on the blockchain side and adding very large arrays this way may take more gas during the contract transaction, than may fit into a single transaction. If this is the case, values can be added in chunks (multiple transactions). Values can be added with: +```typescript +const sampleValue = { + foo: 'sample', + bar: 123, +}; +await dc.addListEntries(contract, 'list_settable_by_member', [sampleValue], accounts[0]); +``` + +When using lists similar to tagging list entries with metadata, entries can be added in multiple lists at once by passing an array of list names: +```typescript +const sampleValue = { + foo: 'sample', + bar: 123, +}; +await dc.addListEntries(contract, ['list_1', 'list_2'], [sampleValue], accounts[0]); +``` + +List entries can be retrieved one at a time: +```typescript +const itemIndex = 0; +await dc.getListEntry(contract, 'list_settable_by_member', itemIndex, accounts[0])); +``` + +Or all at once: +```typescript +await dc.getListEntries(contract, 'list_settable_by_member', accounts[0])); +``` +In the current implementation, this function retrieves the entries one at a time and may take a longer time when querying large lists, so be aware of that, when you retrieve lists with many entries. + + +## Encryption +### Usage +Data, that is going to be encrypted, is put into `Envelopes`, which are objects, that implement the `Envelope` interface and are containers for encrypted or soon to encrypted data. The interface looks like this: +```typescript +/** + * container for encrypting data + */ +export interface Envelope { + /** + * unencrypted part of the data; will stay as is during encryption + */ + public?: any; + /** + * encrypted part of the data + * if encrypting, this part will be encrypted, depending on the encryption + * if already encrypted, this will be the encrypted value + */ + private?: any; + /** + * describes used encryption + */ + cryptoInfo?: CryptoInfo; +} +``` + +Data in an envelop can be split in two sections `public` and `private`. +`public` is the data, that is visible before decrypting anything and is intended be seen by users, that have no address to secured data in the envelope. This may a public contract description, a short introduction for a ÐAPP, etc. +`private` is data, that can only be accessed, when the `CryptoInfo` has been used to decrypt its contents. + +`CryptoInfo` is an annotation for the data that specifies, with which algorithm the data will be encrypted and where to look for the key. The `CryptoInfo` interface looks like this: +```typescript +/** + * describes used encryption + */ +export interface CryptoInfo { + /** + * algorith used for encryption + */ + algorithm: string; + /** + * block number for which related item is encrypted + */ + block?: number; + /** + * version of the cryptor used; + * describes the implementation applied during decryption and not the algorithm version + */ + cryptorVersion?: number; + /** + * context for encryption, this can be + * - a context known to all parties (e.g. key exchange) + * - a key exchanged between two accounts (e.g. bmails) + * - a key from a sharings info from a contract (e.g. DataContract) + * defaults to 0 + */ + originator?: string; + /** + * length of the key used in encryption + */ + keyLength?: number; +} +``` + +For info about the `algorithm`s used see [Crypto Algorithms](https://evannetwork.github.io/dev/security#crypto-algorithms)[+]. Each `algorithm` has a Class, that implements the `Cryptor` interface, which is used for en- and decrypting the `private` data. Theses cryptors are usually bundled in a `CryptoProvider`, which maps algorithms to instances of cryptors. The mapping is as following: + +| algorithm | cryptor | usage | +| --------- | ------- | ----- | +| aes-256-cbc | Aes | default encryption | +| aes-blob | AesBlob | encryption for files | +| unencrypted | Unencrypted | used for public parts [IPLD Graphs](https://github.com/ipld/ipld)[+], for example public keys in profiles | + +### Examples +Encrypting data: +```typescript +// sample data +const toEncrypt: Envelope = { + public: 'this will stay as is', + private: 'Id commodo nulla ut eiusmod.', // will be encrypted +}; + +// encrypted data is stored as 'hex' in envelopes +const encodingEncrypted = 'hex'; +// encryption in this sample +const algorithm = 'aes-256-cbc'; +// context for encryption +const context = 'context known to all parties'; + +// encrypt private section +const cryptor = cryptoProvider.getCryptorByCryptoAlgo(algorithm); +// use static key in sample +// random key can be generated with: +// const key = await cryptor.generateKey(); +const key = '346c22768f84f3050f5c94cec98349b3c5cbfa0b7315304e13647a49181fd1ef'; +const encryptedBuffer = await cryptor.encrypt(toEncrypt.private, { key, }); +const encrypted = encryptedBuffer.toString(this.encodingEncrypted); + +// build encrypted envelope +const envelope: Envelope = { + private: encrypted, + cryptoInfo: cryptor.getCryptoInfo(dataContract.options.address), +}; +if (toEncrypt.public) { + envelope.public = toEncrypt; +} +``` + +Decrypting data: +```typescript +// sample data +const envelope: Envelope = { + public: 'this will stay as is', + private: 'fececfb235919647feb26af368e1fcfe3f3335c02e9aec6700b16d7634286d6b', +}; + +// decrypt private section from envelope +const cryptor = cryptoProvider.getCryptorByCryptoInfo(envelope.cryptoInfo); +// key has been has been stored / tranferred beforehand +const key = '346c22768f84f3050f5c94cec98349b3c5cbfa0b7315304e13647a49181fd1ef'; +const decryptedBuffer = await cryptor.decrypt( + Buffer.from(envelope.private, this.encodingEncrypted), { key, }); +envelope.private = decryptedBuffer; +``` + + +## Sharings / Multikeys + +### The Sharing Concept +For getting a better understanding about how Sharings and Multikeys work, have a look at [Security](https://evannetwork.github.io/dev/security#sharings)[+] in the evan.network wiki. + +### How to use +For abbreviation the creation of the sharing helper has been omitted and an existing instance called `sharing` is used. + +Add a sharing to a contract: +```typescript +// two sample users, user1 wants to share a key with user2 +const user1 = '0x0000000000000000000000000000000000000001'; +const user2 = '0x0000000000000000000000000000000000000002'; +// create a sample contract +// usually you would have an existing contract, for which you want to manage the sharings +const contract = await executor.createContract('Shared', [], { from: user1, gas: 500000, }); +// user1 shares the given key with user2 +// this key is shared for all contexts ('*') and valid starting with block 0 +await sharing.addSharing(contract.options.address, user1, user2, '*', 0, 'i am the secred that will be shared'); +``` + +Get keys from sharing with: +```typescript +// a sample user +const user2 = '0x0000000000000000000000000000000000000002'; +// user2 wants to read a key after receiving a sharing +// the key requested should be valid for all contexts ('*') and valid up to and including block 100 +const key = await sharing.getKey(contract.options.address, user2, '*', 100); +``` + +### Example +This is an example for a sharing info of a contract. This example has +- three users + * 0x01 - owner of a contract + * 0x02 - member of a contract + * 0x03 - another member with differing permissions +- two timestamps + * block 82745 - first sharing + * block 90000 - splitting data, update sharings +- three sections + * "\*" generic "catch all"used in first sharing + * "secret area" - available for all members + * "super secret area" - available 0x03 +```json +{ + "0x01": { + "82745": { + "*": { + "private": "secret for 0x01, starting from block 82745 for all data", + "cryptoInfo": { + "originator": "0x01,0x01", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + } + }, + "90000": { + "secret area": { + "private": "secret for 0x01, starting from block 90000 for 'secret area'", + "cryptoInfo": { + "originator": "0x01,0x01", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + }, + "super secret area": { + "private": "secret for 0x01, starting from block 90000 for 'super secret area'", + "cryptoInfo": { + "originator": "0x01,0x01", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + } + } + }, + "0x02": { + "82745": { + "*": { + "private": "secret for 0x02, starting from block 82745 for all data", + "cryptoInfo": { + "originator": "0x01,0x02", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + } + }, + "90000": { + "secret area": { + "private": "secret for 0x02, starting from block 90000 for 'secret area'", + "cryptoInfo": { + "originator": "0x01,0x02", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + }, + "super secret area": { + "private": "secret for 0x02, starting from block 90000 for 'super secret area'", + "cryptoInfo": { + "originator": "0x01,0x02", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + } + }, + }, + "0x03": { + "90000": { + "secret area": { + "private": "secret for 0x03, starting from block 90000 for 'secret area'", + "cryptoInfo": { + "originator": "0x01,0x03", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + } + } + } +} +``` + +## Rights and Roles +The [`RightsAndRoles`](./src/contracts/rights-and-roles.ts) module follows the approach described in the evan.network wik at: +- [Function Permissions](https://evannetwork.github.io/dev/security#function-permissions) +- [Operation Permissions](https://evannetwork.github.io/dev/security#operations-permissions) + +It allows to manage permissions for contracts, that use the authority [`DSRolesPerContract.sol`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/DSRolesPerContract.sol) for as its permission approach. + +Contracts, that use [`DSRolesPerContract.sol`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/DSRolesPerContract.sol) and therefore allow to configure with the `RightsAndRoles` are: +- [`BaseContract`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/BaseContract.sol) +- [`DataContract`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/DataContract.sol) +- [`ServiceContract`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/ServiceContract.sol) +- [`Shared.sol`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/Shared.sol) +- [`Described.sol`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/Described.sol) +- [`BusinessCenter.sol`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/BusinessCenter.sol) + + +### Add Accounts to Role +The main principle is that accounts can be assigned to roles and those roles can be granted capabilities. "Function Permissions" are basically the capability to call specific functions if the calling account belongs to a certain role. To add an account to the role 'member', for example use: +```typescript +const contractOwner = '0x0000000000000000000000000000000000000001'; +const newMember = '0x0000000000000000000000000000000000000002'; +const memberRole = 1; +await rightsAndRoles.addAccountToRole( + contract, // contract to be updated + contractOwner, // account, that can change permissions + newMember, // add this account to role + memberRole, // role id, uint8 value +); +``` + + +### Get Members of Roles a Contract +The [`DSRolesPerContract.sol`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/DSRolesPerContract.sol) authority tracks used roles and their members and allows to retrieve an overview with all roles and their members. To get this information use: +```typescript +const members = await rightsAndRoles.getMembers(contract); +console.log(members); +// Output: +// { +// "0": [ +// "0x0000000000000000000000000000000000000001" +// ], +// "1": [ +// "0x0000000000000000000000000000000000000001", +// "0x0000000000000000000000000000000000000002" +// ] +// } +``` +The contract from this example has an owner (`0x0000000000000000000000000000000000000001`) and a member (`0x0000000000000000000000000000000000000002`). As the owner account has the member role as well, it is listed among the members. + + +### Setting Function Permissions +"Function permissions" are granted or denying by allowing a certain role to execute a specific function. E.g. to grant the role "member" the permission to use the function `addListEntries`, that has two arguments (a `bytes32` array and a `bytes32` value) use: +```typescript +const contractOwner = '0x0000000000000000000000000000000000000001'; +const memberRole = 1; +await rightsAndRoles.setFunctionPermission( + contract, // contract to be updated + contractOwner, // account, that can change permissions + memberRole, // role id, uint8 value + 'addListEntries(bytes32[],bytes32[])', // (unhashed) function selector + true, // grant this capability +); +``` +The function is specified as the unhashed [function selector](http://solidity.readthedocs.io/en/latest/abi-spec.html#function-selector)[+] and must follow its guidelines (no spaces, property typenames, etc.) for the function to be able to generate valid hashes for later validations. + + +### Setting Operation Permissions +"Operation Permissions" are capabilities granted per contract logic. They have a `bytes32` key, that represents the capability, e.g. in a [`DataContract`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/DataContract.sol) a capability to add values to a certain list can be granted. + +The way, those capability hashes are build depends on the contract logic and differs from contract to contract. For example a capability check for validation if a member is allowed to add an item to the list "example" in a [`DataContract`](https://github.com/evannetwork/smart-contracts/blob/master/contracts/DataContract.sol) has four arguments, in this case: +- which role is allowed to do? (e.g. a member) +- what type of element is modified? (--> a list) +- which element is modified? (name of the list --> "example") +- type of the modification (--> "set an item" (== "add an item")) +These four values are combined into one `bytes32` value, that is used when granting or checking permissions, the `setOperationPermission` function takes care of that: +```typescript +// make sure, you have required the enums from rights-and-roles.ts +import { ModificationType, PropertyType } from 'blockchain-core'; +const contractOwner = '0x0000000000000000000000000000000000000001'; +const memberRole = 1; +await rightsAndRoles.setFunctionPermission( + contract, // contract to be updated + contractOwner, // account, that can change permissions + memberRole, // role id, uint8 value + 'example', // name of the object + PropertyType.ListEntry, // what type of element is modified + ModificationType.Set, // type of the modification + true, // grant this capability +); +``` + + +## IPLD +[IPLD](https://github.com/ipld/ipld)[+] is a way to store data as trees. The used implementation relies on [js-ipld-graph-builder](https://github.com/ipld/js-ipld-graph-builder)[+] for iterating over tree nodes and setting new subtrees, but uses a few modifications to the standard: +- nodes are not stored as [IPFS DAGs](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/DAG.md)[+], but stored as play JSON IPFS files +- nodes, that are encrypted, contain the property `cryptoInfo` for decryption (see [Encryption](#encryption)) + +### Adding Trees to IPLD +To add data to IPLD use the store function. The result is the hash for the root as a `bytes32` hash, that can be stored at smart contracts and used to retrieve the value later on: +```typescript +const sampleObject = { + personalInfo: { + firstName: 'eris', + }, +}; +const stored = await ipld.store(Object.assign({}, sampleObject)); +console.log(stored); +// Output: +// 0x12f6526dbe223eddd6c6a0fb7df118c87c56d34bf0c845b54bdca2fec0f3017d +``` + +### Retrieving Trees from IPLD +To retrieve data from IPLD trees, use the `bytes32` hash from storing the data: +```typescript +const stored = '0x12f6526dbe223eddd6c6a0fb7df118c87c56d34bf0c845b54bdca2fec0f3017d'; +const loaded = await ipld.getLinkedGraph(stored, ''); +console.dir(Ipld.purgeCryptoInfo(loaded)); +// Output: +// { personalInfo: { firstName: 'eris' } } +``` +For info about the `Ipld.purgeCryptoInfo` part see [Encryption in IPLD Trees](#encryption-in-ipld-trees). + +The second argument is the path inside the tree. Passing '' means "retrieve data from root level". To get more specifc data, provide a path: +```typescript +const stored = '0x12f6526dbe223eddd6c6a0fb7df118c87c56d34bf0c845b54bdca2fec0f3017d'; +const loaded = await ipld.getLinkedGraph(stored, 'personalInfo'); +console.dir(Ipld.purgeCryptoInfo(loaded)); +// Output: +// { firstName: 'eris' } +``` + +```typescript +const stored = '0x12f6526dbe223eddd6c6a0fb7df118c87c56d34bf0c845b54bdca2fec0f3017d'; +const loaded = await ipld.getLinkedGraph(stored, 'personalInfo/firstName'); +console.dir(Ipld.purgeCryptoInfo(loaded)); +// Output: +// 'eris' +``` + +### Subtrees +What's pretty useful about IPLD graphs is, that not only plain JSON trees can be stored, but that those trees can be linked to other graphs, which makes it possible to build very powerful tree structures, that consist of multiple separate trees, that can be used on their own or in a tree, that combines all of those. The resulting hash is again `bytes32` hash and this can be stored in smart contracts like any other IPFS hash. + +#### Extending trees +To combine separate IPLD trees, one tree has to be extended with the other one: +```typescript +const sampleObject = { + personalInfo: { + firstName: 'eris', + }, +}; +const sub = { + contracts: ['0x01', '0x02', '0x03'] +}; +const extended = await ipld.set( + sampleObject, // extend this graph + 'dapps', // attach the subgraph under the path "dapps" + sub, // attach this graph as a subgraph +); +console.log(JSON.stringify(extended, null, 2)); +// Output: +// { +// "personalInfo": { +// "firstName": "eris" +// }, +// "dapps": { +// "/": { +// "contracts": [ +// "0x01", +// "0x02", +// "0x03" +// ] +// } +// } +// } +``` + +Not too fancy and still not stored in IPFS, but note the `"/"` key. This is the junction point, that connects the two trees. Subtrees under such a junction point will be stored as a separate IPLD graph and only the reference to this graph is stored as the value of `"/"`. +```typescript +console.log(JSON.stringify(extended, null, 2)); +const extendedstored = await ipld.store(Object.assign({}, extended)); +// Output: +// "0xc74f6946aacbbd1418ddd7dec83a5bcd3710b384de767d529e624f9f08cbf9b4" +const loaded = await ipld.getLinkedGraph(extendedstored, ''); +console.log(JSON.stringify(Ipld.purgeCryptoInfo(loaded), null, 2)); +// Output: +// +// "personalInfo": { +// "firstName": "eris" +// }, +// "dapps": { +// "/": { +// "type": "Buffer", +// "data": [ 18, 32, 246, 21, 166, 135, 236, 212, 70, 130, 94, 47, 81, 135, 153, 154, 201, 69, 109, 249, 97, 84, 252, 56, 214, 195, 149, 133, 116, 253, 19, 87, 217, 66 ] +// } +// } +// +``` + +As you can see, the subgraph is added as a serialized Buffer. This Buffer represents the hash for the root hash of the subtree. + + +#### Getting Values from Subtrees +The path argument from the getLinkedGraph is able to resolve values in subtrees as well: +```typescript +const loaded = await ipld.getLinkedGraph(extendedstored, 'dapps/contracts'); +console.log(JSON.stringify(loaded, null, 2)); +// Output: +// [ +// "0x01", +// "0x02", +// "0x03" +// ] +``` + +The `getLinkedGraph` function resolves subgraphs only when required, which means, that if the path argument does not query into a subgraph, this tree is returned as a Buffer, like in the previous example. + +A second function to retrieve values from IPLD graphs called `getResolvedGraph`, which resolves all subgraphs, but this function is intended for debugging and analysis purposes and should not be used in production environment. + + +### About Encryption in IPLD Trees +The last examples used `Ipld.purgeCryptoInfo` to cleanup the objects before logging them. This was done, because IPLD graphs are encrypted by default, which has a few impact on the data stored: +- The root node of a tree is "encrypted" with the encryption algorithm "unencrypted", resulting in the root node having its data stored as a Buffer. This is done to keep the root node in the same format as the other nodes, as: +- Nodes in the Tree are encrypted. This encryption is specified in the constructor as `defaultCryptoAlgo`. +- All nodes are en- or decrypted with the same account or "originator". The originator, that is used, is specified in the constructor as "originator". This means, that the IPLD instance is account bound and a new instance has to be created if another account should be used. + +Going back to the first example and logging the result without purging the properties, we get: +```typescript +const stored = '0x12f6526dbe223eddd6c6a0fb7df118c87c56d34bf0c845b54bdca2fec0f3017d'; +const loaded = await ipld.getLinkedGraph(stored, ''); +console.dir(loaded); +// Output: +// { personalInfo: { firstName: 'eris' }, +// cryptoInfo: +// { originator: '0xd7c759941fa3962e4833707f2f44f8cb11b471916fb6f9f0facb03119628234e', +// keyLength: 256, +// algorithm: 'aes-256-cbc' } } +// +``` + + +## Mailbox +The [`Mailbox`](src/mailbox.ts) module is used for sending and retrieving bmails (blockchain mails) to other even.network members. Sending regular bmails between to parties requires them to have completed a [Key Exchange](#key-exchange) before being able to send encrypted messages. When exchanging the keys, bmails are encrypted with a commonly known key, that is only valid is this case and the underlying messages, that contain the actual keys are encrypted with Diffie Hellman keys, to ensure, that keys are exchanged in a safe manner (see [Key Exchange](#key-exchange) for details). + +The mailbox is a [smart contract](https://github.com/evannetwork/smart-contracts/blob/master/contracts/MailBox.sol), that holds +- `bytes32` hashes, that are the encrypted contents of the mails +- basic metadata about the mails, like + + recipient of a mail + + sender of a mail + + amount of EVEs, that belongs to the bmail +- if the mail is an answer to another mail, the reference to the original mail + +### Send a Mail +Mails can be sent with: +```typescript +// account, that sends the mail +const account1 = '0x0000000000000000000000000000000000000001'; +// account, that receives the mail +const account2 = '0x0000000000000000000000000000000000000002'; +// mailbox of the sender +const mailbox1 = {}; +// mailbox of the receiver +const mailbox2 = {}; + +const bmail = { + content: { + from: account1, + to, + title: 'Example bmail', + body: 'This is a little example to demonstrate sending a bmail.', + attachments: [ ] + } +}; +await mailbox1.sendMail(bmail, account1, account2); +``` + +### Retrieving bmails +To get received mails use: +```typescript +const received = await mailbox2.getMails(); +console.dir(JSON.stringify(received[0], null, 2)); +// Output: +// { +// "mails": { +// "0x000000000000000000000000000000000000000e": { +// "content": { +// "from": "0x0000000000000000000000000000000000000001", +// "to": "0x0000000000000000000000000000000000000002", +// "title": "Example bmail", +// "body": "This is a little example to demonstrate sending a bmail.", +// "attachments": [ ], +// "sent": 1527083983148 +// }, +// "cryptoInfo": { +// "originator": "0x549704d235e1fe5cd7326a1eb0c44c1e0a5434799ba6ff2370c2955730b66e2b", +// "keyLength": 256, +// "algorithm": "aes-256-cbc" +// } +// } +// }, +// "totalResultCount": 9 +// } +``` + +Results can be paged with passing arguments for page size and offsetto the `getMails` function: +```typescript +const received = await mailbox2.getMails(3, 0); +console.dir(JSON.stringify(received[0], null, 2)); +// Output: +// { mails: +// { '0x000000000000000000000000000000000000000e': { content: [Object], cryptoInfo: [Object] }, +// '0x000000000000000000000000000000000000000d': { content: [Object], cryptoInfo: [Object] }, +// '0x000000000000000000000000000000000000000c': { content: [Object], cryptoInfo: [Object] } }, +// totalResultCount: 9 } +``` + +To get bmails *sent* by an account, use (the example account hasn't sent any bmail yet): +```typescript +const received = await mailbox2.getMails(3, 0, 'Sent'); +console.dir(JSON.stringify(received[0], null, 2)); +// Output: +// { mails: {}, totalResultCount: 0 } +``` + +### Answering to a bmail +Answering to a bmail works similar to sending a bmail, the only difference is, that the id of the original bmail has to be appended to the mail as well: +```typescript +const answer = { + content: { + from: account1, + to, + title: 'Example answer', + body: 'This is a little example to demonstrate sending an answer.', + attachments: [ ], + parentId: '0x000000000000000000000000000000000000000e', + } +}; +await mailbox1.sendMail(answer, account2, account1); +``` + +### Bmails and EVEs +Bmails can contain EVEs for the recipient as well. Because retrieving bmails is a reading operation, funds send with a bmail have to be retrieved separately. + +#### Checking a bmails balance +Funds can be checked with: +```typescript +const bmail = { + content: { + from: account1, + to, + title: 'Example bmail', + body: 'This is a little example to demonstrate sending a bmail.', + attachments: [ ] + } +}; +await mailbox1.sendMail(bmail, account1, account2, web3.utils.toWei('0.1', 'Ether')); +const received = await mailbox2.getMails(1, 0); +const mailBalance = await mailbox2.getBalanceFromMail(Object.keys(received)[0]); +console.log(mailBalance); +// Output: +// 100000000000000000 +``` + +#### Withdrawing funds from bmails +Funds from bmails can be claimed with the account, that received the bmail. Funds are transferred to a specified account, which can be the claiming account or another account of choice. +```typescript +const received = await mailbox2.getMails(1, 0); +const mailBalance = await mailbox2.getBalanceFromMail(Object.keys(received)[0]); +console.log(mailBalance); +// Output: +// 100000000000000000 +await mailbox2.withdrawFromMail(received)[0], accounts2); +const mailBalance = await mailbox2.getBalanceFromMail(Object.keys(received)[0]); +console.log(mailBalance); +// Output: +// 0 +``` + + +## Key Exchange +The `KeyExchange` module is used to exchange communication keys between two parties, assuming that both have created a profile and have a public facing partial Diffie Hellman key part (the combination of their own secret and the shared secret). The key exchange consists of three steps: +1. create a new communication key, that will be used by both parties for en- and decryption and store it on the initiators side +2. look up the other parties partial Diffie Hellman key part and combine it with the own private key to create the exchange key +3. use the exchange key to encrypt the communication key and send it via bmail (blockchain mail) to other party + +### Start the Key Exchange Process +This example retrieves public facing partial Diffie Hellman key part from a second party and sends an invitation mail to it: +```typescript +// +// account, that initiates the invitation +const account1 = '0x0000000000000000000000000000000000000001'; +// account, that will receive the invitation +const account2 = '0x0000000000000000000000000000000000000002'; +// profile from user, that initiates key exchange +const profile1 = {}; +// profile from user, that is going to receive the invitation +const profile2 = {}; +// key exchange instance for account1 +const keyExchange1 = {}; +// key exchange instance for account2 +const keyExchange2 = {}; + +const foreignPubkey = await profile2.getPublicKey(); +const commKey = await keyExchange.generateCommKey(); +await keyExchange.sendInvite(account2, foreignPubkey, commKey, { + fromAlias: 'Bob', // initiating user states, that his name is 'Bob' +}); +await profile1.addContactKey(account2, 'commKey', commKey); +await profile1.storeForAccount(profile1.treeLabels.addressBook); +``` + +### Finishing the Key Exchange Process +Let's assume that the communication key from the last example has been successfully sent to the other party and continue at there end from here. To keep the roles from the last example, the variables profile1, profile2 will belong to the same accounts: +```typescript +const encryptedCommKey = '...'; // key sent by account1 +const profile1 = await profile1.getPublicKey(); +const commSecret = keyExchange2.computeSecretKey(profile1); +const commKey = await keyExchange2.decryptCommKey(encryptedCommKey, commSecret.toString('hex')); +``` + + +## Onboarding +The onboarding process is used to enable users to invite other users, where no blockchain account id is known. It allows to send an email to such contacts, that contains a link. This link points to a evan.network ÐApp, that allows accept the invitation by either creating a new account or by accepting it with an existing account. + +It uses the [Key Exchange](#key-exchange) module described in the last section for its underlying key exchange process but moves the process of creating a new communication key to the invited user. + +To get in contact with a user via email, a smart agent is used. This smart agent has to be added as a contact and a regular key exchange with the smart agent is performed. The agent accepts the invitation automatically and the inviting user sends a bmail (blockchain mail) with the contact details of the user, that should be invited, and an amount of welcome EVEs to the smart agent. + +The onboarding smart creates a session on his end and sends an email to the invited user, that includes the session token, with which the invited user can claim the welcome EVEs. + +The invited user now creates or confirms an account and start the key exchange process on his or her end. The rest of the flow is as described in [Key Exchange](#key-exchange). + +To start the process at from the inviting users side, make sure that this user has exchanged keys with the onboarding smart agent. Then you can use: +```typescript +await onboarding.sendInvitation({ + fromAlias: 'example inviter', + to: 'example invitee ', + lang: 'en', + subject: 'evan.network Onboarding Invitation', + body: 'I\'d like to welcome you on board.', +}, web3.utils.toWei('1')); +``` diff --git a/VERSIONS.md b/VERSIONS.md new file mode 100644 index 00000000..b9843443 --- /dev/null +++ b/VERSIONS.md @@ -0,0 +1,48 @@ +# blockchain-core + +## Next Version +### Features +### Fixes +### Deprecations + +## Version 1.0.2 +### Features +- add support for adding sharing file hashes to cache to avoid duplicate contract calls +- add paging to `getCalls` and `getAnswers` in `ServiceContract` module +- add function to clear sharing caches to `Sharing` module +- add support for nested encryption to `ServiceContract` module +- rename to api-blockchain-core + +### Fixes + +### Deprecations +- change service call encryption schema to multi-sharings based encryption + + +## Version 1.0.1 +### Features +- add docu for rights-and-roles.ts, ipld.ts +- use @evan.network for package name and dependencies scopes +- add .npmignore +- (deprecation) rights-and-roles.ts:hasUserRole second argument "accountId" will be dropped, as it isnt' required anymore +- rename *contractus* variables to *evan* +- rename bcc-core bundle to bcc + - rename BCCCore to CoreRuntime + - rename BCCProfile to ProfileRuntime + - rename BCCBC to BCRuntime +- allow overwriting runtimes nameResolver with runtimeConfig +- fix unbound entry retrieval in DataContract.getListEntries by adding paging to it +- add `removeAccountFromRole` and `transferOwnership` to `RightsAndRoles` for better permission management +- make `extendSharings` publicly accessible for adding properties to sharings withou saving them +- add `createSharing` to `DataContract` and accept a sharings hash in `createContract` , which allows to decouple sharing creation and contract creation +- accept ipld hashes in `storeForAccount` in `Profile` to decouple tree encryption and property storing +- add support for multi-sharings to `Sharings` module +- add multi-sharing support to `ServiceContract` module + +## Version 1.0.0 +- DBCP update +- Fix web3 reconnect +- Add iframe support for dapps + +## Version 0.9.0 +- initial version and release candidate for 1.0.0 \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..a9fb0f68 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = blockchain-core +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/blockchain/account-store.rst b/docs/blockchain/account-store.rst new file mode 100644 index 00000000..5978991b --- /dev/null +++ b/docs/blockchain/account-store.rst @@ -0,0 +1,109 @@ +================================================================================ +Account Store +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - AccountStore + * - Implements + - `KeyStoreInterface `__ + * - Extends + - `Logger `_ + * - Source + - `account-store.ts `__ + +The `AccountStore `_ implements the `KeyStoreInterface `_ and is a wrapper for a storage, where evan.network account ids are stored. The default `AccountStore `_ takes an account --> private key mapping as a pojo as its arguments and uses this to perform lookups, when the :ref:`getPrivateKey ` function is called. This lookup needs to be done, when transactions are signed by the `InternalSigner `_ (see `Signer `_). + +Note that the return value of the :ref:`getPrivateKey ` function is a promise. This may not be required in the default `AccountStore `_, but this allows you to implement own implementations of the `KeyStoreInterface `_, which may enforce a more strict security behavior or are able to access other sources for private keys. + + + +------------------------------------------------------------------------------ + +.. _accountstore_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new AccountStore(options); + +Creates a new AccountStore instance. + +---------- +Parameters +---------- + +#. ``options`` - ``AccountStoreOptions``: options for AccountStore constructor. + * ``accounts`` - ``any``: object with accountid/privatekey mapping + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``AccountStore`` instance + +------- +Example +------- + +.. code-block:: typescript + + const accountStore = new AccountStore({ + accounts: { + '0x1234': '12479abc3df' + } + }); + + + +-------------------------------------------------------------------------------- + +.. _accountstore_getPrivateKey: + +getPrivateKey +=================== + +.. code-block:: javascript + + accountStore.getPrivateKey(accountId); + +get private key for given account + + + +---------- +Parameters +---------- + +#. ``accountId`` - ``string``: eth accountId + +------- +Returns +------- + +Promise resolves to ``string``: private key for this account + +------- +Example +------- + +.. code-block:: javascript + + const privateKey = await runtime.accountStore.getPrivateKey('0x0000000000000000000000000000000000000002'); + +.. required for building markup + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface diff --git a/docs/blockchain/description.rst b/docs/blockchain/description.rst new file mode 100644 index 00000000..014483fe --- /dev/null +++ b/docs/blockchain/description.rst @@ -0,0 +1,479 @@ +================================================================================ +Description +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Description + * - Extends + - `Description `_ + * - Source + - `description.ts `_ + * - Tests + - `description.spec.ts `_ + +The Description module is the main entry point for interacting with contract descriptions. It allows you to: + +- get and set descriptions +- work with contracts and ENS descriptions +- create web3.js contract instances directly from an Ethereum address and its description +- The main use cases for interacting with a contracts descriptin in your application will most probably be reading a contracts description and loading contracts via their description. + +The examples folder folder contains some samples for getting started. With consuming or setting contract descriptions. + + + +.. _description_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new Description(options); + +Creates a new Description instance. + +---------- +Parameters +---------- + +#. ``options`` - ``DescriptionOptions``: options for Description constructor. + * ``cryptoProvider`` - |source cryptoProvider|_: |source cryptoProvider|_ instance + * ``dfs`` - |source dfsInterface|_: |source dfsInterface|_ instance + * ``executor`` - |source executor|_: |source executor|_ instance + * ``keyProvider`` - |source keyProvider|_: |source keyProvider|_ instance + * ``nameResolver`` - |source nameResolver|_: |source nameResolver|_ instance + * ``contractLoader`` - |source contractLoader|_: |source contractLoader|_ instance + * ``web3`` - |source web3|_: |source web3|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``Description`` instance + +------- +Example +------- + +.. code-block:: typescript + + const description = new Description({ + cryptoProvider, + dfs, + executor, + keyProvider, + nameResolver, + contractLoader, + web3, + }); + + + +-------------------------------------------------------------------------------- + +.. _description_getDescription: + +getDescription +=================== + +.. code-block:: javascript + + description.getDescription(address, accountId); + +loads description envelope from ens or contract if an ENS address has a contract set as well and this contract has a defintion, the contract definition is preferred over the ENS definition and therefore returned + +---------- +Parameters +---------- + +#. ``address`` - ``string``: The ens address or contract address where the description is stored +#. ``accountId`` - ``string``: Account id to load the contract address for + +------- +Returns +------- + +``Promise`` returns ``Envelope``: description as an Envelope. + +------- +Example +------- + +.. code-block:: javascript + + const address = '0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49'; + const accountId = '0x000000000000000000000000000000000000beef'; + const description = await runtime.description.getDescription(address, accountId); + console.dir(description); + // Output: + // { public: + // { name: 'DBCP sample greeter', + // description: 'smart contract with a greeting message and a data property', + // author: 'dbcp test', + // tags: [ 'example', 'greeter' ], + // version: '0.1.0', + // abis: { own: [Array] } } } + +------------------------------------------------------------------------------ + +.. _description_setDescription: + +setDescription +=================== + +.. code-block:: javascript + + description.setDescription(address, envelope, accountId); + +set description, can be used for contract addresses and ENS addresses + +---------- +Parameters +---------- + +#. ``address`` - ``string``: contract address or ENS address +#. ``envelope`` - ``Envelope``: description as an envelope +#. ``accountId`` - ``string``: ETH account id + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done. + +------- +Example +------- + +.. code-block:: javascript + + const address = '0x...'; // or 'test.evan' as ens name + const accountId = '0x...'; + const description = { + "public": { + "name": "DBCP sample contract", + "description": "DBCP sample contract description", + "author": "dbcp test", + "tags": [ + "example", + "greeter" + ], + "version": "0.1.0" + } + }; + await runtime.description.setDescription(address, description, accountId); + +------------------------------------------------------------------------------ + +.. _description_validateDescription: + +validateDescription +=================== + +Descriptions are validated when setting them. A list of known DBCP definition schemas is maintained in `description.schema.ts `_ . If a description is set, its property `dbcpVersion` will be used for validating the description, if `dbcpVersion` is not provided, the latest version known to the API is used. + +Descriptions can be checked against the validator before setting them. + + +.. code-block:: javascript + + description.validateDescription(envelope); + +try to validate description envelope; throw Error if validation fails + +---------- +Parameters +---------- + +#. ``envelope`` - ``Envelope``: envelop with description data; private has to be unencrypted + +------- +Returns +------- + +``Promise`` returns ``boolean|any[]``: true if valid or array of issues. + +------- +Example +------- + +.. code-block:: javascript + + const brokenDescription = { + "public": { + "name": "DBCP sample contract with way to few properties", + } + }; + console.log(runtime.description.validateDescription(brokenDescription)); + // Output: + // [ { keyword: 'required', + // dataPath: '', + // schemaPath: '#/required', + // params: { missingProperty: 'description' }, + // message: 'should have required property \'description\'' }, + // { keyword: 'required', + // dataPath: '', + // schemaPath: '#/required', + // params: { missingProperty: 'author' }, + // message: 'should have required property \'author\'' }, + // { keyword: 'required', + // dataPath: '', + // schemaPath: '#/required', + // params: { missingProperty: 'version' }, + // message: 'should have required property \'version\'' } ] + +.. code-block:: javascript + + const workingDescription = { + "public": { + "name": "DBCP sample contract", + "description": "DBCP sample contract description", + "author": "dbcp test", + "tags": [ + "example", + "greeter" + ], + "version": "0.1.0" + } + }; + console.log(runtime.description.validateDescription(workingDescription)); + // Output: + // true + +------------------------------------------------------------------------------ + + + += Contract = +============ + +.. _description_getDescriptionFromContract: + +getDescriptionFromContract +========================== + +.. code-block:: javascript + + description.getDescriptionFromContract(address, accountId); + +loads description envelope from contract + +---------- +Parameters +---------- + +#. ``address`` - ``string``: The ens address or contract address where the description is stored +#. ``accountId`` - ``string``: Account id to load the contract address for + +------- +Returns +------- + +``Promise`` returns ``Envelope``: description as an Envelope. + +------- +Example +------- + +.. code-block:: javascript + + const address = '0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49'; + const accountId = '0x000000000000000000000000000000000000beef'; + const description = await runtime.description.getDescriptionFromContract(address, accountId); + console.dir(description); + // Output: + // { public: + // { name: 'DBCP sample greeter', + // description: 'smart contract with a greeting message and a data property', + // author: 'dbcp test', + // tags: [ 'example', 'greeter' ], + // version: '0.1.0', + // abis: { own: [Array] } } } + +------------------------------------------------------------------------------ + +.. _description_setDescriptionToContract: + +setDescriptionToContract +======================== + +.. code-block:: javascript + + description.setDescriptionToContract(contractAddress, envelope, accountId); + +store description at contract + +---------- +Parameters +---------- + +#. ``contractAddress`` - ``string``: The contract address where description will be stored +#. ``envelope`` - ``Envelope``: description as an envelope +#. ``accountId`` - ``string``: ETH account id + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done. + +------- +Example +------- + +.. code-block:: javascript + + const address = '0x...'; + const accountId = '0x...'; + const description = { + "public": { + "name": "DBCP sample contract", + "description": "DBCP sample contract description", + "author": "dbcp test", + "tags": [ + "example", + "greeter" + ], + "version": "0.1.0" + } + }; + await runtime.description.setDescriptionToContract(address, description, accountId); + +------------------------------------------------------------------------------ + += ENS = +========= + +ENS addresses are able to hold multiple values at once. So they may be holding a contract address and a description. If this is the case and the contract at the ENS address has another description, the contracts description is preferred over the ENS description. If you explicitly intend to retrieve an ENS endpoints description and want to ignore the contracts description, use the function `getDescriptionFromEns`. + +------------------------------------------------------------------------------ + + +.. _description_getDescriptionFromEns: + +getDescriptionFromEns +===================== + +.. code-block:: javascript + + description.getDescriptionFromEns(address); + +loads description envelope from ens + +---------- +Parameters +---------- + +#. ``ensAddress`` - ``string``: The ens address where the description is stored + +------- +Returns +------- + +``Promise`` returns ``Envelope``: description as an Envelope. + +------- +Example +------- + +.. code-block:: javascript + + const address = '0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49'; + const accountId = '0x000000000000000000000000000000000000beef'; + const description = await runtime.description.getDescriptionFromContract(address, accountId); + console.dir(description); + // Output: + // { public: + // { name: 'DBCP sample greeter', + // description: 'smart contract with a greeting message and a data property', + // author: 'dbcp test', + // tags: [ 'example', 'greeter' ], + // version: '0.1.0', + // abis: { own: [Array] } } } + +------------------------------------------------------------------------------ + +.. _description_setDescriptionToEns: + +setDescriptionToEns +=================== + +.. code-block:: javascript + + description.setDescriptionToEns(ensAddress, envelope, accountId); + +store description at contract + +---------- +Parameters +---------- + +#. ``contractAddress`` - ``string``: The ens address where description will be stored +#. ``envelope`` - ``Envelope``: description as an envelope +#. ``accountId`` - ``string``: ETH account id + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done. + +------- +Example +------- + +.. code-block:: javascript + + const address = '0x...'; + const accountId = '0x...'; + const description = { + "public": { + "name": "DBCP sample contract", + "description": "DBCP sample contract description", + "author": "dbcp test", + "tags": [ + "example", + "greeter" + ], + "version": "0.1.0" + } + }; + await runtime.description.setDescriptionToEns(address, description, accountId); + +.. required for building markup + +.. |source executor| replace:: ``Executor`` +.. _source executor: /blockchain/executor.html + +.. |source contractLoader| replace:: ``ContractLoader`` +.. _source contractLoader: /contracts/contract-loader.html + +.. |source cryptoProvider| replace:: ``CryptoProvider`` +.. _source cryptoProvider: /encryption/crypto-provider.html + +.. |source dfsInterface| replace:: ``DfsInterface`` +.. _source dfsInterface: /dfs/dfs-interface.html + +.. |source keyProvider| replace:: ``KeyProvider`` +.. _source keyProvider: /key-provider + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source nameResolver| replace:: ``NameResolver`` +.. _source nameResolver: /blockchain/name-resolver.html + +.. |source web3| replace:: ``Web3`` +.. _source web3: https://github.com/ethereum/web3.js/ \ No newline at end of file diff --git a/docs/blockchain/event-hub.rst b/docs/blockchain/event-hub.rst new file mode 100644 index 00000000..c2c6c1e0 --- /dev/null +++ b/docs/blockchain/event-hub.rst @@ -0,0 +1,222 @@ +================================================================================ +Event Hub +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - EventHub + * - Extends + - `Logger `_ + * - Source + - `event-hub.ts `_ + +The `EventHub `_ helper is wrapper for using contract events. These include +- contract events (e.g. contract factory may trigger an event, announcing the address of the new contract) +- global events (some contracts in the `evan.network `_ economy, like the `MailBox` use such global events) + +------------------------------------------------------------------------------ + +.. _eventhub_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new EventHub(options); + +Creates a new EventHub instance. + +---------- +Parameters +---------- + +#. ``options`` - ``EventHubOptions``: options for EventHub constructor. + * ``config`` - ``any``: configuration object for the eventhub module + * ``nameResolver`` - |source nameResolver|_: |source nameResolver|_ instance + * ``contractLoader`` - |source contractLoader|_: |source contractLoader|_ instance + * ``eventWeb3`` - |source web3|_ (optional): |source web3|_ instance used for event handling (metamask web3 can't handle events correct) + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``EventHub`` instance + +------- +Example +------- + +.. code-block:: typescript + + const eventHub = new EventHub({ + config, + nameResolver, + contractLoader, + }); + + + +-------------------------------------------------------------------------------- + +.. _eventhub_subscribe: + +subscribe +=================== + +.. code-block:: javascript + + eventHub.subscribe(contractName, contractAddress, eventName, filterFunction, onEvent, fromBlock); + +subscribe to a contract event or a global EventHub event + +---------- +Parameters +---------- + +#. ``contractName`` - ``string``: target contract name (must be available within |source contractLoader|_ ) +#. ``contractAddress`` - ``string``: target contract address +#. ``eventName`` - ``string``: name of the event to subscribe to +#. ``filterFunction`` - ``function``: a function that returns true or a Promise that resolves to true if onEvent function should be applied +#. ``onEvent`` - ``function``: executed when event was fired and the filter matches, gets the event as its parameter +#. ``fromBlock`` - ``string`` (optional): get all events from this block, defaults to ``latest`` + +------- +Returns +------- + +``Promise`` resolves to ``string``: event subscription. + +------- +Example +------- + +.. code-block:: javascript + + // subscribe to the 'ContractEvent' at the EventHub located at '00000000000000000000000000000000deadbeef' + runtime.eventHub + .subscribe( + 'EventHub', + '00000000000000000000000000000000deadbeef', + 'ContractEvent', + (event) => true, + (event) => { + console.dir(event) + } + ) + .then((result) => { subscription = result; }) + +------------------------------------------------------------------------------ + + +.. _eventhub_once: + +once +=================== + +.. code-block:: javascript + + eventHub.once(contractName, contractAddress, eventName, filterFunction, onEvent, fromBlock); + +subscribe to a contract event or a global EventHub event, remove subscription when filterFunction matched + +---------- +Parameters +---------- + +#. ``toRemove`` - ``any``: +#. ``contractAddress`` - ``string``: target contract address +#. ``eventName`` - ``string``: name of the event to subscribe to +#. ``filterFunction`` - ``function``: a function that returns true or a Promise that resolves to true if onEvent function should be applied +#. ``onEvent`` - ``function``: executed when event was fired and the filter matches, gets the event as its parameter +#. ``fromBlock`` - ``string`` (optional): get all events from this block, defaults to ``latest`` + +------- +Returns +------- + +``Promise`` resolves to ``string``: event subscription. + +------- +Example +------- + +.. code-block:: javascript + + // subscribe to the 'ContractEvent' at the EventHub located at '00000000000000000000000000000000deadbeef' + runtime.eventHub + .once( + 'EventHub', + '00000000000000000000000000000000deadbeef', + 'ContractEvent', + (event) => true, + (event) => { + console.dir(event) + } + ) + .then((result) => { subscription = result; }) + +------------------------------------------------------------------------------ + + +.. _eventhub_unsubscribe: + +unsubscribe +=================== + +.. code-block:: javascript + + eventHub.unsubscribe(toRemove); + +unsubscribe an event subscription + +---------- +Parameters +---------- + +#. ``contractName`` - ``string``: target contract name (must be available within |source contractLoader|_ ) + * ``subscription`` - ``string``: target guid for the subscription that should be removed + * ``contractId`` - ``string``: target contractId where all subscriptions should be removed (can be 'all') + +------- +Returns +------- + +``Promise`` resolves to ``void``: resolved when done. + +------- +Example +------- + +.. code-block:: javascript + + // subscribe to the 'ContractEvent' at the EventHub located at '00000000000000000000000000000000deadbeef' + runtime.eventHub + .unsubscribe({ + subscription: 'f0315d39-5e03-4e82-b765-df1c03037b3a' + }) + + +.. required for building markup + +.. |source contractLoader| replace:: ``ContractLoader`` +.. _source contractLoader: /contracts/contract-loader.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source nameResolver| replace:: ``NameResolver`` +.. _source nameResolver: /blockchain/name-resolver.html + +.. |source web3| replace:: ``Web3`` +.. _source web3: https://github.com/ethereum/web3.js/ diff --git a/docs/blockchain/executor.rst b/docs/blockchain/executor.rst new file mode 100644 index 00000000..7ffca8b1 --- /dev/null +++ b/docs/blockchain/executor.rst @@ -0,0 +1,347 @@ +================================================================================ +Executor +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Executor + * - Extends + - `Logger `_ + * - Source + - `executor.ts `_ + * - Tests + - `executor.spec.ts `_ + +The executor is used for + +- making contract calls +- executing contract transactions +- creating contracts +- send EVEs to another account or contract + +The signer requires you to have a contract instance, either by + +- loading the contract via `Description `_ helper (if the contract has an abi at its description) +- loading the contract via `ContractLoader `_ helper (if the contract has not abi at its description) +- directly via `web3.js `_. + + + +.. _executor_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new Executor(options); + +Creates a new Executor instance. + +The Executor allows to pass the ``defaultOptions`` property to its constructor. This property contains options for transactions and calls, that will be used if no other properties are provided in calls/transactions. Explicitly passed options always overwrite default options. + +---------- +Parameters +---------- + +#. ``options`` - ``ExecutorOptions``: options for ServiceContract constructor. + * ``config`` - ``any``: configuration object for the executor instance + * ``defaultOptions`` - ``any`` (optional): default options for web3 transactions/calls + * ``eventHub`` - |source eventHub|_: |source eventHub|_ instance + * ``signer`` - |source signerInterface|_: |source signerInterface|_ instance + * ``web3`` - |source web3|_: |source web3|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``Executor`` instance + +------- +Example +------- + +.. code-block:: typescript + + const executor = new Executor({ + config, + eventHub, + signer, + web3 + }); + + + +-------------------------------------------------------------------------------- + +.. _executor_init: + +init +=================== + +.. code-block:: javascript + + executor.init(name); + +initialize executor + +---------- +Parameters +---------- + +#. ``options`` - ``any``: object with the property "eventHub" (of the type EventHub) + * ``eventHub`` - ``EventHub``: The initialized EventHub Module. + +------- +Returns +------- + +``void``. + +------- +Example +------- + +.. code-block:: javascript + + runtime.executor.init({eventHub: runtime.eventHub}) + +------------------------------------------------------------------------------ + +.. _executor_executeContractCall: + +executeContractCall +=================== + +.. code-block:: javascript + + executor.executeContractCall(contract, functionName, ...args); + +gets contract from a solc compilation + +---------- +Parameters +---------- + +#. ``contract`` - ``any``: the target contract +#. ``functionName`` - ``string``: name of the contract function to call +#. ``...args`` - ``any[]``: optional array of arguments for contract call. if last arguments is {Object}, it is used as the options parameter + +------- +Returns +------- + +``Promise`` resolves to ``any``: contract calls result. + +------- +Example +------- + +.. code-block:: javascript + + const greetingMessage = await runtime.executor.executeContractCall( + contract, // web3.js contract instance + 'greet' // function name + ); + +------------------------------------------------------------------------------ + +.. _executor_executeContractTransaction: + +executeContractTransaction +========================== + +.. code-block:: javascript + + executor.executeContractTransaction(contract, functionName, inputOptions, ...functionArguments); + +execute a transaction against the blockchain, handle gas exceeded and return values from contract function + +---------- +Parameters +---------- + +#. ``contract`` - ``any``: contract instance +#. ``functionName`` - ``string``: name of the contract function to call +#. ``inputOptions`` - ``any``: options object + * ``from`` - ``string`` (optional): The address the call "transaction" should be made from. + * ``gas`` - ``number`` (optional): The amount of gas provided with the transaction. + * ``event`` - ``string`` (optional): The event to wait for a result of the transaction, + * ``getEventResult`` - ``function`` (optional): callback function which will be called when the event is triggered. + * ``eventTimeout`` - ``number`` (optional): timeout (in ms) to wait for a event result before the transaction is marked as error + * ``estimate`` - ``boolean`` (optional): Should the amount of gas be estimated for the transaction (overwrites ``gas`` parameter) + * ``force`` - ``string`` (optional): Forces the transaction to be executed. Ignores estimation errors + * ``autoGas`` - ``number`` (optional): enables autoGas 1.1 ==> adds 10% to estimated gas costs. value capped to current block. +#. ``...functionArguments`` - ``any[]``: optional arguments to pass to contract transaction + +------- +Returns +------- + +``Promise`` resolves to: ``no result`` (if no event to watch was given), ``the event`` (if event but no getEventResult was given), ``the`` value returned by getEventResult(eventObject). + +Because an estimation is performed, even if a fixed gas cost has been set, failing transactions are rejected before being executed. This protects users from executing transactions, that consume all provided gas and fail, which is usually not intended, especially if a large amount of gas has been provided. To prevent this behavior for any reason, add a ``force: true`` to the options, though it is **not advised to do so**. + +To allow to retrieve the result of a transaction, events can be used to receive values from a transaction. If an event is provided, the transaction will only be fulfilled, if the event is triggered. To use this option, the executor needs to have the ``eventHub`` property has to be set. Transactions, that contain event related options and are passed to an executor without an ``eventHub`` will be rejected immediately. + +------- +Example +------- + +.. code-block:: javascript + + const accountId = '0x...'; + const greetingMessage = await runtime.executor.executeContractTransaction( + contract, // web3.js contract instance + 'setData', // function name + { from: accountId, }, // perform transaction with this account + 123, // arguments after the options are passed to the contract + ); + +Provided gas is estimated automatically with a fault tolerance of 10% and then used as `gas` limit in the transaction. For a different behavior, set `autoGas` in the transaction options: + +.. code-block:: javascript + + const greetingMessage = await runtime.executor.executeContractTransaction( + contract, // web3.js contract instance + 'setData', // function name + { from: accountId, autoGas: 1.05, }, // 5% fault tolerance + 123, // arguments after the options are passed to the contract + ); + +or set a fixed gas limit: + +.. code-block:: javascript + + const greetingMessage = await runtime.executor.executeContractTransaction( + contract, // web3.js contract instance + 'setData', // function name + { from: accountId, gas: 100000, }, // fixed gas limit + 123, // arguments after the options are passed to the contract + ); + +Using events for getting return values: + +.. code-block:: javascript + + const contractId = await runtime.executor.executeContractTransaction( + factory, + 'createContract', { + from: accountId, + autoGas: 1.1, + event: { target: 'FactoryInterface', eventName: 'ContractCreated', }, + getEventResult: (event, args) => args.newAddress, + }, + ); + + +------------------------------------------------------------------------------ + + + +.. _executor_executeSend: + +executeSend +=================== + +.. code-block:: javascript + + executor.executeSend(options); + +send EVEs to target account + +---------- +Parameters +---------- + +#. ``options`` - ``any``: the target contract + * ``from`` - ``string``: The address the call "transaction" should be made from. + * ``to`` - ``string``: The address where the eve's should be send to. + * ``value`` - ``number``: Amount to send in Wei + +------- +Returns +------- + +``Promise`` resolves to ``void``: resolved when done. + +------- +Example +------- + +.. code-block:: javascript + + await runtime.executor.executeSend({ + from: '0x...', // send from this account + to: '0x...', // receiving account + value: web3.utils.toWei('1'), // amount to send in Wei + }); + +------------------------------------------------------------------------------ + + +.. _executor_createContract: + +createContract +=================== + +.. code-block:: javascript + + executor.createContract(contractName, functionArguments, options); + +creates a contract by contstructing creation transaction and signing it with private key of options.from + +---------- +Parameters +---------- + +#. ``contractName`` - ``string``: contract name (must be available withing contract loader module) +#. ``functionArguments`` - ``any[]``: arguments for contract creation, pass empty Array if no arguments +#. ``options`` - ``any``: options object + * ``from`` - ``string``: The address the call "transaction" should be made from. + * ``gas`` - ``number``: Provided gas amout for contract creation. + +------- +Returns +------- + +``Promise`` resolves to ``any``: new contract. + +------- +Example +------- + +.. code-block:: javascript + + const newContractAddress = await runtime.executor.createContract( + 'Greeter', // contract name + ['I am a demo greeter! :3'], // constructor arguments + { from: '0x...', gas: 100000, }, // gas has to be provided with a fixed value + ); + + + +.. required for building markup + + +.. |source signerInterface| replace:: ``SignerInterface`` +.. _source signerInterface: /blockchain/signer.html + +.. |source eventHub| replace:: ``EventHub`` +.. _source eventHub: /blockchain/event-hub.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source web3| replace:: ``Web3`` +.. _source web3: https://github.com/ethereum/web3.js/ \ No newline at end of file diff --git a/docs/blockchain/index.rst b/docs/blockchain/index.rst new file mode 100644 index 00000000..07783a61 --- /dev/null +++ b/docs/blockchain/index.rst @@ -0,0 +1,22 @@ +================================================================================ +Blockchain +================================================================================ + +.. toctree:: + :glob: + :maxdepth: 1 + + executor + signer + account-store + event-hub + name-resolver + description + +This section contains modules, that deal with basic blockchain interactions, like + +- performing calls and transaction to contracts +- signing transactions +- handle account private keys +- use on-chain services, like ENS name resolver and event emitting contracts +- describe contracts and ENS addresses \ No newline at end of file diff --git a/docs/blockchain/name-resolver.rst b/docs/blockchain/name-resolver.rst new file mode 100644 index 00000000..7cf0addc --- /dev/null +++ b/docs/blockchain/name-resolver.rst @@ -0,0 +1,588 @@ +================================================================================ +Name Resolver +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - NameResolver + * - Extends + - `Logger `_ + * - Source + - `name-resolver.ts `_ + * - Tests + - `name-resolver.spec.ts `_ + +The `NameResolver `_ is a collection of helper functions, that can be used for ENS interaction. These include: + +- setting and getting ENS addresses +- setting and getting ENS content flags, which is used when setting data in distributed file system, especially in case of setting a description for an `ENS` address + +.. _name_resolver_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new NameResolver(options); + +Creates a new NameResolver instance. + +---------- +Parameters +---------- + +#. ``options`` - ``NameResolverOptions``: options for NameResolver constructor. + * ``config`` - ``any``: configuration object for the NameResolver instance + * ``executor`` - |source executor|_: |source executor|_ instance + * ``contractLoader`` - |source contractLoader|_: |source contractLoader|_ instance + * ``web3`` - |source web3|_: |source web3|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``NameResolver`` instance + +------- +Example +------- + +.. code-block:: typescript + + const nameResolver = new NameResolver({ + cryptoProvider, + dfs, + executor, + keyProvider, + nameResolver, + contractLoader, + web3, + }); + + + +-------------------------------------------------------------------------------- + +.. _name_resolver_getAddressOrContent: + +getAddressOrContent +=================== + +.. code-block:: javascript + + nameResolver.getAddressOrContent(name, type); + +get address or content of an ens entry + +---------- +Parameters +---------- + +#. ``name`` - ``string``: ens domain name (plain text) +#. ``type`` - ``string``: content type to get (address or content) + +------- +Returns +------- + +``Promise`` resolves to ``string``: address, returns null if not available + +------- +Example +------- + +.. code-block:: javascript + + const testEvanAddress = await runtime.nameResolver.getAddressOrContent('test.evan', 'address'); + // returns 0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49 + +------------------------------------------------------------------------------ + +.. _name_resolver_getAddress: + +getAddress +=================== + +.. code-block:: javascript + + nameResolver.getAddress(name); + +get address of an ens entry + +---------- +Parameters +---------- + +#. ``name`` - ``string``: ens domain name (plain text) + +------- +Returns +------- + +``Promise`` resolves to ``string``: address, returns null if not available + +------- +Example +------- + +.. code-block:: javascript + + const testEvanAddress = await runtime.nameResolver.getAddress('test.evan'); + // returns 0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49 + +------------------------------------------------------------------------------ + +.. _name_resolver_getContent: + +getContent +=================== + +.. code-block:: javascript + + nameResolver.getContent(name); + +get content of an ens entry + +---------- +Parameters +---------- + +#. ``name`` - ``string``: ens domain name (plain text) + +------- +Returns +------- + +``Promise`` resolves to ``string``: content, returns null if not available + +------- +Example +------- + +.. code-block:: javascript + + const testEvanAddress = await runtime.nameResolver.getContent('test.evan'); + // returns (encoded ipfs hash) 0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49 + +------------------------------------------------------------------------------ + +.. _name_resolver_setAddressOrContent: + +setAddressOrContent +=================== + +.. code-block:: javascript + + nameResolver.setAddressOrContent(name, value, accountId, domainOwnerId, type); + +set ens name. this can be a root level domain domain.test or a subdomain sub.domain.test + +---------- +Parameters +---------- + +#. ``name`` - ``string``: ens domain name (plain text) +#. ``value`` - ``string``: ethereum address +#. ``accountId`` - ``string``: owner of the parent domain +#. ``domainOwnerId`` - ``string``: owner of the address to set +#. ``type`` - ``string``: content type to set + +------- +Returns +------- + +``Promise`` resolves to ``void``: resolves when done + +------- +Example +------- + +.. code-block:: javascript + + const testEvanAddress = await runtime.nameResolver + .setAddressOrContent( + 'test.evan', + '0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49', + '0x000000000000000000000000000000000000beef', + '0x000000000000000000000000000000000000beef', + 'address' + ); + // returns (encoded ipfs hash) 0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49 + +------------------------------------------------------------------------------ + +.. _name_resolver_setAddress: + +setAddress +=================== + +.. code-block:: javascript + + nameResolver.setAddress(name, address, accountId, domainOwnerId); + +set address for ens name. this can be a root level domain domain.test or a subdomain sub.domain.test + +---------- +Parameters +---------- + +#. ``name`` - ``string``: ens domain name (plain text) +#. ``address`` - ``string``: ethereum address +#. ``accountId`` - ``string``: owner of the parent domain +#. ``domainOwnerId`` - ``string``: owner of the address to set + +------- +Returns +------- + +``Promise`` resolves to ``void``: resolves when done + +------- +Example +------- + +.. code-block:: javascript + + const testEvanAddress = await runtime.nameResolver + .setAddress( + 'test.evan', + '0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49', + '0x000000000000000000000000000000000000beef', + '0x000000000000000000000000000000000000beef' + ); + +------------------------------------------------------------------------------ + + +.. _name_resolver_setContent: + +setContent +=================== + +.. code-block:: javascript + + nameResolver.setContent(name, content, accountId, domainOwnerId); + +set content for ens name. this can be a root level domain domain.test or a subdomain sub.domain.test + +---------- +Parameters +---------- + +#. ``name`` - ``string``: ens domain name (plain text) +#. ``content`` - ``string``: ethereum address +#. ``accountId`` - ``string``: owner of the parent domain +#. ``domainOwnerId`` - ``string``: owner of the address to set + +------- +Returns +------- + +``Promise`` resolves to ``void``: resolves when done + +------- +Example +------- + +.. code-block:: javascript + + const testEvanAddress = await runtime.nameResolver + .setContent( + 'test.evan', + '0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49', + '0x000000000000000000000000000000000000beef', + '0x000000000000000000000000000000000000beef' + ); + +------------------------------------------------------------------------------ + +.. _name_resolver_getFactory: + +getFactory +=================== + +.. code-block:: javascript + + nameResolver.getFactory(contractName); + +helper function for retrieving a factory address (e.g. 'tasks.factory.evan') + +---------- +Parameters +---------- + +#. ``contractName`` - ``string``: name of the contract that is created by the factory + +------- +Returns +------- + +``string``: address of the contract factory + +------- +Example +------- + +.. code-block:: javascript + + const taskFactory = await runtime.nameResolver.getFactory('tasks'); + // returns '0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49'; + +------------------------------------------------------------------------------ + +.. _name_resolver_getDomainName: + +getDomainName +=================== + +.. code-block:: javascript + + nameResolver.getDomainName(domainConfig, ...subLabels); + +builds full domain name based on the provided domain config a module initalization. + +---------- +Parameters +---------- + +#. ``domainConfig`` - ``string[] | string``: The domain configuration +#. ``...subLabels`` - ``string[]``: array of domain elements to be looked up and added at the lefthand + +------- +Returns +------- + +``string``: the domain name + +------- +Example +------- + +.. code-block:: javascript + + const domain = runtime.nameResolver.getDomainName(['factory', 'root'], 'task'); + // returns 'task.factory.evan'; + +------------------------------------------------------------------------------ + +.. _name_resolver_getArrayFromIndexContract: + +getArrayFromIndexContract +========================= + +.. code-block:: javascript + + nameResolver.getArrayFromIndexContract(indexContract, listHash, retrievers, chain, triesLeft); + +retrieve an array with all values of a list from an index contract. + +---------- +Parameters +---------- + +#. ``indexContract`` - ``any``: Ethereum contract address (DataStoreIndex) +#. ``listHash`` - ``string``: bytes32 namehash like api.nameResolver.sha3('ServiceContract') +#. ``retrievers`` - ``any`` (optional): overwrites for index or index like contract property retrievals defaults to: + +.. code-block:: javascript + + { + listEntryGet: 'listEntryGet', + listLastModified: 'listLastModified', + listLength: 'listLength', + } + +#. ``chain`` - ``Promise``: Promise, for chaining multiple requests (should be omitted when called 'from outside', defaults to Promise.resolve()) +#. ``triesLeft`` - ``number``: tries left before quitting defaults to ``10`` + +------- +Returns +------- + +``Promise`` resolves to ``string[]``: list of addresses + +------------------------------------------------------------------------------ + +.. _name_resolver_getArrayFromListContract: + +getArrayFromListContract +======================== + +.. code-block:: javascript + + nameResolver.getArrayFromListContract(indexContract, count, offset, reverse, chain, triesLeft); + +retrieve an array with all values of a list from an index contract. + +---------- +Parameters +---------- + +#. ``indexContract`` - ``any``: Ethereum contract address (DataStoreIndex) +#. ``count`` - ``number`` (optional): how many items should be returned, defaults to ``10`` +#. ``offset`` - ``number`` (optional): how many items should be skipped, defaults to ``0`` +#. ``reverse`` - ``boolean`` (optional): should the list be iterated reverse, defaults to ``false`` +#. ``chain`` - ``Promise`` (optional): Promise, for chaining multiple requests (should be omitted when called 'from outside', defaults to Promise.resolve()) +#. ``triesLeft`` - ``number`` (optional): tries left before quitting defaults to ``10`` + +------- +Returns +------- + +``Promise`` resolves to ``string[]``: list of addresses + +------------------------------------------------------------------------------ + +.. _name_resolver_getArrayFromUintMapping: + +getArrayFromUintMapping +======================= + +.. code-block:: javascript + + nameResolver.getArrayFromUintMapping(contract, countRetriever, elementRetriever[, count, offset, reverse]); + +retrieve elements from a contract using a count and element retriever function. + +---------- +Parameters +---------- + +#. ``contract`` - ``any``: Ethereum contract address (DataStoreIndex) +#. ``countRetriever`` - ``Function`` : function which returns the count of the retrieved elements +#. ``elementRetriever`` - ``Function`` : function which returns the element of the retrieved elements +#. ``count`` - ``number`` (optional): number of elements to retrieve, defaults to ``10`` +#. ``offset`` - ``number`` (optional): skip this many items when retrieving, defaults to ``0`` +#. ``reverse`` - ``boolean`` (optional): retrieve items in reverse order, defaults to ``false`` + +------- +Returns +------- + +``Promise`` resolves to ``string[]``: list of addresses + +------------------------------------------------------------------------------ + +.. _name_resolver_sha3: + +sha3 +=================== + +.. code-block:: javascript + + nameResolver.sha3(input); + +sha3 hashes an input, substitutes web3.utils.sha3 from geth console + +---------- +Parameters +---------- + +#. ``input`` - ``string | buffer``: input text or buffer to hash + +------- +Returns +------- + +``string``: hashed output + +------------------------------------------------------------------------------ + +.. _name_resolver_soliditySha3: + +soliditySha3 +=================== + +.. code-block:: javascript + + nameResolver.soliditySha3(...args); + +Will calculate the sha3 of given input parameters in the same way solidity would. This means arguments will be ABI converted and tightly packed before being hashed. + +---------- +Parameters +---------- + +#. ``args`` - ``string | buffer``: arguments for hashing + +------- +Returns +------- + +``string``: hashed output + +------------------------------------------------------------------------------ + +.. _name_resolver_namehash: + +namehash +=================== + +.. code-block:: javascript + + nameResolver.namehash(inputName); + +hash ens name for usage in contracts + +---------- +Parameters +---------- + +#. ``inputName`` - ``string``: inputName ens name to hash + +------- +Returns +------- + +``string``: name hash + +------------------------------------------------------------------------------ + +.. _name_resolver_bytes32ToAddress: + +bytes32ToAddress +=================== + +.. code-block:: javascript + + nameResolver.bytes32ToAddress(hash); + +converts a bytes32 hash to address + +---------- +Parameters +---------- + +#. ``hash`` - ``string``: bytes32 hash + +------- +Returns +------- + +``string``: converted address + + +.. required for building markup + +.. |source executor| replace:: ``Executor`` +.. _source executor: /blockchain/executor.html + +.. |source contractLoader| replace:: ``ContractLoader`` +.. _source contractLoader: /contracts/contract-loader.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source web3| replace:: ``Web3`` +.. _source web3: https://github.com/ethereum/web3.js/ \ No newline at end of file diff --git a/docs/blockchain/signer.rst b/docs/blockchain/signer.rst new file mode 100644 index 00000000..cafd5bf3 --- /dev/null +++ b/docs/blockchain/signer.rst @@ -0,0 +1,308 @@ +================================================================================ +Signer +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - SignerInternal + * - Implements + - `SignerInterface `_ + * - Extends + - `Logger `_ + * - Source + - `signer-internal.ts `_ + +The signers are used to create contract transactions and are used internally by the `Executor `_. The default runtime uses the `SignerInternal `_ helper to sign transaction. + +In most cases, you won't have to use the Signer objects directly yourself, as the `Executor `_ is your entry point for performing contract transactions. + +------------------------------------------------------------------------------ + +.. _signer_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new SignerInternal(options); + +Creates a new SignerInternal instance. + +---------- +Parameters +---------- + +#. ``options`` - ``SignerInternalOptions``: options for SignerInternal constructor. + * ``accountStore`` - |source keyStoreinterface|_: |source keyStoreinterface|_ instance + * ``config`` - ``any``: signer internal configuration + * ``contractLoader`` - |source contractLoader|_: |source contractLoader|_ instance + * ``web3`` - |source web3|_: |source web3|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``SignerInternal`` instance + +------- +Example +------- + +.. code-block:: typescript + + const signer = new SignerInternal({ + accountStore, + config, + contractLoader, + web3 + }); + +------------------------------------------------------------------------------ + +.. _signer_getPrivateKey: + +getPrivateKey +=================== + +.. code-block:: javascript + + signer.getPrivateKey(accountId); + +retrieve private key for given account + +---------- +Parameters +---------- + +#. ``accountId`` - ``string``: eth accountId + +------- +Returns +------- + +``Promise`` resolves to ``string``: private key of given account. + +------- +Example +------- + +.. code-block:: javascript + + const privateKey = await runtime.signer.getPrivateKey('0x00000000000000000000000000000000deadbeef'); + +------------------------------------------------------------------------------ + +.. _signer_ensureHashWithPrefix: + +ensureHashWithPrefix +==================== + +.. code-block:: javascript + + signer.ensureHashWithPrefix(input); + +patch '0x' prefix to input if not already added, also casts numbers to hex string + +---------- +Parameters +---------- + +#. ``input`` - ``string``: input to prefix with '0x' + +------- +Returns +------- + +``string``: patched input. + +------- +Example +------- + +.. code-block:: javascript + + const patchedInput = runtime.signer.ensureHashWithPrefix('00000000000000000000000000000000deadbeef'); + // returns 0x00000000000000000000000000000000deadbeef + +------------------------------------------------------------------------------ + +.. _signer_getGasPricex: + +getGasPrice +=================== + +.. code-block:: javascript + + signer.getGasPrice(); + +get gas price (either from config or from api.eth.web3.eth.gasPrice (gas price median of last blocks) or api.config.eth.gasPrice; unset config value or set it to falsy for median gas price + +------- +Returns +------- + +``string``: hex string with gas price. + +------- +Example +------- + +.. code-block:: javascript + + const gasPrice = await runtime.signer.getGasPrice(); + // returns 0x4A817C800 + +------------------------------------------------------------------------------ + +.. _signer_getNonce: + +getNonce +=================== + +.. code-block:: javascript + + signer.getNonce(accountId); + +gets nonce for current user, looks into actions submitted by current user in current block for this as well + +---------- +Parameters +---------- + +#. ``accountId`` - ``string``: Ethereum account ID + +------- +Returns +------- + +``number``: nonce of given user. + +------- +Example +------- + +.. code-block:: javascript + + const patchedInput = runtime.signer.getNonce('00000000000000000000000000000000deadbeef'); + // returns 10 + +------------------------------------------------------------------------------ + +.. _signer_signAndExecuteSend: + +signAndExecuteSend +=================== + +.. code-block:: javascript + + signer.signAndExecuteSend(options, handleTxResult); + +signs the transaction from `executor.executeSend `_ and publishes to the network + +---------- +Parameters +---------- + +#. ``options`` - ``any``: + * ``from`` - ``string``: The address the call "transaction" should be made from. + * ``to`` - ``string``: The address where the eve's should be send to. + * ``value`` - ``number``: Amount to send in Wei +#. ``handleTxResult`` - ``function(error, receipt)``: callback when transaction receipt is available or error + +------- +Example +------- + +.. code-block:: javascript + + const patchedInput = runtime.signer.signAndExecuteSend({ + from: '0x...', // send from this account + to: '0x...', // receiving account + value: web3.utils.toWei('1'), // amount to send in Wei + }, (err, receipt) => { + console.dir(arguments); + }); + +------------------------------------------------------------------------------ + +.. _signer_signAndExecuteTransaction: + +signAndExecuteTransaction +========================= + +.. code-block:: javascript + + signer.signAndExecuteTransaction(contract, functionName, functionArguments, options, handleTxResult); + +signs the transaction from `executor.executeContractTransaction `_ and publishes to the network + +---------- +Parameters +---------- + +#. ``contract`` - ``any``: contract instance from api.eth.loadContract(...) +#. ``functionName`` - ``string``: function name +#. ``functionArguments`` - ``any[]``: arguments for contract creation, pass empty Array if no arguments +#. ``options`` - ``any``: + * ``from`` - ``string``: The address the call "transaction" should be made from. + * ``gas`` - ``number``: Amount of gas to attach to the transaction + * ``to`` - ``string`` (optional): The address where the eve's should be send to. + * ``value`` - ``number`` (optional): Amount to send in Wei +#. ``handleTxResult`` - ``function(error, receipt)``: callback when transaction receipt is available or error + + +------------------------------------------------------------------------------ + +.. _signer_createContract: + +createContract +=================== + +.. code-block:: javascript + + signer.createContract(contractName, functionArguments, options); + +signs the transaction from `executor.createContract `_ and publishes to the network + +---------- +Parameters +---------- + +#. ``contractName`` - ``any``: contractName from contractLoader +#. ``functionArguments`` - ``any[]``: arguments for contract creation, pass empty Array if no arguments +#. ``options`` - ``any``: + * ``from`` - ``string``: The address the call "transaction" should be made from. + * ``gas`` - ``number``: Amount of gas to attach to the transaction + +------- +Returns +------- + +``Promise`` resolves to ``void``: resolved when done. + + +.. required for building markup + + +.. |source contractLoader| replace:: ``ContractLoader`` +.. _source contractLoader: /contracts/contract-loader + +.. |source keyStoreinterface| replace:: ``KeyStoreInterface`` +.. _source keyStoreinterface: /blockchain/account-store.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source web3| replace:: ``Web3`` +.. _source web3: https://github.com/ethereum/web3.js/ \ No newline at end of file diff --git a/docs/common/index.rst b/docs/common/index.rst new file mode 100644 index 00000000..3a7c66ff --- /dev/null +++ b/docs/common/index.rst @@ -0,0 +1,13 @@ +================================================================================ +Common +================================================================================ + +.. toctree:: + :glob: + :maxdepth: 1 + + logger + validator + utils + +This section contains modules, that are used througout the code, the ``Logger`` for example is the base class for almost every module and enables them to log in a structured manner. diff --git a/docs/common/logger.rst b/docs/common/logger.rst new file mode 100644 index 00000000..66f9cdb7 --- /dev/null +++ b/docs/common/logger.rst @@ -0,0 +1,187 @@ +================================================================================ +Logger +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Logger + * - Source + - `logger.ts `_ + +The `Logger `_ class is used throughout the package for logging events, updates and errors. Logs can be written by classes, that inherit from the `Logger `_ class, by using the `this.log` function. A log level can be set by its second parameter: + +.. code-block:: javascript + + this.log('hello log', 'debug'); + + All log messages without a level default to level 'info'. If not configured otherwise, the following behavior is used: + +- drop all log messages but errors +- log errors to console.error + +It can be useful for analyzing issues to increase the log level. You can do this in two ways: + +- Set the environment variable `DBCP_LOGLEVEL` to a level matching your needs, which increases the log level for all modules and works with the default runtime. For example: + +.. code-block:: sh + + export DBCP_LOGLEVEL=info + + +- When creating a custom runtime, set the `logLevel` property to a value matching your needs, when creating any module instance. This allows you to change log level for single modules, but requires you to create a custom runtime, e.g.: + +.. code-block:: javascript + + const { ContractLoader } = require('@evan.network/dbcp'); + const Web3 = require('web3'); + + // web3 instance for ContractLoader + const web3 = new Web3(); + web3.setProvider(new web3.providers.WebsocketProvider('...')); + + // custom log level 'info' + const contractLoader = new ContractLoader({ web3, logLevel: 'info', }); + +All loggers can have a custom LogLog storage where all logmessages with a given level will be stored. You can access the storage for the current logger module at the property ``logLog``. All messages are stored with the following markup: + +.. code-block:: javascript + + { + timestamp, // current date in millis + level, // loglevel of the message + message // log message + } + +You can configure the current LogLogLevel at the property ``logLogLevel`` at instantiation of your module. + +------------------------------------------------------------------------------ + +.. _logger_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new Logger(options); + +Creates a new Logger instance. + +---------- +Parameters +---------- + +#. ``options`` - ``LoggerOptions``: options for Logger constructor. + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``Logger`` instance + +------- +Example +------- + +.. code-block:: typescript + + const logger = new Logger(); + + + +-------------------------------------------------------------------------------- + +.. _logger_log: + +log +=================== + +.. code-block:: javascript + + logger.log(message, level); + +log message with given level + + + +---------- +Parameters +---------- + +#. ``message`` - ``string``: log message +#. ``level`` - ``string``: log level as string, defaults to 'info' + +------- +Example +------- + +.. code-block:: javascript + + runtime.executor.log('test', 'error'); + +------------------------------------------------------------------------------ + += Additional Components = +========================= + +----------- +Interfaces +----------- + + + +.. _logger_logLogInterface: + + +LogLogInterface +^^^^^^^^^^^^^^^ + +A different LogLog storage can be attached to the logger instance of the module. The storage must implement the following functions (default array like instance) + +.. code-block:: typescript + + export interface LogLogInterface { + push: Function; + map: Function; + filter: Function; + }; + +----- +Enums +----- + +.. _logger_LogLevel: + +LogLevel +^^^^^^^^^^^^^ + +Available LogLevels for the logger instance, free definable between error and gasLog + +.. code-block:: typescript + + export enum LogLevel { + debug, + info, + notice, + warning, + error, + + gasLog = 100, + disabled = 999, + }; + + +.. required for building markup + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface \ No newline at end of file diff --git a/docs/common/utils.rst b/docs/common/utils.rst new file mode 100644 index 00000000..2a964fd4 --- /dev/null +++ b/docs/common/utils.rst @@ -0,0 +1,86 @@ +================================================================================ +Utils +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Source + - `utils.ts `_ + +Utils contain helper functions which are used across the whole project + +------------------------------------------------------------------------------ + +.. _utils_promisify: + +promisify +=================== + +.. code-block:: javascript + + utils.promisify(funThis, functionName, ...args); + +run given function from this, use function(error, result) {...} callback for promise resolve/reject + + + +---------- +Parameters +---------- + +#. ``funThis`` - ``any``: the functions 'this' object +#. ``functionName`` - ``string``: name of the contract function to call +#. ``...args`` - ``any``: any addtional parameters that should be passed to the called function + +------- +Returns +------- + +Promise resolves to ``any``: the result from the function(error, result) {...} callback. + +------- +Example +------- + +.. code-block:: javascript + + runtime.utils + .promisify(fs, 'readFile', 'somefile.txt') + .then(content => console.log('file content: ' + content)) + +------------------------------------------------------------------------------ + +.. _utils_obfuscate: + +obfuscate +=================== + +.. code-block:: javascript + + utils.obfuscate(text); + +obfuscates strings by replacing each character but the last two with 'x' + + +---------- +Parameters +---------- + +#. ``text`` - ``string``: text to obfuscate + +------- +Returns +------- + +``string``: obfuscated text + +------- +Example +------- + +.. code-block:: javascript + + const obfuscated = runtime.utils.obfuscate('sample text'); + // returns 'sample texx' \ No newline at end of file diff --git a/docs/common/validator.rst b/docs/common/validator.rst new file mode 100644 index 00000000..a49685f5 --- /dev/null +++ b/docs/common/validator.rst @@ -0,0 +1,116 @@ +================================================================================ +Validator +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Validator + * - Extends + - `Logger `_ + * - Source + - `validator.ts `_ + * - Tests + - `validator.spec.ts `_ + +The Validator module can be used to verfiy given JSON schemas. + +------------------------------------------------------------------------------ + +.. _validator_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new Validator(options); + +Creates a new Validator instance. + +---------- +Parameters +---------- + +#. ``options`` - ``ValidatorOptions``: options for Validator constructor. + * ``schema`` - ``any``: the validation schema definition + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``Validator`` instance + +------- +Example +------- + +.. code-block:: typescript + + const nameResolver = new Validator({ + schema + }); + + + +-------------------------------------------------------------------------------- + +.. _validator_validate: + +validate +=================== + +.. code-block:: javascript + + validator.validate(data); + +validate a given data object with the instantiated schema + + + +---------- +Parameters +---------- + +#. ``data`` - ``any``: to be validated data + +------- +Returns +------- + +``bool | strings[]``: true if data is valid, array of object if validation is failed + +------------------------------------------------------------------------------ + +.. _validator_getErrorsAsText: + +getErrorsAsText +=================== + +.. code-block:: javascript + + validator.getErrorsAsText(); + +returns errors as text if previous validation was failed + + +------- +Returns +------- + +``string``: all previous validation errors concatenated as readable string + + +.. required for building markup + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..03ce9979 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = u'blockchain-core' +copyright = u'2018, evan.network GmbH' +author = u'evan.network GmbH' + +# The short X.Y version +version = u'' +# The full version, including alpha/beta/rc tags +release = u'' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store', 'template.rst'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'blockchain-coredoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'blockchain-core.tex', u'blockchain-core Documentation', + u'even.network GmbH', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'blockchain-core', u'blockchain-core Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'blockchain-core', u'blockchain-core Documentation', + author, 'blockchain-core', 'One line description of project.', + 'Miscellaneous'), +] diff --git a/docs/contracts/base-contract.rst b/docs/contracts/base-contract.rst new file mode 100644 index 00000000..88d9c45c --- /dev/null +++ b/docs/contracts/base-contract.rst @@ -0,0 +1,351 @@ +================================================================================ +Base Contract +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - BaseContract + * - Extends + - `Logger `_ + * - Source + - `base-contract.ts `_ + * - Tests + - `base-contract.spec.ts `_ + +The `BaseContract `_ is the base contract class used for + +* :doc:`DataContracts ` +* `ServiceContractss <#servicecontract>`_ + +Contracts, that inherit from ``BaseContracts``, are able to: + +* manage a list of contract participants (called "members") +* manage the own state (a flag, that indicate its own life cycle status) +* manage members state (a flag, that indicate the members state in the contract) + +What members can do, what non-members cannot do depends of the implementatin of the inheriting contracts. + + +-------------------------------------------------------------------------------- + +.. _base-contract_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new BaseContract(options); + +Creates a new BaseContract instance. + +---------- +Parameters +---------- + +#. ``options`` - ``BaseContractOptions``: options for BaseContract constructor. + * ``executor`` - |source executor|_: |source executor|_ instance + * ``loader`` - |source contractLoader|_: |source contractLoader|_ instance + * ``nameResolver`` - |source nameResolver|_: |source nameResolver|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``BaseContract`` instance + +------- +Example +------- + +.. code-block:: typescript + + const baseContract = new BaseContract({ + executor, + loader, + nameResolver, + }); + + +.. _base-contract_createUninitialized: + +createUninitialized +================================================================================ + +.. code-block:: typescript + + baseContract.createUninitialized(factoryName, accountId[, businessCenterDomain, descriptionDfsHash]); + +Create new contract but do not initialize it yet. + +The API supports creating contracts, that inhert from ``BaseContract``. This is done by calling the respective factory. The factory calls are done via a function with this interface: + +.. code-block:: typescript + + /// @notice create new contract instance + /// @param businessCenter address of the BusinessCenter to use or 0x0 + /// @param provider future owner of the contract + /// @param _contractDescription DBCP definition of the contract + /// @param ensAddress address of the ENS contract + function createContract( + address businessCenter, + address provider, + bytes32 contractDescription, + address ensAddress) public returns (address); + +The API supports creating contracts with this function. Contracts created this way may not be ready to use and require an additional function at the contract to be called before usage. This function is usually called ``init`` and its arguments and implementation depends of the specific contract. + +The ``createUninitialized`` function performs a lookup for the respective factory contract and calls the ``createContract`` function at it. + +---------- +Parameters +---------- + +#. ``factoryName`` - ``string``: contract factory name, used for ENS lookup; if the factory name contains periods, it is threaded as an absolute ENS domain and used as such, if not it will be used as ``${factoryName}.factory.${businessCenterDomain}`` +#. ``accountId`` - ``string``: Ethereum account id +#. ``businessCenterDomain`` - ``string`` (optional): business center in which the contract will be created; use ``null`` when working without business center +#. ``descriptionDfsHash`` - ``string`` (optional): bytes32 hash for description in dfs + +------- +Returns +------- + +``Promise`` returns ``string``: Ethereum id of new contract + +------- +Example +------- + +.. code-block:: typescript + + const contractOwner = '0x...'; + const businessCenterDomain = 'testbc.evan'; + const contractId = await baseContract.createUninitialized( + 'testdatacontract', // factory name + contractOwner, // account, that will be owner of the new contract + businessCenterDomain, // business center, where the new contract will be created + ); + + +-------------------------------------------------------------------------------- + +.. _base-contract_inviteToContract: + +inviteToContract +================================================================================ + +.. code-block:: javascript + + baseContract.inviteToContract(businessCenterDomain, contract, inviterId, inviteeId); + +Invite user to contract. +To allow accounts to work with contract resources, they have to be added as members to the contract. This function does exactly that. + + +---------- +Parameters +---------- + +#. ``businessCenterDomain`` - ``string`` : ENS domain name of the business center the contract was created in; use null when working without business center +#. ``contract`` - ``string`` : Ethereum id of the contract +#. ``inviterId`` - ``string`` : account id of inviting user +#. ``inviteeId`` - ``string`` : account id of invited user + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: javascript + + const contractOwner = '0x0000000000000000000000000000000000000001'; + const invitee = '0x0000000000000000000000000000000000000002'; + const businessCenterDomain = 'testbc.evan'; + const contract = loader.loadContract('BaseContractInterface', contractId); + await baseContract.inviteToContract( + businessCenterDomain, + contractId, + contractOwner, + invitee, + ); + + +To check if an account is a member of a contract, the contract function ``isMember`` can be used: + +.. code-block:: typescript + + const isMember = await executor.executeContractCall(contract, 'isConsumer', invitee); + console.log(isMember); + // Output: + // true + + +-------------------------------------------------------------------------------- + +.. _base-contract_changeConsumerState: + +changeConsumerState +=================== + +.. code-block:: javascript + + baseContract.changeContractState(contract, accountId, consumerId, state); + +set state of a consumer. +A members state reflects this members status in the contract. These status values can for example be be Active, Draft or Terminated. + +---------- +Parameters +---------- + +#. ``contract`` - ``string|any``: contract instance or contract id +#. ``accountId`` - ``string``: Ethereum account id +#. ``consumerId`` - ``string``: Ethereum account id +#. ``state`` - |source consumerState|_: new state + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: javascript + + await baseContract.changeConsumerState(contractId, accountId, consumerId, ConsumerState.Active); + +|source consumerState|_ is an enum in the BaseContract class, that holds the same state values as the `BaseContract.sol `_. Alternatively integer values matching the enum in `BaseContractInterface.sol `_ can be used. + + + +-------------------------------------------------------------------------------- + +.. _base-contract_changeContractState: + +changeContractState +===================== + +.. code-block:: javascript + + baseContract.changeContractState(contract, accountId, state); + +Set state of the contract. +The contracts state reflects the current state and how other members may be able to interact with it. So for example, a contract for tasks cannot have its tasks resolved, when the contract is still in Draft state. State transitions are limited to configured roles and allow going from one state to another only if configured for this role. + +---------- +Parameters +---------- + +#. ``contract`` - ``string|any``: contract instance or contract id +#. ``accountId`` - ``string``: Ethereum account id +#. ``state`` - |source contractState|_: new state + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await baseContract.changeContractState(contractId, contractOwner, ContractState.Active); + + +|source contractState|_ is an enum in the BaseContract class, that holds the same state values as the `BaseContract.sol `_. Alternatively integer values matching the enum in `BaseContractInterface.sol `_ can be used. + + + +------------------------------------------------------------------------------ + +Additional Components +====================== + +----- +Enums +----- + +.. _base-contract_ContractState: + +ContractState +^^^^^^^^^^^^^ + +Describes contracts overall state. + +In most cases, this property can only be set by the contract owner. + +.. code-block:: typescript + + export enum ContractState { + Initial, + Error, + Draft, + PendingApproval, + Approved, + Active, + VerifyTerminated, + Terminated, + }; + +.. _base-contract_ConsumerState: + +ConsumerState +^^^^^^^^^^^^^ + +Describes the state of a consumer or owner in a contract. + +In most cases, this can be set the the member, thats status is updated or by a more privileged role, like a contract owner. + +.. code-block:: typescript + + export enum ConsumerState { + Initial, + Error, + Draft, + Rejected, + Active, + Terminated + }; + + + +.. required for building markup + +.. |source consumerState| replace:: ``ConsumerState`` +.. _source consumerState: /contracts/base-contract.html#base-contract-consumerstate + +.. |source contractLoader| replace:: ``ContractLoader`` +.. _source contractLoader: /contracts/contract-loader.html + +.. |source contractState| replace:: ``ContractState`` +.. _source contractState: /contracts/base-contract.html#base-contract-contractstate + +.. |source executor| replace:: ``Executor`` +.. _source executor: /blockchain/executor.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source nameResolver| replace:: ``NameResolver`` +.. _source nameResolver: /blockchain/name-resolver.html diff --git a/docs/contracts/contract-loader.rst b/docs/contracts/contract-loader.rst new file mode 100644 index 00000000..a9b0db8c --- /dev/null +++ b/docs/contracts/contract-loader.rst @@ -0,0 +1,120 @@ +================================================================================ +Contract Loader +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - ContractLoader + * - Extends + - `Logger `_ + * - Source + - `contract-loader.ts `_ + * - Tests + - `contract-loader.spec.ts `_ + +The `ContractLoader `_ is used when loading contracts without a DBCP description or when creating new contracts via bytecode. In both cases additional information has to be passed to the `ContractLoader `_ constructor. + +Loading contracts requires an abi interface as a JSON string and creating new contracts requires the bytecode as hex string. Compiling Ethereum smart contracts with `solc `_ provides these. + +Abis, that are included by default are: + +- AbstractENS +- Described +- EventHub +- Owned +- PublicResolver + +Bytecode for these contracts is included by default: + +- Described +- Owned + +Following is an example for loading a contract with a custom abi. The contract is a `Greeter Contract `_ and a shortened interface containing only the `greet` function is used here. + +They can be side-loaded into an existing contract loader instance, e.g. into a runtime: + +.. code-block:: javascript + + runtime.contractLoader.contracts['Greeter'] = { + "interface": "[{\"constant\":true,\"inputs\":[],\"name\":\"greet\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]", + }; + + +.. _contract_loader_getCompiledContractn: + +getCompiledContract +=================== + +.. code-block:: javascript + + contractLoader.getCompiledContract(name); + +gets contract from a solc compilation + +---------- +Parameters +---------- + +#. ``name`` - ``string``: Contract name + +------- +Returns +------- + +``any``: The compiled contract. + +------- +Example +------- + +.. code-block:: javascript + + const address = '0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49'; + const accountId = '0x000000000000000000000000000000000000beef'; + const description = await runtime.description.getDescription(address, accountId); + console.dir(description); + // Output: + // { public: + // { name: 'DBCP sample greeter', + // description: 'smart contract with a greeting message and a data property', + // author: 'dbcp test', + // tags: [ 'example', 'greeter' ], + // version: '0.1.0', + // abis: { own: [Array] } } } + +------------------------------------------------------------------------------ + +.. _contract_loader_loadContract: + +loadContract +=================== + +.. code-block:: javascript + + contractLoader.loadContract(name, address); + +creates a contract instance that handles a smart contract at a given address + +---------- +Parameters +---------- + +#. ``name`` - ``string``: Contract name +#. ``address`` - ``string``: Contract address + +------- +Returns +------- + +``any``: contract instance. + +------- +Example +------- + +.. code-block:: javascript + + const greeter = runtime.contractLoader.loadContract('Greeter', '0x9c0Aaa728Daa085Dfe85D3C72eE1c1AdF425be49'); diff --git a/docs/contracts/data-contract.rst b/docs/contracts/data-contract.rst new file mode 100644 index 00000000..ce23b257 --- /dev/null +++ b/docs/contracts/data-contract.rst @@ -0,0 +1,885 @@ +================================================================================ +Data Contract +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - DataContract + * - Extends + - `BaseContract `_ + * - Source + - `data-contract.ts `_ + * - Tests + - `data-contract.spec.ts `_ + +The `DataContract `_ is a secured data storage contract for single properties and lists. If created on its own, DataContracts cannot do very much. They rely on their authority to check which entries or lists can be used. + +For more information about DataContracts purpose and their authorities see `Data Contract `_ in the evan.network wiki. + + + +-------------------------------------------------------------------------------- + +.. _data-contract_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new DataContract(options); + +Creates a new DataContract instance. + +---------- +Parameters +---------- + +#. ``options`` - ``DataContractOptions``: options for DataContract constructor. + * ``cryptoProvider`` - |source cryptoProvider|_: |source cryptoProvider|_ instance + * ``defaultCryptoAlgo`` - ``string`` (optional): crypto algorith name from |source cryptoProvider|, defaults to ``aes`` + * ``dfs`` - |source dfsInterface|_: |source dfsInterface|_ instance + * ``executor`` - |source executor|_: |source executor|_ instance + * ``loader`` - |source contractLoader|_: |source contractLoader|_ instance + * ``nameResolver`` - |source nameResolver|_: |source nameResolver|_ instance + * ``sharing`` - |source sharing|_: |source sharing|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``DataContract`` instance + +------- +Example +------- + +.. code-block:: typescript + + const dataContract = new DataContract({ + cryptoProvider, + description, + dfs, + executor, + loader, + nameResolver, + sharing, + web3, + }); + + + +.. _data-contract_create: + +create +=================== + +.. code-block:: javascript + + dataContract.create(factoryName, accountId[, businessCenterDomain, contractDescription, allowConsumerInvite]); + +Create and initialize new contract. + +---------- +Parameters +---------- + +#. ``factoryName`` - ``string``: contract factory name, used for ENS lookup; if the factory name contains periods, it is threaded as an absolute ENS domain and used as such, if not it will be used as ``${factoryName}.factory.${businessCenterDomain}`` +#. ``accountId`` - ``string``: owner of the new contract and transaction executor +#. ``businessCenterDomain`` - ``string`` (optional): ENS domain name of the business center +#. ``contractDescription`` - ``string|any`` (optional): bytes32 hash of DBCP description or a schema object +#. ``allowConsumerInvite`` - ``bool`` (optional): true if consumers are allowed to invite other consumer +#. ``sharingsHash`` - ``string`` (optional): existing sharing to add, defaults to ``null`` + +------- +Returns +------- + +``Promise`` returns ``any``: contract instance + +------- +Example +------- + +Let's say, we want to create a DataContract for a business center at the domain "samplebc.evan" and this business center has a DataContractFactory named "testdatacontract". We want to have two users working in our DataContract, so we get these sample values: + +.. code-block:: typescript + + const factoryName = 'testdatacontract'; + const businessCenterDomain = 'samplebc.evan'; + const accounts = [ + '0x0000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000002', + ]; + +Now create a contract with: + +.. code-block:: typescript + + const contract = await dc.create(factoryName, accounts[0], businessCenterDomain); + +Okay, that does not provide a description for the contract. Let's add a description to the process. The definition is a `DBCP `_ contract definition and is stored in an ``Envelope`` (see :doc:`Encryption `): + +.. code-block:: typescript + + const definition: Envelope = { + "public": { + "name": "Data Contract Sample", + "description": "reiterance oxynitrate sat alternize acurative", + "version": "0.1.0", + "author": "contractus", + "dataSchema": { + "list_settable_by_member": { + "$id": "list_settable_by_member_schema", + "type": "object", + "additionalProperties": false, + "properties": { + "foo": { "type": "string" }, + "bar": { "type": "integer" } + } + }, + "entry_settable_by_member": { + "$id": "entry_settable_by_member_schema", + "type": "integer", + } + } + } + }; + definition.cryptoInfo = cryptoProvider.getCryptorByCryptoAlgo('aes').getCryptoInfo(accounts[0]); + const contract = await dc.create('testdatacontract', accounts[0], businessCenterDomain, definition); + + +Now we have a DataContract with a description. This contract is now able to be understood by other components, that understand the dbcp. And on top of that, we provided data schemas for the two properties ``list_settable_by_member`` and ``entry_settable_by_member`` (written for `ajv `_). This means, that when someone adds or sets entries to or in those properties, the incoming data is validated before actually encrypting and storing it. + +To allow other users to work on the contract, they have to be invited with: + +.. code-block:: typescript + + await dc.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[1]); + +Now the user ``accounts[1]`` can use functions from the contract, but to actually store data, the user needs access to the data key for the DataContract. This can be done via updating the contracts sharing: + +.. code-block:: typescript + + const blockNr = await web3.eth.getBlockNumber(); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', blockNr); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', blockNr, contentKey); + +Now the contract has been created, has a sharing and another user has been granted access to it. Variable names from this section will be used in the rest of the document as example values. + +------------------------------------------------------------------------------ + + + +.. _data-contract_createSharing: + +createSharing +================================================================================ + +.. code-block:: typescript + + dataContract.createSharing(accountId); + +Create initial sharing for contract. + +---------- +Parameters +---------- + +#. ``accountId`` - ``string``: owner of the new contract + +------- +Returns +------- + +``Promise`` returns ``any``: sharing info with { contentKey, hashKey, sharings, sharingsHash, } + +------- +Example +------- + +.. code-block:: typescript + + const sharing = await dataContract.createSharing(profileReceiver); + +-------------------------------------------------------------------------------- + + + += Entries = +=========== + + +.. _data-contract_setEntry: + +setEntry +=================== + +.. code-block:: javascript + + dataContract.setEntry(contract, entryName, value, accountId[, dfsStorage, encryptedHashes, encryption); + +Set entry for a key. + + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: contract or contractId +#. ``entryName`` - ``string``: entry name +#. ``value`` - ``any``: value to add +#. ``accountId`` - ``string``: Ethereum account id +#. ``dfsStorage`` - ``Function`` (optional): store values in dfs, defaults to ``true`` +#. ``encryptedHashes`` - ``boolean`` (optional): encrypt hashes from values, defaults to ``true`` +#. ``encryption`` - ``string`` (optional): encryption algorithm to use, defaults to ``defaultCryptoAlgo`` (set in constructor) + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const sampleValue = 123; + await dc.setEntry(contract, 'entry_settable_by_owner', sampleValue, accounts[0]); + + +Entries are automatically encrypted before setting it in the contract. If you want to use values as is, without encrypting them, you can add them in raw mode, which sets them as ``bytes32`` values: + +.. code-block:: typescript + + const sampleValue = '0x000000000000000000000000000000000000007b'; + await dc.setEntry(contract, 'entry_settable_by_owner', sampleValue, accounts[0], true); + + +------------------------------------------------------------------------------ + +.. _data-contract_getEntry: + +getEntry +=================== + +.. code-block:: javascript + + dataContract.getEntry(contract, entryName, accountId[, dfsStorage, encryptedHashes]); + +Return entry from contract. + + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: contract or contractId +#. ``entryName`` - ``string``: entry name +#. ``accountId`` - ``string``: Ethereum account id +#. ``dfsStorage`` - ``Function`` (optional): store values in dfs, defaults to ``true`` +#. ``encryptedHashes`` - ``boolean`` (optional): decrypt hashes from values, defaults to ``true`` + +------- +Returns +------- + +``Promise`` returns ``any[]``: list entries + +------- +Example +------- + +Entries can be retrieved with: + +.. code-block:: typescript + + const retrieved = await dc.getEntry(contract, 'entry_settable_by_owner', accounts[0]); + + +Raw values can be retrieved in the same way: + +.. code-block:: typescript + + const retrieved = await dc.getEntry(contract, 'entry_settable_by_owner', accounts[0], true); + + + +------------------------------------------------------------------------------ + + + += List Entries = +================ + + +.. _data-contract_addListEntries: + +addListEntries +=================== + +.. code-block:: typescript + + dataContract.addListEntries(contract, listName, values, accountId[, dfsStorage, encryptedHashes, encryption]; + +Add list entries to lists. + +List entries support the raw mode as well. To use raw values, pass ``true`` in the same way as wehn using the entries functions. + +List entries can be added in bulk, so the value argument is an array with values. This array can be arbitrarily large **up to a certain degree**. Values are inserted on the blockchain side and adding very large arrays this way may take more gas during the contract transaction, than may fit into a single transaction. If this is the case, values can be added in chunks (multiple transactions). + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: contract or contractId +#. ``listName`` - ``string``: name of the list in the data contract +#. ``values`` - ``any``: values to add +#. ``accountId`` - ``string``: Ethereum account id +#. ``dfsStorage`` - ``string`` (optional): store values in dfs, defaults to ``true`` +#. ``encryptedHashes`` - ``boolean`` (optional): encrypt hashes from values, defaults to ``true`` +#. ``encryption`` - ``string`` (optional): encryption algorithm to use, defaults to ``defaultCryptoAlgo`` (set in constructor) + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const sampleValue = { + foo: 'sample', + bar: 123, + }; + await dc.addListEntries(contract, 'list_settable_by_member', [sampleValue], accounts[0]); + +When using lists similar to tagging list entries with metadata, entries can be added in multiple lists at once by passing an array of list names: + +.. code-block:: typescript + + const sampleValue = { + foo: 'sample', + bar: 123, + }; + await dc.addListEntries(contract, ['list_1', 'list_2'], [sampleValue], accounts[0]); + + + +------------------------------------------------------------------------------ + + +.. _data-contract_getListEntryCount: + +getListEntryCount +=================== + +.. code-block:: typescript + + dataContract.getListEntryCount(contract, listName, index, accountId[, dfsStorage, encryptedHashes]); + +Return number of entries in the list. +Does not try to actually fetch and decrypt values, but just returns the count. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: contract or contractId +#. ``listName`` - ``string``: name of the list in the data contract + +------- +Returns +------- + +``Promise`` returns ``number``: list entry count + +------- +Example +------- + +.. code-block:: typescript + + await dc.getListEntryCount(contract, 'list_settable_by_member'); + + + +------------------------------------------------------------------------------ + + +.. _data-contract_getListEntries: + +getListEntries +=================== + +.. code-block:: typescript + + dataContract.getListEntries(contract, listName, accountId[, dfsStorage, encryptedHashes, count, offset, reverse]); + +Return list entries from contract. +Note, that in the current implementation, this function retrieves the entries one at a time and may take a longer time when querying large lists, so be aware of that, when you retrieve lists with many entries. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: contract or contractId +#. ``listName`` - ``string``: name of the list in the data contract +#. ``accountId`` - ``string``: Ethereum account id +#. ``dfsStorage`` - ``string`` (optional): store values in dfs, defaults to ``true`` +#. ``encryptedHashes`` - ``boolean`` (optional): decrypt hashes from values, defaults to ``true`` +#. ``count`` - ``number`` (optional): number of elements to retrieve, defaults to ``10`` +#. ``offset`` - ``number`` (optional): skip this many items when retrieving, defaults to ``0`` +#. ``reverse`` - ``boolean`` (optional): retrieve items in reverse order, defaults to ``false`` + +------- +Returns +------- + +``Promise`` returns ``any[]``: list entries + +------- +Example +------- + +.. code-block:: typescript + + await dc.getListEntries(contract, 'list_settable_by_member', accounts[0])); + + + +------------------------------------------------------------------------------ + + +.. _data-contract_getListEntry: + +getListEntry +=================== + +.. code-block:: typescript + + dataContract.getListEntry(contract, listName, index, accountId[, dfsStorage, encryptedHashes]); + +Return a single list entry from contract. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: contract or contractId +#. ``listName`` - ``string``: name of the list in the data contract +#. ``index`` - ``number``: list entry id to retrieve +#. ``accountId`` - ``string``: Ethereum account id +#. ``dfsStorage`` - ``string`` (optional): store values in dfs, defaults to ``true`` +#. ``encryptedHashes`` - ``boolean`` (optional): decrypt hashes from values, defaults to ``true`` + +------- +Returns +------- + +``Promise`` returns ``any``: list entry + +------- +Example +------- + +.. code-block:: typescript + + const itemIndex = 0; + await dc.getListEntry(contract, 'list_settable_by_member', itemIndex, accounts[0])); + + +------------------------------------------------------------------------------ + + +.. _data-contract_removeListEntry: + +removeListEntry +=================== + +.. code-block:: typescript + + redataContract.moveListEntry(contract, listName, entryIndex, accountId); + +Remove list entry from list. + +This will reposition last list entry into emptied slot. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: contract or contractId +#. ``listName`` - ``string``: name of the list in the data contract +#. ``index`` - ``number``: index of the entry to move in the origin list +#. ``accountId`` - ``string``: Ethereum account id + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const listName = 'list_removable_by_owner' + const itemIndexInList = 1; + await dc.removeListEntry(contract, listNameF, itemIndexInList, accounts[0]); + + +------------------------------------------------------------------------------ + + +.. _data-contract_moveListEntry: + +moveListEntry +=================== + +.. code-block:: typescript + + dataContract.moveListEntry(contract, listNameFrom, entryIndex, listNamesTo, accountId); + +Move one list entry to one or more lists. + +Note, that moving items requires the executing account to have ``remove`` permissions on the list ``listNameFrom``. If this isn't the case, the transaction will not be exetured and not updates will be made. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: contract or contractId +#. ``listNameFrom`` - ``string``: origin list +#. ``index`` - ``number``: index of the entry to move in the origin list +#. ``listNamesTo`` - ``string``: lists to move data into +#. ``accountId`` - ``string``: Ethereum account id + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const listNameFrom = 'list_removable_by_owner'; + const listNameTo = 'list_settable_by_member'; + const itemIndexInFromList = 1; + await dc.moveListEntry(contract, listNameFrom, itemIndexInFromList, [listNameTo], accounts[0]); + + +------------------------------------------------------------------------------ + + += Mappings = +================ + + +.. _data-contract_setMappingValue: + +setMappingValue +=================== + +.. code-block:: typescript + + dataContract.setMappingValue(contract, mappingName, entryName, value, accountId[, dfsStorage, encryptedHashes, encryption]); + +Set entry for a key in a mapping. +Mappings are basically dictionaries in data contracts. They are a single permittable entry, that allows to set any keys to it. This can be used for properties, that should be extended during the contracts life as needed, but without the need to update its permission settings. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: contract or contractId +#. ``mappingName`` - ``string``: name of a data contracts mapping property +#. ``entryName`` - ``string``: entry name (property in the mapping) +#. ``value`` - ``any``: value to add +#. ``accountId`` - ``string``: Ethereum account id +#. ``dfsStorage`` - ``string`` (optional): store values in dfs, defaults to ``true`` +#. ``encryptedHashes`` - ``boolean`` (optional): encrypt hashes from values, defaults to ``true`` +#. ``encryption`` - ``string`` (optional): encryption algorithm to use, defaults to ``defaultCryptoAlgo`` (set in constructor) + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await dataContract.setMappingValue( + contract, + 'mapping_settable_by_owner', + 'sampleKey', + 'sampleValue', + accounts[0], + storeInDfs, + ); + + +------------------------------------------------------------------------------ + + +.. _data-contract_getMappingValue: + +getMappingValue +=================== + +.. code-block:: typescript + + dataContract.getMappingValue(contract, listName, index, accountId[, dfsStorage, encryptedHashes]); + +Return a value from a mapping. +Looks up a single key from a mapping and returns its value. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: contract or contractId +#. ``mappingName`` - ``string``: name of a data contracts mapping property +#. ``entryName`` - ``string``: entry name (property in the mapping) +#. ``accountId`` - ``string``: Ethereum account id +#. ``dfsStorage`` - ``string`` (optional): store values in dfs, defaults to ``true`` +#. ``encryptedHashes`` - ``boolean`` (optional): encrypt hashes from values, defaults to ``true`` +#. ``encryption`` - ``string`` (optional): encryption algorithm to use, defaults to ``defaultCryptoAlgo`` (set in constructor) + +------- +Returns +------- + +``Promise`` returns ``any``: mappings value for given key + +------- +Example +------- + +.. code-block:: typescript + + const value = await dataContract.getMappingValue( + contract, + 'mapping_settable_by_owner', + 'sampleKey', + accounts[0], + storeInDfs, + ); + + + +------------------------------------------------------------------------------ + + += Encryption = +================ + + +.. data-contract_encrypt: + +encrypt +=================== + +.. code-block:: typescript + + dataContract.encrypt(toEncrypt, contract, accountId, propertyName, block[, encryption]); + +Encrypt incoming envelope. + +---------- +Parameters +---------- + +#. ``toEncrypt`` - ``Envelope``: envelope with data to encrypt +#. ``contract`` - ``any``: contract instance or contract id +#. ``accountId`` - ``string``: encrypting account +#. ``propertyName`` - ``string``: property in contract, the data is encrypted for +#. ``block`` - ``block``: block the data belongs to +#. ``encryption`` - ``string``: encryption name, defaults to ``defaultCryptoAlgo`` (set in constructor) + +------- +Returns +------- + +``Promise`` returns ``string``: encrypted envelope or hash as string + +------- +Example +------- + +.. code-block:: typescript + + const data = { + public: { + foo: 'example', + }, + private: { + bar: 123, + }, + cryptoInfo: cryptor.getCryptoInfo(nameResolver.soliditySha3(accounts[0])), + }; + const encrypted = await dataContract.encrypt(data, contract, accounts[0], 'list_settable_by_member', 12345); + + +------------------------------------------------------------------------------ + +.. data-contract_decrypt: + +decrypt +=================== + +.. code-block:: typescript + + dataContract.decrypt(toDecrypt, contract, accountId, propertyName, block[, encryption]); + +Decrypt input envelope return decrypted envelope. + +---------- +Parameters +---------- + +#. ``toDecrypt`` - ``string``: data to decrypt +#. ``contract`` - ``any``: contract instance or contract id +#. ``accountId`` - ``string``: account id that decrypts the data +#. ``propertyName`` - ``string``: property in contract that is decrypted + +------- +Returns +------- + +``Promise`` returns ``Envelope``: decrypted envelope + +------- +Example +------- + +.. code-block:: typescript + + const encrypted = await dataContract.decrypt(encrypted, contract, accounts[0], 'list_settable_by_member'); + + +------------------------------------------------------------------------------ + +.. data-contract_encryptHash: + +encryptHash +=================== + +.. code-block:: typescript + + dataContract.encryptHash(toEncrypt, contract, accountId); + +Encrypt incoming hash. +This function is used to encrypt DFS file hashes, uses AES ECB for encryption. + +---------- +Parameters +---------- + +#. ``toEncrypt`` - ``Envelope``: hash to encrypt +#. ``contract`` - ``any``: contract instance or contract id +#. ``accountId`` - ``string``: encrypting account + +------- +Returns +------- + +``Promise`` returns ``string``: hash as string + +------- +Example +------- + +.. code-block:: typescript + + const hash = '0x1111111111111111111111111111111111111111111111111111111111111111'; + const encrypted = await dataContract.encryptHash(hash, contract, accounts[0]); + + +------------------------------------------------------------------------------ + +.. data-contract_decryptHash: + +decryptHash +=================== + +.. code-block:: typescript + + dataContract.encrypt(toEncrypttoDecrypt, contract, accountId, propertyName, block[, encryption]); + +Decrypt input hash, return decrypted hash. +This function is used to decrypt encrypted DFS file hashes, uses AES ECB for decryption. + +---------- +Parameters +---------- + +#. ``toDecrypt`` - ``Envelope``: hash to decrypt +#. ``contract`` - ``any``: contract instance or contract id +#. ``accountId`` - ``string``: encrypting account + +------- +Returns +------- + +``Promise`` returns ``string``: decrypted hash + +------- +Example +------- + +.. code-block:: typescript + + const encryptedHash = '0x2222222222222222222222222222222222222222222222222222222222222222'; + const encrypted = await dataContract.decryptHash(encryptedHash, contract, accounts[0]); + + + +.. required for building markup + +.. |source contractLoader| replace:: ``ContractLoader`` +.. _source contractLoader: /contracts/contract-loader.html + +.. |source cryptoProvider| replace:: ``CryptoProvider`` +.. _source cryptoProvider: /encryption/crypto-provider.html + +.. |source dfsInterface| replace:: ``DfsInterface`` +.. _source dfsInterface: /dfs/dfs-interface.html + +.. |source executor| replace:: ``Executor`` +.. _source executor: /blockchain/executor.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source nameResolver| replace:: ``NameResolver`` +.. _source nameResolver: /blockchain/name-resolver.html + +.. |source sharing| replace:: ``Sharing`` +.. _source sharing: /contracts/sharing.html diff --git a/docs/contracts/index.rst b/docs/contracts/index.rst new file mode 100644 index 00000000..a353a203 --- /dev/null +++ b/docs/contracts/index.rst @@ -0,0 +1,26 @@ +================================================================================ +Contracts +================================================================================ + +.. toctree:: + :glob: + :maxdepth: 1 + + contract-loader + rights-and-roles + sharing + base-contract + data-contract + service-contract + + +This section includes modules, that deal with smart contract interactions, which includes: + +- contract helper libraries, e.g. for + + * ``BaseContract`` + * ``DataContract`` + * ``ServiceContract`` + +- permission management +- sharing keys for contracts \ No newline at end of file diff --git a/docs/contracts/rights-and-roles.rst b/docs/contracts/rights-and-roles.rst new file mode 100644 index 00000000..a78ccec2 --- /dev/null +++ b/docs/contracts/rights-and-roles.rst @@ -0,0 +1,436 @@ +================================================================================ +Rights and Roles +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - RightsAndRoles + * - Extends + - `Logger `_ + * - Source + - `rights-and-roles.ts `_ + * - Tests + - `rights-and-roles.spec.ts `_ + +The `RightsAndRoles `_ module follows the approach described in the evan.network wik at: + +- `Function Permissions `_ +- `Operation Permissions `_ + +It allows to manage permissions for contracts, that use the authority `DSRolesPerContract.sol `_ for as its permission approach. + +Contracts, that use `DSRolesPerContract `_ and therefore allow to configure its permissions with the ``RightsAndRoles`` module are: + +- `BaseContract `_ +- `DataContract `_ +- `ServiceContract `_ +- `Shared `_ +- `Described `_ +- `BusinessCenter `_ + + +------------------------------------------------------------------------------ + +.. _rights-and-roles_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new RightsAndRole(options); + +Creates new RightsAndRole instance. + +---------- +Parameters +---------- + +#. ``options`` - ``RightsAndRolesOptions``: options for RightsAndRole constructor. + * ``contractLoader`` - |source contractLoader|_: |source contractLoader|_ instance + * ``executor`` - |source executor|_: |source executor|_ instance + * ``nameResolver`` - |source nameResolver|_: |source nameResolver|_ instance + * ``web3`` - |source web3|_: |source web3|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``RightsAndRoles`` instance + +------- +Example +------- + +.. code-block:: typescript + + const rightsAndRoles = new RightsAndRoles({ + contractLoader, + executor, + nameResolver, + web3, + }); + + +-------------------------------------------------------------------------------- + +.. _rights-and-roles_addAccountToRole: + +addAccountToRole +================================================================================ + +.. code-block:: typescript + + rightsAndRoles.addAccountToRole(contract, accountId, targetAccountId, role); + +Adds the traget account to a specific role. + +The main principle is that accounts can be assigned to roles and those roles can be granted capabilities. :ref:`Function Permissions ` are basically the capability to call specific functions if the calling account belongs to a certain role. To add an account to the role 'member'. + +---------- +Parameters +---------- + +#. ``contract`` - ``string|any``: contractId or contract instance +#. ``accountId`` - ``string``: executing accountId +#. ``targetAccountId`` - ``string``: target accountId +#. ``role`` - ``number``: roleId + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const contractOwner = '0x0000000000000000000000000000000000000001'; + const newMember = '0x0000000000000000000000000000000000000002'; + const memberRole = 1; + await rightsAndRoles.addAccountToRole( + contract, // contract to be updated + contractOwner, // account, that can change permissions + newMember, // add this account to role + memberRole, // role id, uint8 value + ); + + +-------------------------------------------------------------------------------- + +.. _rights-and-roles_removeAccountFromRole: + +removeAccountFromRole +================================================================================ + +.. code-block:: typescript + + rightsAndRoles.removeAccountFromRole(contract, accountId, targetAccountId, role); + +Removes target account from a specific role. + +---------- +Parameters +---------- + +#. ``contract`` - ``string|any``: contractId or contract instance +#. ``accountId`` - ``string``: executing accountId +#. ``targetAccountId`` - ``string``: target accountId +#. ``role`` - ``number``: roleId + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const contractOwner = '0x0000000000000000000000000000000000000001'; + const newMember = '0x0000000000000000000000000000000000000002'; + const memberRole = 1; + await rightsAndRoles.removeAccountFromRole( + contract, // contract to be updated + contractOwner, // account, that can change permissions + newMember, // remove this account from role + memberRole, // role id, uint8 value + ); + + +------------------------------------------------------------------------------ + +.. _rights-and-roles_getMembers: + +getMembers +================================================================================ + +.. code-block:: typescript + + rightsAndRoles.getMembers(contract); + +Returns all roles with all members. + +The `DSRolesPerContract `_ authority tracks used roles and their members and allows to retrieve an overview with all roles and their members. To get this information, you can use the ``getMembes`` function. + +---------- +Parameters +---------- + +#. ``contract`` - ``string|any``: contractId or contract instance + +------- +Returns +------- + +``Promise`` returns ``any``: Object with mapping roleId -> [accountId, accountId,...] + +------- +Example +------- + +.. code-block:: typescript + + const members = await rightsAndRoles.getMembers(contract); + console.log(members); + // Output: + // { + // "0": [ + // "0x0000000000000000000000000000000000000001" + // ], + // "1": [ + // "0x0000000000000000000000000000000000000001", + // "0x0000000000000000000000000000000000000002" + // ] + // } + +The contract from this example has an owner (``0x0000000000000000000000000000000000000001``) and a member (``0x0000000000000000000000000000000000000002``). As the owner account has the member role as well, it is listed among the members. + + +------------------------------------------------------------------------------ + +.. _rights-and-roles_setFunctionPermission: + +setFunctionPermission +================================================================================ + +.. code-block:: typescript + + rightsAndRoles.setFunctionPermission(contract, accountId, role, functionSignature, allow); + +Allows or denies contract function for the accountId. + +"Function permissions" are granted or denying by allowing a certain role to execute a specific function. The function is specified as the unhashed `function selector `_ and must follow its guidelines (no spaces, property typenames, etc.) for the function to be able to generate valid hashes for later validations. E.g. to grant the role "member" the permission to use the function `addListEntries`, that has two arguments (a ``bytes32`` array and a ``bytes32`` value), the function permission for ``addListEntries(bytes32[],bytes32[])`` has to be granted. + +---------- +Parameters +---------- + +#. ``contract`` - ``string|any``: contractId or contract instance +#. ``accountId`` - ``string``: executing accountId +#. ``role`` - ``number``: roleid +#. ``functionSignature`` - ``string``: 4 Bytes function signature +#. ``allow`` - ``boolean``: allow or deny function + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const contractOwner = '0x0000000000000000000000000000000000000001'; + const memberRole = 1; + await rightsAndRoles.setFunctionPermission( + contract, // contract to be updated + contractOwner, // account, that can change permissions + memberRole, // role id, uint8 value + 'addListEntries(bytes32[],bytes32[])', // (unhashed) function selector + true, // grant this capability + ); + + +------------------------------------------------------------------------------ + +.. _rights-and-roles_setOperationPermission: + +setOperationPermission +================================================================================ + +.. code-block:: typescript + + rightsAndRoles.setOperationPermission(contract, accountId, role, propertyName, propertyType, modificationType, allow); + +Allows or denies setting properties on a contract. + +"Operation Permissions" are capabilities granted per contract logic. They have a ``bytes32`` key, that represents the capability, e.g. in a `DataContract `_ a capability to add values to a certain list can be granted. + +The way, those capability hashes are build, depends on the contract logic and differs from contract to contract. For example a capability check for validation if a member is allowed to add an item to the list "example" in a `DataContract `_ has four arguments, in this case: + +- which role is allowed to do? (e.g. a member) +- what type of element is modified? (--> a list) +- which element is modified? (name of the list --> "example") +- type of the modification (--> "set an item" (== "add an item")) + +These four values are combined into one ``bytes32`` value, that is used when granting or checking permissions, the ``setOperationPermission`` function takes care of that. + +---------- +Parameters +---------- + +#. ``contract`` - ``string|any``: contractId or contract instance +#. ``accountId`` - ``string``: executing accountId +#. ``role`` - ``number``: roleId +#. ``propertyName`` - ``string``: target property name +#. ``propertyType`` - ``PropertyType``: list or entry +#. ``modificationType`` - ``ModificationType``: set or remove +#. ``allow`` - ``boolean``: allow or deny + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + // make sure, you have required the enums from rights-and-roles.ts + import { ModificationType, PropertyType } from 'blockchain-core'; + const contractOwner = '0x0000000000000000000000000000000000000001'; + const memberRole = 1; + await rightsAndRoles.setOperationPermission( + contract, // contract to be updated + contractOwner, // account, that can change permissions + memberRole, // role id, uint8 value + 'example', // name of the object + PropertyType.ListEntry, // what type of element is modified + ModificationType.Set, // type of the modification + true, // grant this capability + ); + + +------------------------------------------------------------------------------ + +.. _rights-and-roles_hasUserRole: + +hasUserRole +================================================================================ + +.. code-block:: typescript + + rightsAndRoles.hasUserRole(contract, accountId, targetAccountId, role); + +Returns true or false, depending on if the account has the specific role. + +---------- +Parameters +---------- + +#. ``contract`` - ``string|any``: contractId or contract instance +#. ``accountId`` - ``string``: executing accountId +#. ``targetAccountId`` - ``string``: to be checked accountId +#. ``role`` - ``number``: roleId + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const accountToCheck = '0x0000000000000000000000000000000000000002'; + const memberRole = 1; + const hasRole = await rightsAndRoles.hashUserRole(contract, null, accountToCheck, memberRole); + console.log(hasRole); + // Output: + // true + + + +.. required for building markup +.. |source contractLoader| replace:: ``ContractLoader`` +.. _source contractLoader: /contracts/contract-loader.html + +.. |source executor| replace:: ``Executor`` +.. _source executor: /blockchain/executor.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source nameResolver| replace:: ``NameResolver`` +.. _source nameResolver: /blockchain/name-resolver.html + +.. |source web3| replace:: ``Web3`` +.. _source web3: https://github.com/ethereum/web3.js/ + + +-------------------------------------------------------------------------------- + +.. _rights-and-roles_transferOwnership: + +transferOwnership +================================================================================ + +.. code-block:: typescript + + rightsAndRoles.transferOwnership(); + +Function description + +---------- +Parameters +---------- + +#. ``contract`` - ``string|any``: contractId or contract instance +#. ``accountId`` - ``string``: executing accountId +#. ``targetAccountId`` - ``string``: target accountId + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const contractOwner = '0x0000000000000000000000000000000000000001'; + const newOwner = '0x0000000000000000000000000000000000000002'; + await rightsAndRoles.transferOwnership( + contract, // contract to be updated + contractOwner, // current owner + newOwner, // this account becomes new owner + ); \ No newline at end of file diff --git a/docs/contracts/service-contract.rst b/docs/contracts/service-contract.rst new file mode 100644 index 00000000..84b2c548 --- /dev/null +++ b/docs/contracts/service-contract.rst @@ -0,0 +1,614 @@ +================================================================================ +Service Contract +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - ServiceContract + * - Extends + - `Logger `_ + * - Source + - `service-contract.ts `_ + * - Tests + - `service-contract.spec.ts `_ + + + +.. _serviceContract_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new ServiceContract(options); + +Creates a new ServiceContract instance. + +---------- +Parameters +---------- + +#. ``options`` - ``ServiceContractOptions``: options for ServiceContract constructor. + * ``cryptoProvider`` - |source cryptoProvider|_: |source cryptoProvider|_ instance + * ``dfs`` - |source dfsInterface|_: |source dfsInterface|_ instance + * ``keyProvider`` - |source keyProvider|_: |source keyProvider|_ instance + * ``sharing`` - |source sharing|_: |source sharing|_ instance + * ``web3`` - |source web3|_: |source web3|_ instance + * ``defaultCryptoAlgo`` - ``string`` (optional): crypto algorith name from |source cryptoProvider|, defaults to ``aes`` + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``ServiceContract`` instance + +------- +Example +------- + +.. code-block:: typescript + + const serviceContract = new ServiceContract({ + cryptoProvide, + dfs, + executor, + keyProvider, + loader, + nameResolver, + sharing, + web3, + }); + + + +-------------------------------------------------------------------------------- + +.. _service-contract_create: + +create +================================================================================ + +.. code-block:: typescript + + serviceContract.create(accountId, businessCenterDomain, service[, descriptionDfsHash]); + +create and initialize new contract + +---------- +Parameters +---------- + +#. ``accountId`` - ``string``: owner of the new contract and transaction executor +#. ``businessCenterDomain`` - ``string``: ENS domain name of the business center +#. ``service`` - ``any``: service definition +#. ``descriptionHash`` - ``string`` (optional): bytes2 hash of DBCP description, defaults to ``0x0000000000000000000000000000000000000000000000000000000000000000`` + +------- +Returns +------- + +``Promise`` returns ``any``: contract instance + +------- +Example +------- + +.. code-block:: typescript + + const serviceContract = await serviceContract.create(accounts[0], businessCenterDomain, sampleService); + + + +-------------------------------------------------------------------------------- + +.. _service-contract_service: + += Service = +=========== + +The service is the communication pattern definition for the ``ServiceContract``. A single service contract can only have one service definition and all calls and answers must follow its defition. + +To create calls and answers with different patterns, create a new ``ServiceContract`` and use an updated service definition there. + + + +-------------------------------------------------------------------------------- + +.. _serviceContract_setService: + +setService +================================================================================ + +.. code-block:: typescript + + serviceContract.setService(contract, accountId, service, businessCenterDomain); + +Set service description. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: smart contract instance or contract ID +#. ``accountId`` - ``string``: Ethereum account ID +#. ``service`` - ``any``: service to set +#. ``businessCenterDomain`` - ``string``: domain of the business the service contract belongs to + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await serviceContract.setService(contract, accounts[0], sampleService, businessCenterDomain); + + + +.. _serviceContract_getService: + +getService +================================================================================ + +.. code-block:: typescript + + serviceContract.getService(contract, accountId); + +Gets the service of a service contract. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: smart contract instance or contract ID +#. ``accountId`` - ``string``: Ethereum account ID + +------- +Returns +------- + +``Promise`` returns ``string``: service description as JSON string + +------- +Example +------- + +.. code-block:: typescript + + const service = await sc.getService(contract, accounts[0]); + + + +-------------------------------------------------------------------------------- + +.. _service-contract_calls: + += Calls = +=========== + +Calls are the requests done by authors, that initiate a service conversation. They are basically the first part of conversations and allow answers to be added to them. Calls are usually broadcasted or multicasted. + +Samples for calls are: + +- capacity requests +- information requests +- information broadcasts + + + +.. _service-contract_sendCall: + +sendCall +================================================================================ + +.. code-block:: typescript + + serviceContract.sendCall(contract, accountId, call); + +Send a call to a service. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: smart contract instance or contract ID +#. ``accountId`` - ``string``: Ethereum account ID +#. ``call`` - ``any``: call to send + +------- +Returns +------- + +``Promise`` returns ``number``: id of new call + +------- +Example +------- + +.. code-block:: typescript + + const callId = await serviceContract.sendCall(contract, accounts[0], sampleCall); + + + +-------------------------------------------------------------------------------- + +.. _service-contract_getCalls: + +getCalls +================================================================================ + +.. code-block:: typescript + + serviceContract.getCalls(contract, accountId[, count, offset, reverse]); + +Get all calls from a contract. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: smart contract instance or contract ID +#. ``accountId`` - ``string``: Ethereum account ID +#. ``count`` - ``number`` (optional): number of elments to retrieve, defaults to ``10`` +#. ``offset`` - ``number`` (optional): skip this many elements, defaults to ``0`` +#. ``reverse`` - ``boolean`` (optional): retrieve last elements first, defaults to ``false`` + + +------- +Returns +------- + +``Promise`` returns ``any[]``: the calls + +------- +Example +------- + +.. code-block:: typescript + + const calls = await serviceContract.getCalls(contract, accounts[0]); + + + +-------------------------------------------------------------------------------- + +.. _service-contract_getCall: + +getCall +================================================================================ + +.. code-block:: typescript + + serviceContract.getCall(contract, accountId, callId); + +Get a call from a contract. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: smart contract instance or contract ID +#. ``accountId`` - ``string``: Ethereum account ID +#. ``callId`` - ``number``: index of the call to retrieve + +------- +Returns +------- + +``Promise`` returns ``any``: a single call + +------- +Example +------- + +.. code-block:: typescript + + const call = await serviceContract.getCall(contract, accounts[0], 12); + + + +-------------------------------------------------------------------------------- + +.. _service-contract_getCallCount: + +getCallCount +================================================================================ + +.. code-block:: typescript + + serviceContract.getCallCount(contract); + +Get number of calls of a contract. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: smart contract instance or contract ID + +------- +Returns +------- + +``Promise`` returns ``number``: number of calls + +------- +Example +------- + +.. code-block:: typescript + + let callCount = await serviceContract.getCallCount(contract); + console.log(callCount); + // Output: + // 2 + await serviceContract.sendCall(contract, accounts[0], sampleCall); + callCount = await serviceContract.getCallCount(contract); + console.log(callCount); + // Output: + // 3 + + + +-------------------------------------------------------------------------------- + +.. _servicecontract_addToCallSharing: + +addToCallSharing +================================================================================ + +.. code-block:: typescript + + serviceContract.addToCallSharing(contract, accountId, callId, to[, hashKey, contentKey, section]); + +Adds list of accounts to a calls sharings list. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: smart contract instance or contract ID +#. ``accountId`` - ``string``: Ethereum account ID +#. ``callId`` - ``number``: id of the call to retrieve +#. ``to`` - ``string[]``: accountIds, to add sharings for +#. ``hashKey`` - ``string`` (optional): hash key to share, if omitted, key is retrieved with ``accountId`` +#. ``contentKey`` - ``string`` (optional): content key to share, if omitted, key is retrieved with ``accountId`` +#. ``section`` - ``string`` (optional): section to share key for, defaults to '*' + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + // account[0] adds accounts[2] to a sharing + await serviceContract.addToCallSharing(contract, accounts[0], callId, [accounts[2]]); + + + +-------------------------------------------------------------------------------- + + +.. _servicecontract_answers: + += Answers = +=========== + +Answers are replies to calls. Answers can only be created as answers to calls. Answers are usually directed to the author of a call. + +Examples are + +- capacity replies +- information responses + + + +-------------------------------------------------------------------------------- + +.. _service-contract_sendAnswer: + +sendAnswer +================================================================================ + +.. code-block:: typescript + + serviceContract.sendAnswer(contract, accountId, answer, callId, callAuthor); + +Send answer to service contract call. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: smart contract instance or contract ID +#. ``accountId`` - ``string``: Ethereum account ID +#. ``answer`` - ``any``: answer to send +#. ``callId`` - ``number``: index of the call to which the answer was created +#. ``callAuthor`` - ``string``: Ethereum account ID of the creator of the initial call + +------- +Returns +------- + +``Promise`` returns ``number``: id of new answer + +------- +Example +------- + +.. code-block:: typescript + + await serviceContract.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[2]); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', 0); + await sharing.addSharing(contract.options.address, accounts[0], accounts[2], '*', 0, contentKey); + await serviceContract.sendCall(contract, accounts[0], sampleCall); + const call = await serviceContract.getCall(contract, accounts[0], 0); + const answerId = await serviceContract.sendAnswer(contract, accounts[2], sampleAnswer, 0, call.metadata.author); + + + +-------------------------------------------------------------------------------- + +.. _service-contract_getAnswers: + +getAnswers +================================================================================ + +.. code-block:: typescript + + serviceContract.getAnswers(contract, accountId, callid[, count, offset, reverse]); + +Retrieves answers for a given call. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: smart contract instance or contract ID +#. ``accountId`` - ``string``: Ethereum account ID +#. ``callId`` - ``number``: index of the call to which the answers were created +#. ``count`` - ``number`` (optional): number of elements to retrieve, defaults to ``10`` +#. ``offset`` - ``number`` (optional): skip this many elements, defaults to ``0`` +#. ``reverse`` - ``boolean`` (optional): retrieve last elements first, defaults to ``false`` + +------- +Returns +------- + +``Promise`` returns ``any[]``: the answers + +------- +Example +------- + +.. code-block:: typescript + + const answers = await serviceContract.getAnswers(contract, accounts[0], 12); + + + +-------------------------------------------------------------------------------- + +.. _service-contract_getAnswer: + +getAnswer +================================================================================ + +.. code-block:: typescript + + serviceContract.getAnswer(contract, accountId, answerIndex); + +Get a answer from a contract. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: smart contract instance or contract ID +#. ``accountId`` - ``string``: Ethereum account ID +#. ``callId`` - ``number``: index of the call to which the answer was created +#. ``answerIndex`` - ``number``: index of the answer to retrieve + +------- +Returns +------- + +``Promise`` returns ``any``: a single answer + +------- +Example +------- + +.. code-block:: typescript + + const answer = await serviceContract.getAnswer(contract, accounts[0], 12, 2); + + + +-------------------------------------------------------------------------------- + +.. _service-contract_getAnswerCount: + +getAnswerCount +================================================================================ + +.. code-block:: typescript + + serviceContract.getAnswerCount(contract, callId); + +Retrieves number of answers for a given call. + +---------- +Parameters +---------- + +#. ``contract`` - ``any|string``: smart contract instance or contract ID +#. ``callId`` - ``number``: index of the call to which the answer was created + +------- +Returns +------- + +``Promise`` returns ``number``: number of answers + +------- +Example +------- + +.. code-block:: typescript + + const sampleCallId = 3; + let answerCount = await serviceContract.getAnswerCount(contract, sampleCallId); + console.log(answerCount); + // Output: + // 2 + await serviceContract.sendAnswer(contract, accounts[0], sampleAnswer, sampleCallId, accounts[1]); + answerCount = await serviceContract.getAnswerCount(contract, sampleCallId); + console.log(answerCount); + // Output: + // 3 + + + +.. required for building markup + +.. |source cryptoProvider| replace:: ``CryptoProvider`` +.. _source cryptoProvider: /encryption/crypto-provider.html + +.. |source dfsInterface| replace:: ``DfsInterface`` +.. _source dfsInterface: /dfs/dfs-interface.html + +.. |source keyProvider| replace:: ``KeyProvider`` +.. _source keyProvider: /key-provider + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source sharing| replace:: ``Sharing`` +.. _source sharing: /contracts/sharing.html + +.. |source web3| replace:: ``Web3`` +.. _source web3: https://github.com/ethereum/web3.js/ \ No newline at end of file diff --git a/docs/contracts/sharing.rst b/docs/contracts/sharing.rst new file mode 100644 index 00000000..0dddbcea --- /dev/null +++ b/docs/contracts/sharing.rst @@ -0,0 +1,568 @@ +================================================================================ +Sharing +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Sharing + * - Extends + - `Logger `_ + * - Source + - `sharing.ts `_ + * - Tests + - `sharing.spec.ts `_ + +For getting a better understanding about how Sharings and Multikeys work, have a look at `Security `_ in the evan.network wiki. + +Following is a sample for a sharing info with these properties: + +- three users + + * ``0x01`` - owner of a contract + * ``0x02`` - member of a contract + * ``0x03`` - another member with differing permissions + +- two timestamps + + * block 82745 - first sharing + * block 90000 - splitting data, update sharings + +- three sections + + * ``*`` generic "catch all" used in first sharing + * ``secret area`` - available for all members + * ``super secret area`` - available for ``0x03`` + +.. code-block:: typescript + + { + "0x01": { + "82745": { + "*": { + "private": "secret for 0x01, starting from block 82745 for all data", + "cryptoInfo": { + "originator": "0x01,0x01", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + } + }, + "90000": { + "secret area": { + "private": "secret for 0x01, starting from block 90000 for 'secret area'", + "cryptoInfo": { + "originator": "0x01,0x01", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + }, + "super secret area": { + "private": "secret for 0x01, starting from block 90000 for 'super secret area'", + "cryptoInfo": { + "originator": "0x01,0x01", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + } + } + }, + "0x02": { + "82745": { + "*": { + "private": "secret for 0x02, starting from block 82745 for all data", + "cryptoInfo": { + "originator": "0x01,0x02", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + } + }, + "90000": { + "secret area": { + "private": "secret for 0x02, starting from block 90000 for 'secret area'", + "cryptoInfo": { + "originator": "0x01,0x02", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + }, + "super secret area": { + "private": "secret for 0x02, starting from block 90000 for 'super secret area'", + "cryptoInfo": { + "originator": "0x01,0x02", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + } + }, + }, + "0x03": { + "90000": { + "secret area": { + "private": "secret for 0x03, starting from block 90000 for 'secret area'", + "cryptoInfo": { + "originator": "0x01,0x03", + "keyLength": 256, + "algorithm": "aes-256-cbc" + } + } + } + } + } + + + +-------------------------------------------------------------------------------- + +.. _sharing_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new Sharing(options); + +Creates a new Sharing instance. + +---------- +Parameters +---------- + +#. ``options`` - ``SharingOptions``: options for Sharing constructor. + * ``contractLoader`` - |source contractLoader|_: |source contractLoader|_ instance + * ``cryptoProvider`` - |source cryptoProvider|_: |source cryptoProvider|_ instance + * ``description`` - |source description|_: |source description|_ instance + * ``dfs`` - |source dfsInterface|_: |source dfsInterface|_ instance + * ``executor`` - |source executor|_: |source executor|_ instance + * ``keyProvider`` - |source keyProvider|_: |source keyProvider|_ instance + * ``nameResolver`` - |source nameResolver|_: |source nameResolver|_ instance + * ``defaultCryptoAlgo`` - ``string`` (optional): crypto algorith name from |source cryptoProvider|, defaults to ``aes`` + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``Sharing`` instance + +------- +Example +------- + +.. code-block:: typescript + + const sharing = new Sharing({ + contractLoader, + cryptoProvider, + description, + executor, + dfs, + keyProvider, + nameResolver, + defaultCryptoAlgo: 'aes', + }); + + + +-------------------------------------------------------------------------------- + +.. _sharing_addSharing: + +addSharing +================================================================================ + +.. code-block:: typescript + + sharing.addSharing(address, originator, partner, section, block, sharingKey[, context, isHashKey, sharingId]); + +Add a sharing to a contract or an ENS address. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: contract address or ENS address +#. ``originator`` - ``string``: Ethereum account id of the sharing user +#. ``partner`` - ``string``: Ethereum account id for which key shall be added +#. ``section`` - ``string``: data section the key is intended for or '*' +#. ``block`` - ``number|string``: starting with this block, the key is valid +#. ``sharingKey`` - ``string``: key to share +#. ``context`` - ``string`` (optional): context to share key in +#. ``isHashKey`` - ``bool`` (optional): indicates if given key already is a hash key, defaults to ``false`` +#. ``sharingId`` - ``string`` (optional): id of a sharing (when multi-sharings is used) + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + // two sample users, user1 wants to share a key with user2 + const user1 = '0x0000000000000000000000000000000000000001'; + const user2 = '0x0000000000000000000000000000000000000002'; + // create a sample contract + // usually you would have an existing contract, for which you want to manage the sharings + const contract = await executor.createContract('Shared', [], { from: user1, gas: 500000, }); + // user1 shares the given key with user2 + // this key is shared for all contexts ('*') and valid starting with block 0 + await sharing.addSharing(contract.options.address, user1, user2, '*', 0, 'i am the secred that will be shared'); + + + +-------------------------------------------------------------------------------- + +.. _sharing_extendSharing: + +extendSharing +================================================================================ + +.. code-block:: typescript + + sharing.extendSharing(address, originator, partner, section, block, sharingKey[, context, isHashKey]); + +Extend an existing sharing info with given key; this is done on a sharings object and does not perform a transaction on its own. + +---------- +Parameters +---------- + +#. ``sharings`` - ``any``: object with sharings info +#. ``originator`` - ``string``: Ethereum account id of the sharing user +#. ``partner`` - ``string``: Ethereum account id for which key shall be added +#. ``section`` - ``string``: data section the key is intended for or '*' +#. ``block`` - ``number|string``: starting with this block, the key is valid +#. ``sharingKey`` - ``string``: key to share +#. ``context`` - ``string`` (optional): context to share key in + +------- +Returns +------- + +``Promise`` returns ``any``: updated sharings info + +------- +Example +------- + +.. code-block:: typescript + + const sharings = {}; + await this.options.sharing.extendSharings(sharings, accountId, accountId, '*', blockNr, contentKey); + await this.options.sharing.extendSharings(sharings, accountId, accountId, '*', 'hashKey', hashKey); + + + +-------------------------------------------------------------------------------- + +.. _sharing_getKey: + +getKey +================================================================================ + +.. code-block:: typescript + + sharing.getKey(address, partner, section[, block, sharingId]); + +Get a content key from the sharing of a contract. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: contract address or ENS address +#. ``partner`` - ``string``: Ethereum account id for which key shall be retrieved +#. ``section`` - ``string``: data section the key is intended for or '*' +#. ``block`` - ``number|string`` (optional): starting with this block, the key is valid, defaults to ``Number.MAX_SAFE_INTEGER`` +#. ``sharingId`` - ``string`` (optional): id of a sharing (when multi-sharings is used), defaults to ``null`` + +------- +Returns +------- + +``Promise`` returns ``string``: matching key + +------- +Example +------- + +.. code-block:: typescript + + // a sample user + const user2 = '0x0000000000000000000000000000000000000002'; + // user2 wants to read a key after receiving a sharing + // the key requested should be valid for all contexts ('*') and valid up to and including block 100 + const key = await sharing.getKey(contract.options.address, user2, '*', 100); + + + +-------------------------------------------------------------------------------- + +.. _sharing_ensureHashKey: + +ensureHashKey +================================================================================ + +.. code-block:: typescript + + sharing.ensureHashKey(address, originator, partner, hashKey[, context, sharingId]); + +Give hash key "hashKey" to account "partner", if this account does not have a hash key already. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: contract address or ENS address +#. ``originator`` - ``string``: Ethereum account id of the sharing user +#. ``partner`` - ``string``: Ethereum account id for which key shall be added +#. ``hashKey`` - ``string``: key for DFS hashes +#. ``context`` - ``string`` (optional): context to share key in +#. ``sharingId`` - ``string`` (optional): id of a sharing (when multi-sharings is used) + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const hashCryptor = cryptoProvider.getCryptorByCryptoAlgo('aesEcb'); + const hashKey = await hashCryptor.generateKey(); + await sharing.ensureHashKey(contract.options.address, accounts[0], accounts[1], hashKey); + + + +-------------------------------------------------------------------------------- + +.. _sharing_getHashKey: + +getHashKey +================================================================================ + +.. code-block:: typescript + + sharing.getHashKey(address, partner[, sharingid]); + +Function description + +---------- +Parameters +---------- + +#. ``address`` - ``string``: contract address or ENS address +#. ``partner`` - ``string``: Ethereum account id for which key shall be retrieved +#. ``sharingId`` - ``string`` (optional): id of a sharing (when multi-sharings is used) + +------- +Returns +------- + +``Promise`` returns ``string``: matching key + +------- +Example +------- + +.. code-block:: typescript + + const hashCryptor = cryptoProvider.getCryptorByCryptoAlgo('aesEcb'); + const hashKey = await hashCryptor.generateKey(); + await sharing.ensureHashKey(contract.options.address, accounts[0], accounts[1], hashKey); + const rerieved = sharing.ensureHashKey(contract.options.address, accounts[1]); + console.log(hashKey === retrieved); + // Output: + // true + + + +-------------------------------------------------------------------------------- + +.. _sharing_getSharings: + +getSharings +================================================================================ + +.. code-block:: typescript + + sharing.getSharings(address[, _partner, _section, _block, sharingId]); + +Get sharing from a contract, if _partner, _section, _block matches. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: contract address or ENS address +#. ``_partner`` - ``string`` (optional): Ethereum account id for which key shall be retrieved +#. ``_section`` - ``string`` (optional): data section the key is intended for or '*' +#. ``_block`` - ``number`` (optional): starting with this block, the key is valid +#. ``sharingId`` - ``string`` (optional): id of a sharing (when multi-sharings is used) + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const randomSecret = `super secret; ${Math.random()}`; + await sharing.addSharing(testAddress, accounts[1], accounts[0], '*', 0, randomSecret); + const sharings = await sharing.getSharings(testAddress); + + + +-------------------------------------------------------------------------------- + +.. _sharing_removeSharing: + +removeSharing +================================================================================ + +.. code-block:: typescript + + sharing.removeSharing(address, originator, partner, section[, sharingId]); + +Remove a sharing key from a contract with sharing info. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: contract address or ENS address +#. ``originator`` - ``string``: Ethereum account id of the sharing user +#. ``partner`` - ``string``: Ethereum account id for which key shall be removed +#. ``section`` - ``string``: data section of the key +#. ``sharingId`` - ``string`` (optional): id of a sharing (when multi-sharings is used), defaults to ``null`` + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', 0, randomSecret); + + let sharings = await sharing.getSharings(contract.options.address); + console.log(Object.keys(sharings[nameResolver.soliditySha3(accounts[1])]).length); + // Output: + // 1 + + await sharing.removeSharing(contract.options.address, accounts[0], accounts[1], '*'); + + let sharings = await sharing.getSharings(contract.options.address); + console.log(Object.keys(sharings[nameResolver.soliditySha3(accounts[1])]).length); + // Output: + // 0 + + + +-------------------------------------------------------------------------------- + +.. _sharing_addHashToCache: + +addHashToCache +================================================================================ + +.. code-block:: typescript + + sharing.addHashToCache(address, sharingHash[, sharingId]); + +Add a hash to to cache, can be used to speed up sharing key retrieval, when sharings hash is already known. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: contract address +#. ``sharingHash`` - ``string``: bytes32 hash of a sharing +#. ``sharingId`` - ``string`` (optional): id of a multisharing, defaults to ``null`` + +------- +Example +------- + +.. code-block:: typescript + + sharing.addHashToCache(contract.options.address, sharingHash, sharingId); + + + +-------------------------------------------------------------------------------- + +.. _sharing_clearCache: + +clearCache +================================================================================ + +.. code-block:: typescript + + sharing.clearCache(); + +Clear caches and fetch new hashes and sharing on next request. + +------- +Example +------- + +.. code-block:: typescript + + sharing.clearCache(); + + +.. required for building markup + +.. |source contractLoader| replace:: ``ContractLoader`` +.. _source contractLoader: /contracts/contract-loader.html + +.. |source cryptoProvider| replace:: ``CryptoProvider`` +.. _source cryptoProvider: /encryption/crypto-provider.html + +.. |source description| replace:: ``Description`` +.. _source description: /blockchain/description.html + +.. |source dfsInterface| replace:: ``DfsInterface`` +.. _source dfsInterface: /dfs/dfs-interface.html + +.. |source executor| replace:: ``Executor`` +.. _source executor: /blockchain/executor.html + +.. |source keyProvider| replace:: ``KeyProvider`` +.. _source keyProvider: /key-provider + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source nameResolver| replace:: ``NameResolver`` +.. _source nameResolver: /blockchain/name-resolver.html \ No newline at end of file diff --git a/docs/dfs/dfs-interface.rst b/docs/dfs/dfs-interface.rst new file mode 100644 index 00000000..b2bbd5fb --- /dev/null +++ b/docs/dfs/dfs-interface.rst @@ -0,0 +1,156 @@ +================================================================================ +DFS Interface +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Interface Name + - DfsInterface + * - Source + - `dfs-interface.ts `_ + +The `DfsInterface `_ is used to add or get files from the distributed file system. It is the only class, that has to be used before having access to a runtime, when using the `createDefaultRuntime`. + +Internally the modules use the `DfsInterface `_ to access data as well. As the actual implementation of the file access may vary, an instance of the interface has to be created beforehand and passed to the `createDefaultRuntime` function. An implementation called `Ipfs `_, that relies on the `IPFS framework `_ is included as in the package. + +------------------------------------------------------------------------------ + +.. _dfs_add: + +add +=================== + +.. code-block:: javascript + + dfs.add(name, data); + +add content to ipfs +file content is converted to Buffer (in NodeJS) or an equivalent "polyfill" (in browsers) + +---------- +Parameters +---------- + +#. ``name`` - ``string``: name of the added file +#. ``data`` - ``buffer``: data (as buffer) of the added file + +------- +Returns +------- + +``string``: ipfs hash of the data. + +------- +Example +------- + +.. code-block:: javascript + + const fileHash = await runtime.dfs.add( + 'about-maika-1.txt', + Buffer.from('we have a cat called "Maika"', 'utf-8'), + ); + console.log(fileHash); + // Output: + // 0x695adc2137f1f069ff697aa287d0eae486521925a23482f180b3ae4e6dbf8d70 + +------------------------------------------------------------------------------ + +.. _dfs_addMultiple: + +addMultiple +=================== + +.. code-block:: javascript + + dfs.addMultiple(files); + +Multiple files can be added at once. This way of adding should be preferred for performance reasons, when adding files, as requests are combined. + +---------- +Parameters +---------- + +#. ``files`` - ``FileToAdd[]``: array with files to add + +------- +Returns +------- + +``Promise`` resolves to ``string[]``: ipfs hash array of the data. + +------- +Example +------- + +.. code-block:: javascript + + const fileHashes = await runtime.dfs.addMultiple([{ + path: 'about-maika-1.txt', + content: Buffer.from('we have a cat called "Maika"', 'utf-8'), + }, { + path: 'about-maika-2.txt', + content: Buffer.from('she can be grumpy from time to time"', 'utf-8'), + } + ]); + console.dir(fileHashes); + // Output: + // [ '0x695adc2137f1f069ff697aa287d0eae486521925a23482f180b3ae4e6dbf8d70', + // '0x6b85c8b24b59b12a630141143c05bbf40a8adc56a8753af4aa41ebacf108b2e7' ] + +------------------------------------------------------------------------------ + + +.. _dfs_get: + +get +=================== + +.. code-block:: javascript + + dfs.get(hash, returnBuffer); + +get data from ipfs by ipfs hash + +---------- +Parameters +---------- + +#. ``hash`` - ``string``: ipfs hash (or bytes32 encoded) of the data +#. ``returnBuffer`` - ``bool``: should the function return the plain buffer, defaults to ``false`` + +------- +Returns +------- + +``Promise`` resolves to ``string | buffer``: data as text or buffer. + +------- +Example +------- + +.. code-block:: javascript + + const fileBuffer = await runtime.dfs.get('0x695adc2137f1f069ff697aa287d0eae486521925a23482f180b3ae4e6dbf8d70'); + console.log(fileBuffer.toString('utf-8')); + // Output: + // we have a cat called "Maika" + +------------------------------------------------------------------------------ + += Additional Components = +========================== + +Interfaces +================ + +.. _dfs_FileToAdd: + +---------- +FileToAdd +---------- + +#. ``path`` - ``string``: name of the added file +#. ``content`` - ``buffer``: data (as buffer) of the added file \ No newline at end of file diff --git a/docs/dfs/index.rst b/docs/dfs/index.rst new file mode 100644 index 00000000..b0d32f89 --- /dev/null +++ b/docs/dfs/index.rst @@ -0,0 +1,13 @@ +================================================================================ +DFS (Distributed File System) +================================================================================ + +.. toctree:: + :glob: + :maxdepth: 1 + + dfs-interface + ipfs + ipld + +The DFS section handles modules, that deal with managing data in distributed file system, like the DFS interface, its implementation for IPFS and the IPLD graph builder, that works on top of the IPFS impementation. \ No newline at end of file diff --git a/docs/dfs/ipfs.rst b/docs/dfs/ipfs.rst new file mode 100644 index 00000000..915e5890 --- /dev/null +++ b/docs/dfs/ipfs.rst @@ -0,0 +1,314 @@ +================================================================================ +IPFS +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Ipfs + * - Implements + - `DfsInterface `_ + * - Extends + - `Logger `_ + * - Source + - `ipfs.ts `_ + * - Tests + - `ipfs.spec.ts `_ + + +This is `DfsInterface `_ implementation, that relies on the `IPFS `_ framework. + +.. _ipfs_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new Ipfs(options); + +Creates a new IPFS instance. + +---------- +Parameters +---------- + +#. ``options`` - ``IpfsOptions``: options for IPFS constructor. + * ``remoteNode`` - ``any``: ipfs-api instance to remote server + * ``cache`` - |source dfsCache|_ (optional): |source dfsCache|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``Ipfs`` instance + +------- +Example +------- + +.. code-block:: typescript + + const ipfs = new Ipfs({ + remoteNode, + cache + }); + + +------------------------------------------------------------------------------ + +.. _ipfs_ipfsHashToBytes32: + +ipfsHashToBytes32 +=================== + +.. code-block:: javascript + + dfs.ipfsHashToBytes32(hash); + +convert IPFS hash to bytes 32 see https://www.reddit.com/r/ethdev/comments/6lbmhy/a_practical_guide_to_cheap_ipfs_hash_storage_in + +---------- +Parameters +---------- + +#. ``hash`` - ``string``: IPFS hash + +------- +Returns +------- + +``string``: bytes32 string. + +------- +Example +------- + +.. code-block:: javascript + + runtime.dfs.ipfsHashToBytes32('QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz') + // returns 0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89 + +------------------------------------------------------------------------------ + +.. _ipfs_bytes32ToIpfsHash: + +bytes32ToIpfsHash +=================== + +.. code-block:: javascript + + dfs.bytes32ToIpfsHash(str); + +convert bytes32 to IPFS hash see https://www.reddit.com/r/ethdev/comments/6lbmhy/a_practical_guide_to_cheap_ipfs_hash_storage_in + +---------- +Parameters +---------- + +#. ``str`` - ``string``: bytes32 string + +------- +Returns +------- + +``string``: IPFS Hash. + +------- +Example +------- + +.. code-block:: javascript + + runtime.dfs.ipfsHashToBytes32('0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F8') + // returns QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz + +------------------------------------------------------------------------------ + +.. _ipfs_add: + +add +=================== + +.. code-block:: javascript + + dfs.add(name, data); + +add content to ipfs +file content is converted to Buffer (in NodeJS) or an equivalent "polyfill" (in browsers) + +---------- +Parameters +---------- + +#. ``name`` - ``string``: name of the added file +#. ``data`` - ``buffer``: data (as buffer) of the added file + +------- +Returns +------- + +``string``: ipfs hash of the data. + +------- +Example +------- + +.. code-block:: javascript + + const fileHash = await runtime.dfs.add( + 'about-maika-1.txt', + Buffer.from('we have a cat called "Maika"', 'utf-8'), + ); + console.log(fileHash); + // Output: + // 0x695adc2137f1f069ff697aa287d0eae486521925a23482f180b3ae4e6dbf8d70 + +------------------------------------------------------------------------------ + +.. _ipfs_addMultiple: + +addMultiple +=================== + +.. code-block:: javascript + + dfs.addMultiple(files); + +Multiple files can be added at once. This way of adding should be preferred for performance reasons, when adding files, as requests are combined. + +---------- +Parameters +---------- + +#. ``files`` - ``FileToAdd[]``: array with files to add + +------- +Returns +------- + +``Promise`` resolves to ``string[]``: ipfs hash array of the data. + +------- +Example +------- + +.. code-block:: javascript + + const fileHashes = await runtime.dfs.addMultiple([{ + path: 'about-maika-1.txt', + content: Buffer.from('we have a cat called "Maika"', 'utf-8'), + }, { + path: 'about-maika-2.txt', + content: Buffer.from('she can be grumpy from time to time"', 'utf-8'), + } + ]); + console.dir(fileHashes); + // Output: + // [ '0x695adc2137f1f069ff697aa287d0eae486521925a23482f180b3ae4e6dbf8d70', + // '0x6b85c8b24b59b12a630141143c05bbf40a8adc56a8753af4aa41ebacf108b2e7' ] + +------------------------------------------------------------------------------ + +.. _ipfs_pinFileHash: + +pinFileHash +=================== + +.. code-block:: javascript + + dfs.pinFileHash(hash); + +pins file hashes on ipfs cluster + +---------- +Parameters +---------- + +#. ``hash`` - ``string``: filehash of the pinned item + +------- +Returns +------- + +``Promise`` resolves to ``void``: resolved when done. + +------- +Example +------- + +.. code-block:: javascript + + const fileBuffer = await runtime.dfs.pinFileHash('QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz'); + +------------------------------------------------------------------------------ + + +.. _ipfs_get: + +get +=================== + +.. code-block:: javascript + + dfs.get(hash, returnBuffer); + +get data from ipfs by ipfs hash + +---------- +Parameters +---------- + +#. ``hash`` - ``string``: ipfs hash (or bytes32 encoded) of the data +#. ``returnBuffer`` - ``bool``: should the function return the plain buffer, defaults to ``false`` + +------- +Returns +------- + +``Promise`` resolves to ``string | buffer``: data as text or buffer. + +------- +Example +------- + +.. code-block:: javascript + + const fileBuffer = await runtime.dfs.get('0x695adc2137f1f069ff697aa287d0eae486521925a23482f180b3ae4e6dbf8d70'); + console.log(fileBuffer.toString('utf-8')); + // Output: + // we have a cat called "Maika" + +------------------------------------------------------------------------------ + += Additional Components = +========================= + +Interfaces +================ + +.. _ipfs_FileToAdd: + +---------- +FileToAdd +---------- + +#. ``path`` - ``string``: name of the added file +#. ``content`` - ``buffer``: data (as buffer) of the added file + +.. required for building markup + +.. |source dfsCache| replace:: ``DfsCacheInterface`` +.. _source dfsCache: /dfs/dfs-interface.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface \ No newline at end of file diff --git a/docs/dfs/ipld.rst b/docs/dfs/ipld.rst new file mode 100644 index 00000000..b7a2e93a --- /dev/null +++ b/docs/dfs/ipld.rst @@ -0,0 +1,473 @@ +================================================================================ +IPLD +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Ipld + * - Extends + - `Logger `_ + * - Source + - `ipld.ts `_ + * - Tests + - `ipld.spec.ts `_ + + +`IPLD `_ is a way to store data as trees. The used implementation relies on `js-ipld-graph-builder `_ for iterating over tree nodes and setting new subtrees, but uses a few modifications to the standard: +- nodes are not stored as `IPFS DAGs `_, but stored as play JSON IPFS files +- nodes, that are encrypted, contain the property `cryptoInfo` for decryption (see `Encryption `_) + + + +-------------------------------------------------------------------------------- + +.. _ipld_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new IPLD(options); + +Creates a new Ipld instance. + +Requires + +---------- +Parameters +---------- + +#. ``options`` - ``IpldOptions``: The options used for calling + * ``cryptoProvider`` - |source cryptoProvider|_: |source cryptoProvider|_ instance + * ``defaultCryptoAlgo`` - ``string``: default encryption algorithm + * ``ipfs`` - |source ipfs|_: |source ipfs|_ instance + * ``keyProvider`` - |source keyProviderInterface|_: |source keyProviderInterface|_ instance + * ``originator`` - ``string``: originator of tree (default encryption context) + * ``nameResolver`` - |source nameResolver|_: |source nameResolver|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``IpldOptions`` instance + +------- +Example +------- + +.. code-block:: typescript + + const ipld = new IPLD(options); + + + +-------------------------------------------------------------------------------- + +.. _ipld_store: + +store +================================================================================ + +.. code-block:: typescript + + ipld.store(toSet); + +Store tree, if tree contains merklefied links, stores tree with multiple linked subtrees. +Hashes returned from this function represent the the final tree, that can be stored as bytes32 hashes in smart contracts, etc. + +---------- +Parameters +---------- + +#. ``toSet`` - ``any``: tree to store + +------- +Returns +------- + +``Promise`` returns ``string``: hash reference to a tree with with merklefied links + +------- +Example +------- + +.. code-block:: typescript + + const sampleObject = { + personalInfo: { + firstName: 'eris', + }, + }; + const stored = await ipld.store(Object.assign({}, sampleObject)); + console.log(stored); + // Output: + // 0x12f6526dbe223eddd6c6a0fb7df118c87c56d34bf0c845b54bdca2fec0f3017d + +When storing nested trees created with _ipld_set_, subtrees at junction points are stored as separate trees, then converted to serialized buffers, which are automatically deserialized and cast back when calling ipld_getLinkedGraph_. + +.. code-block:: typescript + + console.log(JSON.stringify(extended, null, 2)); + const extendedstored = await ipld.store(Object.assign({}, extended)); + // Output: + // "0xc74f6946aacbbd1418ddd7dec83a5bcd3710b384de767d529e624f9f08cbf9b4" + const loaded = await ipld.getLinkedGraph(extendedstored, ''); + console.log(JSON.stringify(Ipld.purgeCryptoInfo(loaded), null, 2)); + // Output: + // + // "personalInfo": { + // "firstName": "eris" + // }, + // "dapps": { + // "/": { + // "type": "Buffer", + // "data": [ 18, 32, 246, 21, 166, 135, 236, 212, 70, 130, 94, 47, 81, 135, 153, 154, 201, 69, 109, 249, 97, 84, 252, 56, 214, 195, 149, 133, 116, 253, 19, 87, 217, 66 ] + // } + // } + // + + + +-------------------------------------------------------------------------------- + +.. _ipld_getLinkedGraph: + +getLinkedGraph +================================================================================ + +.. code-block:: typescript + + ipld.getLinkedGraph(graphReference[, path]); + +Get a path from a tree; resolve subtrees only if required (depends on requested path). + +---------- +Parameters +---------- + +#. ``graphReference`` - ``string | Buffer | any``: hash/buffer to look up or a graph object +#. ``path`` - ``string`` (optional): path in the tree, defaults to ``''`` + +------- +Returns +------- + +``Promise`` returns ``any``: linked graph + +------- +Example +------- + +To retrieve data from IPLD trees, use the `bytes32` hash from storing the data: + +.. code-block:: typescript + + const stored = '0x12f6526dbe223eddd6c6a0fb7df118c87c56d34bf0c845b54bdca2fec0f3017d'; + const loaded = await ipld.getLinkedGraph(stored, ''); + console.dir(Ipld.purgeCryptoInfo(loaded)); + // Output: + // { personalInfo: { firstName: 'eris' } } + +For info about the ``Ipld.purgeCryptoInfo`` part see :doc:`Encryption `. + +The second argument is the path inside the tree. Passing '' means "retrieve data from root level". To get more specifc data, provide a path: + +.. code-block:: typescript + + const stored = '0x12f6526dbe223eddd6c6a0fb7df118c87c56d34bf0c845b54bdca2fec0f3017d'; + const loaded = await ipld.getLinkedGraph(stored, 'personalInfo'); + console.dir(Ipld.purgeCryptoInfo(loaded)); + // Output: + // { firstName: 'eris' } + + +.. code-block:: typescript + + const stored = '0x12f6526dbe223eddd6c6a0fb7df118c87c56d34bf0c845b54bdca2fec0f3017d'; + const loaded = await ipld.getLinkedGraph(stored, 'personalInfo/firstName'); + console.dir(Ipld.purgeCryptoInfo(loaded)); + // Output: + // 'eris' + + + +-------------------------------------------------------------------------------- + +.. _ipld_getResolvedGraph: + +getResolvedGraph +================================================================================ + +.. code-block:: typescript + + ipld.getResolvedGraph(graphReference[, path, depth]); + +Get a path from a tree; resolve links in paths up to depth (default is 10). + +This function is for **debugging and analysis purposes only**, it tries to resolve the entire graph, which would be too much requests in most scenarios. If resolving graphs, prefer using ipld_getLinkedGraph_, with specific queries into the tree, that limit the resolve requests. + +---------- +Parameters +---------- + +#. ``graphReference`` - ``string | Buffer | any``: hash/buffer to look up or a graph object +#. ``path`` - ``string`` (optional): path in the tree, defaults to ``''`` +#. ``depth`` - ``number`` (optional): resolve up do this many levels of depth, defaults to ``10`` + +------- +Returns +------- + +``Promise`` returns ``any``: resolved graph + +------- +Example +------- + +.. code-block:: typescript + + const treeHash = '0xc74f6946aacbbd1418ddd7dec83a5bcd3710b384de767d529e624f9f08cbf9b4'; + console.dir(await ipld.getResolvedGraph(treeHash, '')); + // Output: + // { personalInfo: { firstName: 'eris' }, + // dapps: { '/': { contracts: [Array], cryptoInfo: [Object] } }, + // cryptoInfo: + // { originator: '0xd7c759941fa3962e4833707f2f44f8cb11b471916fb6f9f0facb03119628234e', + // keyLength: 256, + // algorithm: 'aes-256-cbc' } } + +Compared to ipld_getLinkedGraph_: + +.. code-block:: typescript + + const treeHash = '0xc74f6946aacbbd1418ddd7dec83a5bcd3710b384de767d529e624f9f08cbf9b4'; + console.dir(await ipld.getLinkGraph(treeHash, '')); + // Output: + // { personalInfo: { firstName: 'eris' }, + // dapps: + // { '/': + // Buffer [18, 32, 246, 21, 166, 135, 236, 212, 70, 130, 94, 47, 81, 135, 153, 154, 201, 69, 109, 249, 97, 84, 252, 56, 214, 195, 149, 133, 116, 253, 19, 87, 217, 66] }, + // cryptoInfo: + // { originator: '0xd7c759941fa3962e4833707f2f44f8cb11b471916fb6f9f0facb03119628234e', + // keyLength: 256, + // algorithm: 'aes-256-cbc' } } + + + +-------------------------------------------------------------------------------- + +.. _ipld_set: + +set +================================================================================ + +.. code-block:: typescript + + ipld.set(tree, path, subtree[, plainObject, cryptoInfo]); + +Set a value to a tree node; inserts new element as a linked subtree by default. + +What's pretty useful about IPLD graphs is, that not only plain JSON trees can be stored, but that those trees can be linked to other graphs, which makes it possible to build very powerful tree structures, that consist of multiple separate trees, that can be used on their own or in a tree, that combines all of those. The resulting hash is again ``bytes32`` hash and this can be stored in smart contracts like any other IPFS hash. + +This function adds the given subtree under a path in the existing tree. Different subtrees can be added by using this function multiple times. The final tree can then be stored to IPFS with ipld_store_. + +---------- +Parameters +---------- + +#. ``tree`` - ``any``: tree to extend +#. ``path`` - ``string``: path of inserted element +#. ``subtree`` - ``any``: element that will be added +#. ``plainObject`` - ``boolean`` (optional): do not link values as new subtree, defaults to ``false`` +#. ``cryptoInfo`` - ``CryptoInfo`` (optional): crypto info for encrypting subtree + +------- +Returns +------- + +``Promise`` returns ``any``: tree with merklefied links + +------- +Example +------- + +.. code-block:: typescript + + const sampleObject = { + personalInfo: { + firstName: 'eris', + }, + }; + const sub = { + contracts: ['0x01', '0x02', '0x03'] + }; + const extended = await ipld.set( + sampleObject, // extend this graph + 'dapps', // attach the subgraph under the path "dapps" + sub, // attach this graph as a subgraph + ); + console.log(JSON.stringify(extended, null, 2)); + // Output: + // { + // "personalInfo": { + // "firstName": "eris" + // }, + // "dapps": { + // "/": { + // "contracts": [ + // "0x01", + // "0x02", + // "0x03" + // ] + // } + // } + // } + + + +-------------------------------------------------------------------------------- + +.. _ipld_remove: + +remove +================================================================================ + +.. code-block:: typescript + + ipld.remove(tree, path); + +Delete a value from a tree node. + +---------- +Parameters +---------- + +#. ``tree`` - ``any``: tree to extend +#. ``string`` - ``string``: path of inserted element + +------- +Returns +------- + +``Promise`` returns ``any``: tree with merklefied links + +------- +Example +------- + +.. code-block:: typescript + + const treeHash = '0xc74f6946aacbbd1418ddd7dec83a5bcd3710b384de767d529e624f9f08cbf9b4'; + const loaded = await ipld.getLinkedGraph(treeHash, ''); + console.log(loaded); + // Output: + // { personalInfo: { firstName: 'eris' }, + // dapps: + // { '/': }, + // cryptoInfo: + // { originator: '0xd7c759941fa3962e4833707f2f44f8cb11b471916fb6f9f0facb03119628234e', + // keyLength: 256, + // algorithm: 'aes-256-cbc' } } + + const updated = await ipld.remove(loaded, 'dapps'); + console.log(updated); + // Output: + // { personalInfo: { firstName: 'eris' }, + // cryptoInfo: + // { originator: '0xd7c759941fa3962e4833707f2f44f8cb11b471916fb6f9f0facb03119628234e', + // keyLength: 256, + // algorithm: 'aes-256-cbc' } } + + + +-------------------------------------------------------------------------------- + +.. _ipld_purgeCryptoInfo: + +purgeCryptoInfo +================================================================================ + +.. code-block:: typescript + + Ipld.purgeCryptoInfo(toPurge); + +(static class function) + +Remove all cryptoInfos from tree. + +Some example here use ``Ipld.purgeCryptoInfo`` to cleanup the objects before logging them. This is done, because IPLD graphs are encrypted by default, which has a few impact on the data stored: + + - The root node of a tree is "encrypted" with the encryption algorithm "unencrypted", resulting in the root node having its data stored as a Buffer. This is done to keep the root node in the same format as the other nodes, as: + - Nodes in the Tree are encrypted. This encryption is specified in the constructor as `defaultCryptoAlgo`. + - All nodes are en- or decrypted with the same account or "originator". The originator, that is used, is specified in the constructor as "originator". This means, that the IPLD instance is account bound and a new instance has to be created if another account should be used. + +---------- +Parameters +---------- + +#. ``toPurge`` - ``any``: The options used for calling + +------- +Returns +------- + +``void`` + +------- +Example +------- + +To show the difference, without purging: + +.. code-block:: typescript + + const stored = '0x12f6526dbe223eddd6c6a0fb7df118c87c56d34bf0c845b54bdca2fec0f3017d'; + const loaded = await ipld.getLinkedGraph(stored, ''); + console.dir(loaded); + // Output: + // { personalInfo: { firstName: 'eris' }, + // cryptoInfo: + // { originator: '0xd7c759941fa3962e4833707f2f44f8cb11b471916fb6f9f0facb03119628234e', + // keyLength: 256, + // algorithm: 'aes-256-cbc' } } + // + +With purging: + +.. code-block:: typescript + + const stored = '0x12f6526dbe223eddd6c6a0fb7df118c87c56d34bf0c845b54bdca2fec0f3017d'; + const loaded = await ipld.getLinkedGraph(stored, ''); + console.dir(Ipld.purgeCryptoInfo(loaded)); + // Output: + // { personalInfo: { firstName: 'eris' } } + + + +.. required for building markup + +.. |source cryptoProvider| replace:: ``CryptoProvider`` +.. _source cryptoProvider: /encryption/crypto-provider.html + +.. |source ipfs| replace:: ``Ipfs`` +.. _source ipfs: /dfs/ipfs.html + +.. |source keyProviderInterface| replace:: ``KeyProviderInterface`` +.. _source keyProviderInterface: /encryption/key-provider.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source nameResolver| replace:: ``NameResolver`` +.. _source nameResolver: /blockchain/name-resolver.html \ No newline at end of file diff --git a/docs/encryption/crypto-provider.rst b/docs/encryption/crypto-provider.rst new file mode 100644 index 00000000..26e43b01 --- /dev/null +++ b/docs/encryption/crypto-provider.rst @@ -0,0 +1,191 @@ +================================================================================ +Crypto Provider +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - CryptoProvider + * - Extends + - `CryptoProvider `_ + * - Source + - `crypto-provider.ts `_ + +The `CryptoProvider `_ is a container for supported `Cryptors <#cryptors>`_ and is able to determine, which `Cryptor <#cryptors>`_ to use for encryption / decryption. + +------------------------------------------------------------------------------ + +.. _crypto_provider_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new CryptoProvider(cryptors); + +Creates a new CryptoProvider instance. + +---------- +Parameters +---------- + +#. ``cryptors`` - ``any``: object with available |source cryptors|_. + +------- +Returns +------- + +``CryptoProvider`` instance + +------- +Example +------- + +.. code-block:: typescript + + const serviceContract = new CryptoProvider({ + cryptors: { + aes: new Aes(), + unencrypted: new Unencrypted() + } + }); + + + +-------------------------------------------------------------------------------- + +.. _crypto_provider_getCryptorByCryptoAlgo: + +getCryptorByCryptoAlgo +======================= + +.. code-block:: javascript + + cryptoProvider.getCryptorByCryptoAlgo(cryptoAlgo); + +get a Cryptor matching the crypto algorithm + +---------- +Parameters +---------- + +#. ``cryptoAlgo`` - ``string``: crypto algorithm + +------- +Returns +------- + +``Cryptor``: matching cryptor. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = runtime.cryptoProvider.getCryptorByCryptoAlgo('aes'); + +------------------------------------------------------------------------------ + +.. _crypto_provider_getCryptorByCryptoInfo: + +getCryptorByCryptoInfo +======================= + +.. code-block:: javascript + + cryptoProvider.getCryptorByCryptoInfo(info); + +get a Cryptor matching the provided CryptoInfo + +---------- +Parameters +---------- + +#. ``info`` - ``CryptoInfo``: details about en-/decryption + +------- +Returns +------- + +``Cryptor``: matching cryptor. + +------- +Example +------- + +.. code-block:: javascript + + const cryptoInfo = { + "public": { + "name": "envelope example" + }, + "private": "...", + "cryptoInfo": { + "algorithm": "unencrypted", + "keyLength": 256, + "originator": "0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002", + "block": 123 + } + }; + const cryptor = runtime.cryptoProvider.getCryptorByCryptoInfo(cryptoInfo); + +------------------------------------------------------------------------------ + += Additional Components = +========================== + +Interfaces +================ + +.. _crypto_provider_cryptor: + +---------- +Cryptor +---------- + +#. ``options`` - ``any``: options which will passed to the cryptor to work (like key for encryption) +#. ``generateKey`` - ``function``: generates a random key for encryption/decryption +#. ``getCryptoInfo`` - ``function``: returns a empty CryptoInfo object for the current Cryptor +#. ``encrypt`` - ``function``: function to encrypt a given message +#. ``decrypt`` - ``function``: function to decrypt a given message + +.. _crypto_provider_envelope: + +---------- +Envelope +---------- + +#. ``algorithm`` - ``string``: algorithm used for encryption +#. ``block`` - ``number`` (optional): block number for which related item is encrypted +#. ``cryptorVersion`` - ``number`` (optional): version of the cryptor used. describes the implementation applied during decryption and not the algorithm version. +#. ``originator`` - ``string`` (optional): context for encryption, this can be + + - a context known to all parties (e.g. key exchange) + + - a key exchanged between two accounts (e.g. bmails) + + - a key from a sharings info from a contract (e.g. DataContract) + + defaults to 0 + +#. ``keyLength`` - ``number`` (optional): length of the key used in encryption + +.. _crypto_provider_cryptoinfo: + +---------- +CryptoInfo +---------- + +#. ``public`` - ``any`` (optional): unencrypted part of the data; will stay as is during encryption +#. ``private`` - ``any`` (optional): encrypted part of the data. If encrypting, this part will be encrypted, depending on the encryption. If already encrypted, this will be the encrypted value +#. ``cryptoInfo`` - ``CryptoInfo``: describes used encryption + + +.. required for building markup + +.. |source cryptors| replace:: ``Cryptors`` +.. _source cryptors: /encryption/crypto-provider.html#cryptors \ No newline at end of file diff --git a/docs/encryption/cryptor-aes-blob.rst b/docs/encryption/cryptor-aes-blob.rst new file mode 100644 index 00000000..a91023a0 --- /dev/null +++ b/docs/encryption/cryptor-aes-blob.rst @@ -0,0 +1,206 @@ +================================================================================ +Cryptor - AES Blob +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - AesBlob + * - Implements + - `Cryptor `_ + * - Extends + - `Logger `_ + * - Source + - `aes-blob.ts `_ + * - Tests + - `aes-blob.spec.ts `_ + +The `AES Blob `_ cryptor encodes and decodes content with aes-cbc. + +------------------------------------------------------------------------------ + +.. _cryptor_aes_blob_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new AesBlob(options); + +Creates a new AesBlob instance. + +---------- +Parameters +---------- + +#. ``options`` - ``AesBlobOptions``: options for AesBlob constructor. + * ``dfs`` - |source dfsInterface|_: |source dfsInterface|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``AesBlob`` instance + +------- +Example +------- + +.. code-block:: typescript + + const aesBlob = new AesBlob({ + dfs + }); + + +------------------------------------------------------------------------------ + +.. _cryptor_aes_blob_getCryptoInfo: + +getCryptoInfo +=================== + +.. code-block:: javascript + + cryptor.getCryptoInfo(originator); + +create new crypto info for this cryptor + +---------- +Parameters +---------- + +#. ``originator`` - ``string``: originator or context of the encryption + +------- +Returns +------- + +``CryptoInfo``: details about encryption for originator with this cryptor. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new AesBlob(); + const cryptoInfo = cryptor.getCryptoInfo('0x123'); + +------------------------------------------------------------------------------ + +.. _cryptor_aes_blob_generateKey: + +generateKey +=================== + +.. code-block:: javascript + + cryptor.generateKey(); + +generate key for cryptor/decryption + +------- +Returns +------- + +Promise resolves to ``string``: key used for encryption. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new AesBlob(); + const cryptoInfo = cryptor.generateKey(); + +------------------------------------------------------------------------------ + +.. _cryptor_aes_blob_encrypt: + +encrypt +=================== + +.. code-block:: javascript + + cryptor.encrypt(message, options); + +'encrypt' a message (serializes message) + +---------- +Parameters +---------- + +#. ``message`` - ``string``: message which should be encrypted +#. ``options`` - ``any``: cryptor options + * ``key`` - ``string``: key used for encryption + +------- +Returns +------- + +Promise resolves to ``string``: encrypted message. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new AesBlob(); + const cryptoInfo = cryptor.encrypt('Hello World', { key: '0x12345' }); + +------------------------------------------------------------------------------ + +.. _cryptor_aes_blob_decrypt: + +decrypt +=================== + +.. code-block:: javascript + + cryptor.decrypt(message, options); + +'decrypt' a message (deserializes message) + +---------- +Parameters +---------- + +#. ``message`` - ``Buffer``: message which should be decrypted +#. ``options`` - ``any``: cryptor options + * ``key`` - ``string``: key used for encryption + +------- +Returns +------- + +Promise resolves to ``any``: decrypted message. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new AesBlob(); + const cryptoInfo = cryptor.decrypt('afeweq41f1e61e3f', { key: '0x12345' }); + +.. required for building markup + +.. |source dfsInterface| replace:: ``DfsInterface`` +.. _source dfsInterface: /dfs/dfs-interface.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface \ No newline at end of file diff --git a/docs/encryption/cryptor-aes-ecb.rst b/docs/encryption/cryptor-aes-ecb.rst new file mode 100644 index 00000000..fed009e8 --- /dev/null +++ b/docs/encryption/cryptor-aes-ecb.rst @@ -0,0 +1,200 @@ +================================================================================ +Cryptor - AES ECB +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - AesEcb + * - Implements + - `Cryptor `_ + * - Extends + - `Logger `_ + * - Source + - `aes-ecb.ts `_ + * - Tests + - `aes-ecb.spec.ts `_ + +The `AES ECB `_ cryptor encodes and decodes content with aes-ecb. + +------------------------------------------------------------------------------ + +.. _cryptor_aes_ecb_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new AesEcb(options); + +Creates a new AesEcb instance. + +---------- +Parameters +---------- + +#. ``options`` - ``AesEcbOptions``: options for AesEcb constructor. + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``AesEcb`` instance + +------- +Example +------- + +.. code-block:: typescript + + const aesEcb = new AesEcb(); + + +------------------------------------------------------------------------------ + +.. _cryptor_aes_ecb_getCryptoInfo: + +getCryptoInfo +=================== + +.. code-block:: javascript + + cryptor.getCryptoInfo(originator); + +create new crypto info for this cryptor + +---------- +Parameters +---------- + +#. ``originator`` - ``string``: originator or context of the encryption + +------- +Returns +------- + +``CryptoInfo``: details about encryption for originator with this cryptor. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new AesEcb(); + const cryptoInfo = cryptor.getCryptoInfo('0x123'); + +------------------------------------------------------------------------------ + +.. _cryptor_aes_ecb_generateKey: + +generateKey +=================== + +.. code-block:: javascript + + cryptor.generateKey(); + +generate key for cryptor/decryption + +------- +Returns +------- + +Promise resolves to ``string``: key used for encryption. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new Unencrypted(); + const cryptoInfo = cryptor.generateKey(); + +------------------------------------------------------------------------------ + +.. _cryptor_aes_ecb_encrypt: + +encrypt +=================== + +.. code-block:: javascript + + cryptor.encrypt(message, options); + +'encrypt' a message (serializes message) + +---------- +Parameters +---------- + +#. ``message`` - ``string``: message which should be encrypted +#. ``options`` - ``any``: cryptor options + * ``key`` - ``string``: key used for encryption + +------- +Returns +------- + +Promise resolves to ``string``: encrypted message. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new Unencrypted(); + const cryptoInfo = cryptor.encrypt('Hello World', { key: '0x12345' }); + +------------------------------------------------------------------------------ + +.. _cryptor_aes_ecb_decrypt: + +decrypt +=================== + +.. code-block:: javascript + + cryptor.decrypt(message, options); + +'decrypt' a message (deserializes message) + +---------- +Parameters +---------- + +#. ``message`` - ``Buffer``: message which should be decrypted +#. ``options`` - ``any``: cryptor options + * ``key`` - ``string``: key used for encryption + +------- +Returns +------- + +Promise resolves to ``any``: decrypted message. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new Unencrypted(); + const cryptoInfo = cryptor.decrypt('afeweq41f1e61e3f', { key: '0x12345' }); + +.. required for building markup + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface \ No newline at end of file diff --git a/docs/encryption/cryptor-aes.rst b/docs/encryption/cryptor-aes.rst new file mode 100644 index 00000000..ab313e8b --- /dev/null +++ b/docs/encryption/cryptor-aes.rst @@ -0,0 +1,200 @@ +================================================================================ +Cryptor - AES CBC +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Aes + * - Implements + - `Cryptor `_ + * - Extends + - `Logger `_ + * - Source + - `aes.ts `_ + * - Tests + - `aes.spec.ts `_ + +The `AES `_ cryptor encodes and decodes content with aes-cbc. + +------------------------------------------------------------------------------ + +.. _cryptor_aes_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new Aes(options); + +Creates a new Aes instance. + +---------- +Parameters +---------- + +#. ``options`` - ``AesOptions``: options for Aes constructor. + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``Aes`` instance + +------- +Example +------- + +.. code-block:: typescript + + const aes = new Aes(); + + +------------------------------------------------------------------------------ + +.. _cryptor_aes_getCryptoInfo: + +getCryptoInfo +=================== + +.. code-block:: javascript + + cryptor.getCryptoInfo(originator); + +create new crypto info for this cryptor + +---------- +Parameters +---------- + +#. ``originator`` - ``string``: originator or context of the encryption + +------- +Returns +------- + +``CryptoInfo``: details about encryption for originator with this cryptor. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new Aes(); + const cryptoInfo = cryptor.getCryptoInfo('0x123'); + +------------------------------------------------------------------------------ + +.. _cryptor_aes_generateKey: + +generateKey +=================== + +.. code-block:: javascript + + cryptor.generateKey(); + +generate key for cryptor/decryption + +------- +Returns +------- + +Promise resolves to ``string``: key used for encryption. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new Aes(); + const cryptoInfo = cryptor.generateKey(); + +------------------------------------------------------------------------------ + +.. _cryptor_aes_encrypt: + +encrypt +=================== + +.. code-block:: javascript + + cryptor.encrypt(message, options); + +'encrypt' a message (serializes message) + +---------- +Parameters +---------- + +#. ``message`` - ``string``: message which should be encrypted +#. ``options`` - ``any``: cryptor options + * ``key`` - ``string``: key used for encryption + +------- +Returns +------- + +Promise resolves to ``string``: encrypted message. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new Aes(); + const cryptoInfo = cryptor.encrypt('Hello World', { key: '0x12345' }); + +------------------------------------------------------------------------------ + +.. _cryptor_aes_decrypt: + +decrypt +=================== + +.. code-block:: javascript + + cryptor.decrypt(message, options); + +'decrypt' a message (deserializes message) + +---------- +Parameters +---------- + +#. ``message`` - ``Buffer``: message which should be decrypted +#. ``options`` - ``any``: cryptor options + * ``key`` - ``string``: key used for encryption + +------- +Returns +------- + +Promise resolves to ``any``: decrypted message. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new Aes(); + const cryptoInfo = cryptor.decrypt('afeweq41f1e61e3f', { key: '0x12345' }); + +.. required for building markup + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface \ No newline at end of file diff --git a/docs/encryption/cryptor-unencrypted.rst b/docs/encryption/cryptor-unencrypted.rst new file mode 100644 index 00000000..259ab8ab --- /dev/null +++ b/docs/encryption/cryptor-unencrypted.rst @@ -0,0 +1,197 @@ +================================================================================ +Cryptor - Unencrypted +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Unencrypted + * - Implements + - `Cryptor `_ + * - Extends + - `Logger `_ + * - Source + - `unencrypted.ts `_ + +The `Unencrypted `_ cryptor encodes and decodes content "unencrypted" this means no encryption is applied to the content. So simply the content is public, only HEX encoded. + +------------------------------------------------------------------------------ + +.. _cryptor_unencrypted_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new Unencrypted(options); + +Creates a new Unencrypted instance. + +---------- +Parameters +---------- + +#. ``options`` - ``UnencryptedOptions``: options for Unencrypted constructor. + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``Unencrypted`` instance + +------- +Example +------- + +.. code-block:: typescript + + const unencrypted = new Unencrypted(); + +------------------------------------------------------------------------------ + +.. _cryptor_unencrypted_getCryptoInfo: + +getCryptoInfo +=================== + +.. code-block:: javascript + + cryptor.getCryptoInfo(originator); + +create new crypto info for this cryptor + +---------- +Parameters +---------- + +#. ``originator`` - ``string``: originator or context of the encryption + +------- +Returns +------- + +``CryptoInfo``: details about encryption for originator with this cryptor. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new Unencrypted(); + const cryptoInfo = cryptor.getCryptoInfo('0x123'); + +------------------------------------------------------------------------------ + +.. _cryptor_unencrypted_generateKey: + +generateKey +=================== + +.. code-block:: javascript + + cryptor.generateKey(); + +generate key for cryptor/decryption + +------- +Returns +------- + +Promise resolves to ``string``: key used for encryption. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new Unencrypted(); + const cryptoInfo = cryptor.generateKey(); + +------------------------------------------------------------------------------ + +.. _cryptor_unencrypted_encrypt: + +encrypt +=================== + +.. code-block:: javascript + + cryptor.encrypt(message, options); + +'encrypt' a message (serializes message) + +---------- +Parameters +---------- + +#. ``message`` - ``string``: message which should be encrypted +#. ``options`` - ``any``: cryptor options + * ``key`` - ``string``: key used for encryption + +------- +Returns +------- + +Promise resolves to ``string``: encrypted message. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new Unencrypted(); + const cryptoInfo = cryptor.encrypt('Hello World', { key: '0x12345' }); + +------------------------------------------------------------------------------ + +.. _cryptor_unencrypted_decrypt: + +decrypt +=================== + +.. code-block:: javascript + + cryptor.decrypt(message, options); + +'decrypt' a message (deserializes message) + +---------- +Parameters +---------- + +#. ``message`` - ``Buffer``: message which should be decrypted +#. ``options`` - ``any``: cryptor options + * ``key`` - ``string``: key used for encryption + +------- +Returns +------- + +Promise resolves to ``any``: decrypted message. + +------- +Example +------- + +.. code-block:: javascript + + const cryptor = new Unencrypted(); + const cryptoInfo = cryptor.decrypt('afeweq41f1e61e3f', { key: '0x12345' }); + +.. required for building markup + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface \ No newline at end of file diff --git a/docs/encryption/index.rst b/docs/encryption/index.rst new file mode 100644 index 00000000..371c300f --- /dev/null +++ b/docs/encryption/index.rst @@ -0,0 +1,37 @@ +================================================================================ +Encryption +================================================================================ + +.. toctree:: + :glob: + :maxdepth: 1 + + key-provider + crypto-provider + cryptor-aes + cryptor-aes-ecb + cryptor-aes-blob + cryptor-unencrypted + +To allow extended data security, contents added to DFS can be encrypted before storing them and decrypted before reading them. To allow this encryption support has been added to the library. + +Data is encrypted and stored in so called "Envelopes", which act as container for the data itself and contain enough information for the API to determine which key to use for decryption and where to retrieve the key from. If you were wondering why the `descriptions <#description>`_ had the property `public`, this is the right section for you. + +.. code-block:: javascript + + { + "public": { + "name": "envelope example" + }, + "private": "...", + "cryptoInfo": { + "algorithm": "unencrypted", + "keyLength": 256, + "originator": "0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002", + "block": 123 + } + } + +The "public" section contains data, that is visible without any knowledge of the encryption key. The "private" section can only be decrypted if the user that tries to read the data has access to the encryption key. The `cryptoInfo` part is used to determine which decryption algorithm to use and where to look for it. + +When decrypted, the `private` section takes precedence over the `public` section. This can lead to the private section overwriting sections of the `public` part. For example a public title may be replace with a "true" title (only visible for a group of people) from the private section. diff --git a/docs/encryption/key-provider.rst b/docs/encryption/key-provider.rst new file mode 100644 index 00000000..f133c3a0 --- /dev/null +++ b/docs/encryption/key-provider.rst @@ -0,0 +1,141 @@ +================================================================================ +Key Provider +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - KeyProvider + * - Implements + - `KeyProviderInterface `_ + * - Extends + - `Logger `_ + * - Source + - `key-provider.ts `_ + +The `KeyProvider `_ returns given decryption/encryption keys for a given CryptoInfo. They use a given evan.network profile to retrieve the needed keys to encrypt/decrypt the envelope + +------------------------------------------------------------------------------ + +.. _key_provider_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new KeyProvider(options); + +Creates a new KeyProvider instance. + +---------- +Parameters +---------- + +#. ``options`` - ``KeyProviderOptions``: options for KeyProvider constructor. + * ``keys`` - ``any`` (optional): object with key mappings of accounts + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``KeyProvider`` instance + +------- +Example +------- + +.. code-block:: typescript + + const keyProvider = new KeyProvider({ + keys: { + '0x123': 'abcdeF9043' + } + }); + + +-------------------------------------------------------------------------------- + +.. _key_provider_init: + +init +=================== + +.. code-block:: javascript + + keyProvider.init(_profile); + +initialize a new KeyProvider with a given evan.network Profile + +---------- +Parameters +---------- + +#. ``_profile`` - ``Profile``: initialized evan.network profile + +------- +Example +------- + +.. code-block:: javascript + + runtime.keyProvider.init(runtime.profile); + +------------------------------------------------------------------------------ + +.. _key_provider_getKey: + +getKey +=================== + +.. code-block:: javascript + + keyProvider.getKey(info); + +get a encryption/decryption key for a specific CryptoInfo from the associated AccountStore or the loaded evan.network profile + +---------- +Parameters +---------- + +#. ``cryptoAlgo`` - ``string``: crypto algorithm + +------- +Returns +------- + +Promise resolves to ``string``: the found key for the cryptoinfo. + +------- +Example +------- + +.. code-block:: javascript + + const cryptoInfo = { + "public": { + "name": "envelope example" + }, + "private": "...", + "cryptoInfo": { + "algorithm": "unencrypted", + "keyLength": 256, + "originator": "0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002", + "block": 123 + } + }; + const key = runtime.keyProvider.getKey(info); + +.. required for building markup + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface diff --git a/docs/getting-started.rst b/docs/getting-started.rst new file mode 100644 index 00000000..d1c09daf --- /dev/null +++ b/docs/getting-started.rst @@ -0,0 +1,60 @@ +=============== +Getting Started +=============== + +The blockchain core is a helper library, that offers helpers for interacting with the evan.network blockchain. It is written in TypeScript and offers several (up to a certain degree) stand-alone modules, that can be used for + +- creating and updating contracts +- managing user profiles +- en- and decryption +- distributed filesystem file handling +- key exchange and key handling +- ENS domain handling +- sending and receiving bmails + +.. _adding-blockchain-core: + +Adding blockchain core +====================== + +First you need to get blockchain core and its dependencies into your project. This can be done using the following methods: + +- npm: ``npm install @evan.network/api-blockchain-core ipfs-api web3`` + +After that you need to create a blockchain core runtime with a predefined configuration. + +Configuring and initializing blockchain core +============================================ + +.. code-block:: javascript + + // require blockchain-core dependencies + const IpfsApi = require('ipfs-api'); + const Web3 = require('web3'); + + // require blockchain-core + const { Ipfs, createDefaultRuntime } = require('blockchain-core'); + + const runtimeConfig = { + // account map to blockchain accounts with their private key + accountMap: { + 'ACCOUNTID': + 'PRIVATE KEY', + }, + // ipfs configuration for evan.network storage + ipfs: {host: 'ipfs.evan.network', port: '443', protocol: 'https'}, + // web3 provider config (currently evan.network testcore) + web3Provider: 'wss://testcore.evan.network/ws', + }; + + // initialize dependencies + const web3 = new Web3(); + web3.setProvider(new web3.providers.WebsocketProvider(runtimeConfig.web3Provider)); + const dfs = new Ipfs({ remoteNode: new IpfsApi(runtimeConfig.ipfs), }); + + // create runtime + const runtime = await createDefaultRuntime(web3, dfs, { accountMap: runtimeConfig.accountMap, }); + +That's it! now you can use the ``runtime`` object and interact with the evan.network blockchain. + +The blockchain-core api is a set of modules which can be plugged in individually. So the above ``runtime`` is a full blown entry point to the api. You can also plug your own runtime with needed modules together. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..6328d109 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,32 @@ +.. blockchain-core documentation master file, created by + sphinx-quickstart on Thu May 24 11:12:10 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to blockchain-core's documentation! +=========================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents + + getting-started + +.. toctree:: + :maxdepth: 2 + :glob: + :caption: API Reference: + + blockchain/index + common/index + contracts/index + dfs/index + encryption/index + profile/index + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..b96cdcf0 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=blockchain-core + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/profile/business-center-profile.rst b/docs/profile/business-center-profile.rst new file mode 100644 index 00000000..a23c174d --- /dev/null +++ b/docs/profile/business-center-profile.rst @@ -0,0 +1,323 @@ +================================================================================ +Business Center Profile +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - BusinessCenterProfile + * - Extends + - `Logger `_ + * - Source + - `business-center-profile.ts `_ + * - Tests + - `business-center-profile.spec.ts `_ + +The ``BusinessCenterProfile`` module allows to create profiles in a business center. + +These profiles are like business cards for specific contexts and can be used to share data like contact data or certificates under this business centers context. + + + +-------------------------------------------------------------------------------- + +Basic Usage +================================================================================ + +.. code-block:: typescript + + // update/set contact card locally + await profile.setContactCard(JSON.parse(JSON.stringify(sampleProfile))); + + // store to business center + await profile.storeForBusinessCenter(businessCenterDomain, accounts[0]); + + // load from business center + await profile.loadForBusinessCenter(businessCenterDomain, accounts[0]); + const loadedProfile = await profile.getContactCard(); + + + +-------------------------------------------------------------------------------- + +.. _businessCenterProfile_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new BusinessCenterProfile(options); + +Creates a new BusinessCenterProfile instance. + +---------- +Parameters +---------- + +#. ``options`` - ``BusinessCenterProfileOptions``: options for BusinessCenterProfile constructor. + * ``bcAddress`` - ``string``: ENS address (domain name) of the business center, the module instance is scoped to + * ``cryptoProvider`` - |source cryptoProvider|_: |source cryptoProvider|_ instance + * ``defaultCryptoAlgo`` - ``string``: crypto algorith name from |source cryptoProvider| + * ``ipld`` - |source ipld|_: |source ipld|_ instance + * ``nameResolver`` - |source nameResolver|_: |source nameResolver|_ instance + * ``ipldData`` - ``any`` (optional): preloaded profile data + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``BusinessCenterProfile`` instance + +------- +Example +------- + +.. code-block:: typescript + + const businessCenterProfile = new BusinessCenterProfile({ + ipld, + nameResolver, + defaultCryptoAlgo: 'aes', + bcAddress: businessCenterDomain, + cryptoProvider, + });; + + + +-------------------------------------------------------------------------------- + +.. _businessCenterProfile_setContactCard: + +setContactCard +================================================================================ + +.. code-block:: typescript + + businessCenterProfile.setContactCard(); + +Set contact card on current profile. + +---------- +Parameters +---------- + +#. ``contactCard`` - ``any``: contact card to store + +------- +Returns +------- + +``Promise`` returns ``any``: updated tree + +------- +Example +------- + +.. code-block:: typescript + + const updated = await businessCenterProfile.setContactCard(contactCard); + + + +-------------------------------------------------------------------------------- + +.. _businessCenterProfile_getContactCard: + +getContactCard +================================================================================ + +.. code-block:: typescript + + businessCenterProfile.getContactCard(); + +Get contact card from. + +---------- +Parameters +---------- + +(none) + +------- +Returns +------- + +``Promise`` returns ``any``: contact card + +------- +Example +------- + +.. code-block:: typescript + + const loadedProfile = await businessCenterProfile.getContactCard(); + + + +-------------------------------------------------------------------------------- + +.. _businessCenterProfile_storeForBusinessCenter: + +storeForBusinessCenter +================================================================================ + +.. code-block:: typescript + + businessCenterProfile.storeForBusinessCenter(businessCenterDomain, account); + +Stores profile to business centers profile store. + +---------- +Parameters +---------- + +#. ``businessCenerDomain`` - ``string``: ENS domain name of a business center +#. ``account`` - ``string``: Ethereum account id + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await businessCenterProfile.setContactCard(contactCard); + await businessCenterProfile.storeForBusinessCenter(businessCenterDomain, accounts[0]); + + + +-------------------------------------------------------------------------------- + +.. _businessCenterProfile_loadForBusinessCenter: + +loadForBusinessCenter +================================================================================ + +.. code-block:: typescript + + businessCenterProfile.loadForBusinessCenter(businessCenterDomain, account); + +Function description + +---------- +Parameters +---------- + +#. ``businessCenerDomain`` - ``string``: ENS domain name of a business center +#. ``account`` - ``string``: Ethereum account id + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await newProfilebusinessCenterProfile.loadForBusinessCenter(businessCenterDomain, accounts[0]); + const contactCard = await businessCenterProfile.getContactCard(); + + + +------------------------------------------------------------------------------ + +.. _business-center-profile_storeToIpld: + +storeToIpld +================================================================================ + +.. code-block:: typescript + + businessCenterProfile.storeToIpld(); + +Store profile in ipfs as an ipfs file that points to a ipld dag. + +---------- +Parameters +---------- + +(none) + +------- +Returns +------- + +``Promise`` returns ``string``: hash of the ipfs file + +------- +Example +------- + +.. code-block:: typescript + + await businessCenterProfile.storeToIpld(); + + + +------------------------------------------------------------------------------ + +.. _business-center-profile_loadFromIpld: + +loadFromIpld +================================================================================ + +.. code-block:: typescript + + businessCenterProfile.loadFromIpld(tree, ipldIpfsHash); + +Load profile from ipfs via ipld dag via ipfs file hash. + +---------- +Parameters +---------- + +#. ``ipldIpfsHash`` - ``string``: ipfs file hash that points to a file with ipld a hash + +------- +Returns +------- + +``Promise`` returns ``BusinessCenterProfile``: this profile + +------- +Example +------- + +.. code-block:: typescript + + businessCenterProfile.loadFromIpld(ipldIpfsHash); + + + +.. required for building marku + +.. |source cryptoProvider| replace:: ``CryptoProvider`` +.. _source cryptoProvider: /encryption/crypto-provider.html + +.. |source ipld| replace:: ``Ipld`` +.. _source ipld: /dfs/ipld.htmlp + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source nameResolver| replace:: ``NameResolver`` +.. _source nameResolver: /blockchain/name-resolver.html \ No newline at end of file diff --git a/docs/profile/index.rst b/docs/profile/index.rst new file mode 100644 index 00000000..1fc51377 --- /dev/null +++ b/docs/profile/index.rst @@ -0,0 +1,20 @@ +================================================================================ +Profile +================================================================================ + +.. toctree:: + :glob: + :maxdepth: 1 + + profile + business-center-profile + onboarding + key-exchange + mailbox + +Profiles are personal data for accounts. They can be shared with other accounts or used for storing own data. This section contains modules for maintaining profile and interacting with profiles from other accounts. + +Two types of profiles are supported: + +- personal `profiles `_, that hold users data like contacts, bookmarks, encryption keys +- `business center profiles `_, that are like contact cards and hold data intended for other participants \ No newline at end of file diff --git a/docs/profile/key-exchange.rst b/docs/profile/key-exchange.rst new file mode 100644 index 00000000..8c525df5 --- /dev/null +++ b/docs/profile/key-exchange.rst @@ -0,0 +1,430 @@ +================================================================================ +Key Exchange +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - KeyExchange + * - Extends + - `Logger `_ + * - Source + - `keyExchange.ts `_ + * - Tests + - `keyExchange.spec.ts `_ + +The ``KeyExchange`` module is used to exchange communication keys between two parties, assuming that both have created a profile and have a public facing partial Diffie Hellman key part (the combination of their own secret and the shared secret). The key exchange consists of three steps: + +#. create a new communication key, that will be used by both parties for en- and decryption and store it on the initiators side +#. look up the other parties partial Diffie Hellman key part and combine it with the own private key to create the exchange key +#. use the exchange key to encrypt the communication key and send it via bmail (blockchain mail) to other party + + + +-------------------------------------------------------------------------------- + +Basic Usage +================================================================================ + +--------------------------------- +Starting the Key Exchange Process +--------------------------------- + +This example retrieves public facing partial Diffie Hellman key part from a second party and sends an invitation mail to it: + +.. code-block:: typescript + + // account, that initiates the invitation + const account1 = '0x0000000000000000000000000000000000000001'; + // account, that will receive the invitation + const account2 = '0x0000000000000000000000000000000000000002'; + // profile from user, that initiates key exchange + const profile1 = {}; + await profile1.loadForAccount(); + // profile from user, that is going to receive the invitation + const profile2 = {}; + await profile2.loadForAccount(); + // key exchange instance for account1 + const keyExchange1 = {}; + // key exchange instance for account2 + const keyExchange2 = {}; + + const foreignPubkey = await profile2.getPublicKey(); + const commKey = await keyExchange1.generateCommKey(); + await keyExchange1.sendInvite(account2, foreignPubkey, commKey, { + fromAlias: 'Bob', // initiating user states, that his name is 'Bob' + }); + await profile1.addContactKey(account2, 'commKey', commKey); + await profile1.storeForAccount(profile1.treeLabels.addressBook); + +---------------------------------- +Finishing the Key Exchange Process +---------------------------------- + +Let's assume that the communication key from the last example has been successfully sent to the other party and continue at there end from here. To keep the roles from the last example, the variables profile1, profile2 will belong to the same accounts: + +.. code-block:: typescript + + const encryptedCommKey = '...'; // key sent by account1 + const profile1 = await profile1.getPublicKey(); + const commSecret = keyExchange2.computeSecretKey(profile1); + const commKey = await keyExchange2.decryptCommKey(encryptedCommKey, commSecret.toString('hex')); + + + +-------------------------------------------------------------------------------- + +.. _keyExchange_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new KeyExchange(options); + +Creates a new KeyExchange instance. + +---------- +Parameters +---------- + +#. ``options`` - ``KeyExchangeOptions``: options for KeyExchange constructor. + * ``account`` - ``string``: account, that will perform actions + * ``cryptoProvider`` - |source cryptoProvider|_: |source cryptoProvider|_ instance + * ``defaultCryptoAlgo`` - ``string``: default encryption algorithm + * ``keyProvider`` - |source keyProviderInterface|_: |source keyProviderInterface|_ instance + * ``mailbox`` - |source mailbox|_: |source mailbox|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + * ``privateKey`` - ``object`` (optional): private key for key exchange, if ``privateKey`` or ``publicKey`` is omitted, new keys are generated + * ``publicKey`` - ``object`` (optional): public key for key exchange, if ``privateKey`` or ``publicKey`` is omitted, new keys are generated + +------- +Returns +------- + +``KeyExchange`` instance + +------- +Example +------- + +.. code-block:: typescript + + const keyExchange = new KeyExchange({ + mailbox, + cryptoProvider, + defaultCryptoAlgo: 'aes', + account: accounts[0], + keyProvider, + }); + + + +-------------------------------------------------------------------------------- + +.. _keyExchange_computeSecretKey: + +computeSecretKey +================================================================================ + +.. code-block:: typescript + + keyExchange.computeSecretKey(partialKey); + +Combines given partial key from another profile with own private key. + +---------- +Parameters +---------- + +#. ``partialKey`` - ``string``: The options used for calling + +------- +Returns +------- + +``string`` combined exchange key + +------- +Example +------- + +.. code-block:: typescript + + // encrypted communication key sent from account 1 to account 2 + const encryptedKey = '...' + // (profile 1 belongs to account 1, keyExchange 2 to account 2) + const publicKeyProfile1 = await profile1.getPublicKey(); + const commSecret = keyExchange2.computeSecretKey(publicKeyProfile1); + commKey = await keyExchange2.decryptCommKey(encryptedKey, commSecret.toString('hex')); + + + +-------------------------------------------------------------------------------- + +.. _keyExchange_decryptCommKey: + +decryptCommKey +================================================================================ + +.. code-block:: typescript + + keyExchange.decryptCommKey(encryptedCommKey, exchangeKey); + +Decrypts a given communication key with an exchange key. + +---------- +Parameters +---------- + +#. ``encryptedCommKey`` - ``string``: encrypted communications key received from another account +#. ``exchangeKey`` - ``string``: Diffie Hellman exchange key from computeSecretKey + +------- +Returns +------- + +``Promise`` returns ``Buffer``: commKey as a buffer + +------- +Example +------- + +.. code-block:: typescript + + // encrypted communication key sent from account 1 to account 2 + const encryptedKey = '...' + // (profile 1 belongs to account 1, keyExchange 2 to account 2) + const publicKeyProfile1 = await profile1.getPublicKey(); + const commSecret = keyExchange2.computeSecretKey(publicKeyProfile1); + commKey = await keyExchange2.decryptCommKey(encryptedKey, commSecret.toString('hex')); + + + +-------------------------------------------------------------------------------- + +.. _keyExchange_getDiffieHellmanKeys: + +getDiffieHellmanKeys +================================================================================ + +.. code-block:: typescript + + keyExchange.getDiffieHellmanKeys(); + +Returns the public and private key from the diffieHellman. + +---------- +Parameters +---------- + +(void) + +------- +Returns +------- + +``Promise`` returns ``any``: object with public and private keys + +------- +Example +------- + +.. code-block:: typescript + + console.dir(await keyExchange.getDiffieHellmanKeys()); + // Output: + // { + // private: '...', + // public: '...', + // } + + + +-------------------------------------------------------------------------------- + +.. _keyExchange_generateCommKey: + +generateCommKey +================================================================================ + +.. code-block:: typescript + + keyExchange.generateCommKey(); + +Generates a new communication key end returns the hex string. + +---------- +Parameters +---------- + +(none) + +------- +Returns +------- + +``Promise`` returns ``string``: comm key as string + +------- +Example +------- + +.. code-block:: typescript + + console.dir(await keyExchange.generateCommKey()); + // Output: + // '1c967697c192235680efbb24b980981b4778c8058b5e0864f1471fc1d941499d' + + + +-------------------------------------------------------------------------------- + +.. _keyExchange_getExchangeMail: + +getExchangeMail +================================================================================ + +.. code-block:: typescript + + keyExchange.getExchangeMail(from, mailContent[, encryptionCommKey]); + +Creates a bmail for exchanging comm keys. + +---------- +Parameters +---------- + +#. ``from`` - ``string``: sender accountId +#. ``mailContent`` - ``any``: bmail metadata +#. ``encryptedCommKey`` - ``string`` (optional): comm key, that should be exchanged + +------- +Returns +------- + +``Promise`` returns ``Mail``: mail for key exchange + +------- +Example +------- + +.. code-block:: typescript + + const commKey = '1c967697c192235680efbb24b980981b4778c8058b5e0864f1471fc1d941499d'; + const mail = keyExchange.getExchangeMail( + '0x0000000000000000000000000000000000000001', + { fromAlias: 'user 1', fromMail: 'user1@example.com', title:'sample', body:'sample', } + ); + console.log(mail); + // Output: + // { content: + // { from: '0x0000000000000000000000000000000000000001', + // fromAlias: 'user 1', + // fromMail: 'user1@example.com', + // title: 'sample', + // body: 'sample', + // attachments: [ [Object] ] } } + + + +-------------------------------------------------------------------------------- + +.. _keyExchange_sendInvite: + +sendInvite +================================================================================ + +.. code-block:: typescript + + keyExchange.sendInvite(targetAccount, targetPublicKey, commKey, mailContent); + +Sends a mailbox mail to the target account with the partial key for the key exchange. + +---------- +Parameters +---------- + +#. ``string`` - ``targetAccount``: receiver of the invitation +#. ``string`` - ``targetPublicKey``: combination of shared secret plus targetAccounts private secret +#. ``string`` - ``commKey``: communication key between sender and targetAccount +#. ``any`` - ``mailContent``: mail to send + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const foreignPubkey = await profile2.getPublicKey(); + const commKey = await keyExchange1.generateCommKey(); + await keyExchange1.sendInvite(accounts[1], foreignPubkey, commKey, { fromAlias: 'Bob', }); + await profile.addContactKey(accounts[1], 'commKey', commKey); + await profile.storeForAccount(profile.treeLabels.addressBook); + + + +-------------------------------------------------------------------------------- + +.. _keyExchange_setPublicKey: + +setPublicKey +================================================================================ + +.. code-block:: typescript + + keyExchange.setPublicKey(publicKey, privateKey); + +Set the private and public key on the current diffieHellman object. + +---------- +Parameters +---------- + +#. ``publicKey`` - ``string``: public Diffie Hellman key +#. ``privateKey`` - ``string``: private Diffie Hellman key + +------- +Returns +------- + +(no return value) + +------- +Example +------- + +.. code-block:: typescript + + keyExchange.setPublicKey('...', '...'); + + + +.. required for building markup + +.. |source cryptoProvider| replace:: ``CryptoProvider`` +.. _source cryptoProvider: /encryption/crypto-provider.html + +.. |source keyProviderInterface| replace:: ``KeyProviderInterface`` +.. _source keyProviderInterface: /encryption/key-provider.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source mailbox| replace:: ``Mailbox`` +.. _source mailbox: /profile/mailbox.html \ No newline at end of file diff --git a/docs/profile/mailbox.rst b/docs/profile/mailbox.rst new file mode 100644 index 00000000..be783394 --- /dev/null +++ b/docs/profile/mailbox.rst @@ -0,0 +1,523 @@ +================================================================================ +Mailbox +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Mailbox + * - Extends + - `Logger `_ + * - Source + - `mailbox.ts `_ + * - Tests + - `mailbox.spec.ts `_ + +The `Mailbox `_ module is used for sending and retrieving bmails (blockchain mails) to other even.network members. Sending regular bmails between to parties requires them to have completed a `Key Exchange `_ before being able to send encrypted messages. When exchanging the keys, bmails are encrypted with a commonly known key, that is only valid is this case and the underlying messages, that contain the actual keys are encrypted with Diffie Hellman keys, to ensure, that keys are exchanged in a safe manner (see `Key Exchange `_ for details). + +The mailbox is a `smart contract `_, that holds + +- ``bytes32`` hashes, that are the encrypted contents of the mails +- basic metadata about the mails, like + + + recipient of a mail + + sender of a mail + + amount of EVEs, that belongs to the bmail + +- if the mail is an answer to another mail, the reference to the original mail + + + +-------------------------------------------------------------------------------- + +.. _mailbox_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new Mailbox(options); + +Creates a new Mailbox instance. + +Instances created with the constructor are **not usable** right from the start. They require the :ref:`init() ` function to be called, before they are ready to use. + +---------- +Parameters +---------- + +#. ``options`` - ``MailboxOptions``: options for Mailbox constructor. + * ``contractLoader`` - |source contractLoader|_: |source contractLoader|_ instance + * ``cryptoProvider`` - |source cryptoProvider|_: |source cryptoProvider|_ instance + * ``defaultCryptoAlgo`` - ``string``: crypto algorith name from |source cryptoProvider|_ + * ``ipfs`` - |source ipfs|_: |source ipfs|_ instance + * ``keyProvider`` - |source keyProviderInterface|_: |source keyProviderInterface|_ instance + * ``mailboxOwner`` - ``string``: account, that will be used, when working with the mailbox + * ``nameResolver`` - |source nameResolver|_: |source nameResolver|_ instance + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``Mailbox`` instance + +------- +Example +------- + +.. code-block:: typescript + + const mailbox = new Mailbox({ + mailboxOwner, + nameResolver, + ipfs, + contractLoader, + cryptoProvider, + keyProvider, + defaultCryptoAlgo: 'aes', + }); + await mailbox.init(); + + + +-------------------------------------------------------------------------------- + +.. _mailbox_init: + +init +================================================================================ + +.. code-block:: typescript + + mailbox.init(); + +Initialize mailbox module. + +This function needs to be called, before the mailbox module can be used. + +---------- +Parameters +---------- + +(none) + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const mailbox = new Mailbox({ + mailboxOwner, + nameResolver, + ipfs, + contractLoader, + cryptoProvider, + keyProvider, + defaultCryptoAlgo: 'aes', + }); + await mailbox.init(); + + + +-------------------------------------------------------------------------------- + +.. _mailbox_sendMail: + +sendMail +================================================================================ + +.. code-block:: typescript + + mailbox.sendMail(mail, from, to[, value, context]); + +Sends a mail to given target. + +---------- +Parameters +---------- + +#. ``mail`` - ``Mail``: a mail to send +#. ``from`` - ``string``: account id to send mail from +#. ``to`` - ``string``: account id to send mail to +#. ``value`` - ``string`` (optional): amount of EVEs to send with mail in Wei, can be created with ``web3[.utils].toWei(...)``, defaults to ``0`` +#. ``context`` - ``string`` (optional): encryption context for bmail, if a special context should be used (e.g. ``keyExchange``) + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + // account, that sends the mail + const account1 = '0x0000000000000000000000000000000000000001'; + // account, that receives the mail + const account2 = '0x0000000000000000000000000000000000000002'; + // mailbox of the sender + const mailbox1 = {}; + // mailbox of the receiver + const mailbox2 = {}; + + const bmail = { + content: { + from: account1, + to, + title: 'Example bmail', + body: 'This is a little example to demonstrate sending a bmail.', + attachments: [ ] + } + }; + await mailbox1.sendMail(bmail, account1, account2); + + + +-------------------------------------------------------------------------------- + +.. _mailbox_sendAnswer: + +sendAnswer +================================================================================ + +.. code-block:: typescript + + mailbox.sendAnswer(mail, from, to[, value, context]); + +Send answer to a mail. + +---------- +Parameters +---------- + +#. ``mail`` - ``Mail``: a mail to send, ``mail.parentId`` must be set to mailId of mail, that is answered +#. ``from`` - ``string``: account id to send mail from +#. ``to`` - ``string``: account id to send mail to +#. ``value`` - ``string`` (optional): amount of EVEs to send with mail in Wei, can be created with ``web3[.utils].toWei(...)``, defaults to ``0`` +#. ``context`` - ``string`` (optional): encryption context for bmail, if a special context should be used (e.g. ``keyExchange``) + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + // account, that sends the answer + const account1 = '0x0000000000000000000000000000000000000001'; + // account, that receives the answer + const account2 = '0x0000000000000000000000000000000000000002'; + // mailbox of the sender + const mailbox1 = {}; + // mailbox of the receiver + const mailbox2 = {}; + + const bmail = { + content: { + from: account1, + to, + title: 'Example bmail', + body: 'This is a little example to demonstrate sending a bmail.', + attachments: [ ] + }, + parentId: '0x0000000000000000000000000000000000000000000000000000000000000012', + }; + await mailbox1.sendAnswer(bmail, account1, account2); + + + +-------------------------------------------------------------------------------- + +.. _mailbox_getMails: + +getMails +================================================================================ + +.. code-block:: typescript + + mailbox.getMails([count, offset, type]); + +Gets the last n mails, resolved contents. + +---------- +Parameters +---------- + +#. ``count`` - ``number`` (optional): retrieve up to this many answers (for paging), defaults to ``10`` +#. ``offset`` - ``number`` (optional): skip this many answers (for paging), defaults to ``0`` +#. ``type`` - ``string`` (optional): retrieve sent or received mails, defaults to ``'Received'`` + +------- +Returns +------- + +``Promise`` returns ``any``: resolved mails + +------- +Example +------- + +.. code-block:: typescript + + const received = await mailbox2.getMails(); + console.dir(JSON.stringify(received[0], null, 2)); + // Output: + // { + // "mails": { + // "0x000000000000000000000000000000000000000e": { + // "content": { + // "from": "0x0000000000000000000000000000000000000001", + // "to": "0x0000000000000000000000000000000000000002", + // "title": "Example bmail", + // "body": "This is a little example to demonstrate sending a bmail.", + // "attachments": [ ], + // "sent": 1527083983148 + // }, + // "cryptoInfo": { + // "originator": "0x549704d235e1fe5cd7326a1eb0c44c1e0a5434799ba6ff2370c2955730b66e2b", + // "keyLength": 256, + // "algorithm": "aes-256-cbc" + // } + // } + // }, + // "totalResultCount": 9 + // } + +Results can be paged with passing arguments for page size and offsetto the ``getMails`` function: + +.. code-block:: typescript + + const received = await mailbox2.getMails(3, 0); + console.dir(JSON.stringify(received[0], null, 2)); + // Output: + // { mails: + // { '0x000000000000000000000000000000000000000e': { content: [Object], cryptoInfo: [Object] }, + // '0x000000000000000000000000000000000000000d': { content: [Object], cryptoInfo: [Object] }, + // '0x000000000000000000000000000000000000000c': { content: [Object], cryptoInfo: [Object] } }, + // totalResultCount: 9 } + +To get bmails *sent* by an account, use (the example account hasn't sent any bmail yet): + +.. code-block:: typescript + + const received = await mailbox2.getMails(3, 0, 'Sent'); + console.dir(JSON.stringify(received[0], null, 2)); + // Output: + // { mails: {}, totalResultCount: 0 } + + + +-------------------------------------------------------------------------------- + +.. _mailbox_getMail: + +getMail +================================================================================ + +.. code-block:: typescript + + mailbox.getMail(mail); + +Gets one single mail directly. + +---------- +Parameters +---------- + +#. ``mail`` - ``string``: mail to resolve (mailId or hash) + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const mailId = '0x0000000000000000000000000000000000000000000000000000000000000012'; + const bmail = await mailbox.getMail(mailId); + + + +-------------------------------------------------------------------------------- + +.. _mailbox_getAnswersForMail: + +getAnswersForMail +================================================================================ + +.. code-block:: typescript + + mailbox.getAnswersForMail(mailId[, count, offset]); + +Gets answer tree for mail, traverses subanswers as well. + +---------- +Parameters +---------- + +#. ``mailId`` - ``string``: mail to resolve +#. ``count`` - ``number`` (optional): retrieve up to this many answers, defaults to ``5`` +#. ``offset`` - ``number`` (optional): skip this many answers, defaults to ``0`` + +------- +Returns +------- + +``Promise`` returns ``any``: answer tree for mail + +------- +Example +------- + +.. code-block:: typescript + + const mailId = '0x0000000000000000000000000000000000000000000000000000000000000012'; + const answers = await mailbox.getAnswersForMail(mailId); + + + +-------------------------------------------------------------------------------- + +.. _mailbox_getBalanceFromMail: + +getBalanceFromMail +================================================================================ + +.. code-block:: typescript + + mailbox.getBalanceFromMail(mailId); + +Returns amount of EVE deposited for a mail. + +Bmails can contain EVEs for the recipient as well. Because retrieving bmails is a reading operation, funds send with a bmail have to be retrieved separately. + +---------- +Parameters +---------- + +#. ``mailId`` - ``string``: mail to resolve + +------- +Returns +------- + +``Promise`` returns ``string``: balance of the mail in Wei, can be converted with web3[.utils].fromWei(...) + +------- +Example +------- + +.. code-block:: typescript + + const bmail = { + content: { + from: account1, + to, + title: 'Example bmail', + body: 'This is a little example to demonstrate sending a bmail.', + attachments: [ ] + } + }; + await mailbox1.sendMail(bmail, account1, account2, web3.utils.toWei('0.1', 'Ether')); + const received = await mailbox2.getMails(1, 0); + const mailBalance = await mailbox2.getBalanceFromMail(Object.keys(received)[0]); + console.log(mailBalance); + // Output: + // 100000000000000000 + + + +-------------------------------------------------------------------------------- + +.. _mailbox_withdrawFromMail: + +withdrawFromMail +================================================================================ + +.. code-block:: typescript + + mailbox.withdrawFromMail(mailId, recipient); + +Funds from bmails can be claimed with the account, that received the bmail. Funds are transferred to a specified account, which can be the claiming account or another account of choice. + +---------- +Parameters +---------- + +#. ``mailId`` - ``string``: mail to resolve +#. ``recipient`` - ``string``: account, that receives the EVEs + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const received = await mailbox2.getMails(1, 0); + const mailBalance = await mailbox2.getBalanceFromMail(Object.keys(received)[0]); + console.log(mailBalance); + // Output: + // 100000000000000000 + await mailbox2.withdrawFromMail(received)[0], accounts2); + const mailBalance = await mailbox2.getBalanceFromMail(Object.keys(received)[0]); + console.log(mailBalance); + // Output: + // 0 + + + +.. required for building markup + +.. |source contractLoader| replace:: ``ContractLoader`` +.. _source contractLoader: /contracts/contract-loader.html + +.. |source cryptoProvider| replace:: ``CryptoProvider`` +.. _source cryptoProvider: /encryption/crypto-provider.html + +.. |source ipfs| replace:: ``Ipfs`` +.. _source ipfs: /dfs/ipfs.html + +.. |source keyProviderInterface| replace:: ``KeyProviderInterface`` +.. _source keyProviderInterface: /encryption/key-provider.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source nameResolver| replace:: ``NameResolver`` +.. _source nameResolver: /blockchain/name-resolver.html \ No newline at end of file diff --git a/docs/profile/onboarding.rst b/docs/profile/onboarding.rst new file mode 100644 index 00000000..223a1532 --- /dev/null +++ b/docs/profile/onboarding.rst @@ -0,0 +1,131 @@ +================================================================================ +Onboarding +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Onboarding + * - Extends + - `Logger `_ + * - Source + - `onboarding.ts `_ + * - Tests + - `onboarding.spec.ts `_ + +The onboarding process is used to enable users to invite other users, where no blockchain account id is known. It allows to send an email to such contacts, that contains a link. This link points to a evan.network ÐApp, that allows accept the invitation by either creating a new account or by accepting it with an existing account. + +It uses the `Key Exchange `_ module described in the last section for its underlying key exchange process but moves the process of creating a new communication key to the invited user. + +To get in contact with a user via email, a smart agent is used. This smart agent has to be added as a contact and a regular key exchange with the smart agent is performed. The agent accepts the invitation automatically and the inviting user sends a bmail (blockchain mail) with the contact details of the user, that should be invited, and an amount of welcome EVEs to the smart agent. + +The onboarding smart creates a session on his end and sends an email to the invited user, that includes the session token, with which the invited user can claim the welcome EVEs. + +The invited user now creates or confirms an account and start the key exchange process on his or her end. The rest of the flow is as described in `Key Exchange `_. + +To start the process at from the inviting users side, make sure that this user has exchanged keys with the onboarding smart agent. + + + +-------------------------------------------------------------------------------- + +.. _onboarding_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new Onboarding(options); + +Creates a new Onboarding instance. + +---------- +Parameters +---------- + +#. ``options`` - ``OnboardingOptions``: options for Onboarding constructor + * ``executor`` - |source executor|_: |source executor|_ instance + * ``mailbox`` - |source mailbox|_: |source mailbox|_ instance + * ``smartAgentId`` - ``string``: account id of onboarding smart agent + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + +------- +Returns +------- + +``Onboarding`` instance + +------- +Example +------- + +.. code-block:: typescript + + const onboarding = new Onboarding({ + mailbox, + smartAgentId: config.smartAgents.onboarding.accountId, + executor, + }); + + + +-------------------------------------------------------------------------------- + +.. _onboarding_sendInvitation: + +sendInvitation +================================================================================ + +.. code-block:: typescript + + onboarding.sendInvitation(invitation, weiToSend); + +Send invitation to another user via smart agent that sends a mail. + +---------- +Parameters +---------- + +#. ``invitation`` - ``invitation``: mail that will be sent to invited person +#. ``weiToSend`` - ``string``: amount of ETC to transfert to new member, can be created with web3.utils.toWei(10, 'ether') [web3 >=1.0] / web.toWei(10, 'ether') [web3 < 1.0] + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await onboarding.sendInvitation({ + fromAlias: 'example inviter', + to: 'example invitee ', + lang: 'en', + subject: 'evan.network Onboarding Invitation', + body: 'I\'d like to welcome you on board.', + }, web3.utils.toWei('1')); + + +.. required for building markup + +.. |source executor| replace:: ``Executor`` +.. _source executor: /blockchain/executor.html + +.. |source mailbox| replace:: ``Mailbox`` +.. _source mailbox: /profile/mailbox.html + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface \ No newline at end of file diff --git a/docs/profile/profile.rst b/docs/profile/profile.rst new file mode 100644 index 00000000..9fd14c75 --- /dev/null +++ b/docs/profile/profile.rst @@ -0,0 +1,1184 @@ +================================================================================ +Profile +================================================================================ + +.. list-table:: + :widths: auto + :stub-columns: 1 + + * - Class Name + - Profile + * - Extends + - `Logger `_ + * - Source + - `profile.ts `_ + * - Tests + - `profile.spec.ts `_ + +A users profile is its personal storage for +- contacts +- encryption keys exchanged with contacts +- an own public key for exchanging keys with new contacts +- bookmarked ÐAPPs +- created contracts + +This data is stored as an `IPLD Graphs `_ per type and stored in a users profile contract. These graphs are independant from each other and have to be saved separately. + +This contract is a `DataContract `_ and can be created via the factory at `profile.factory.evan` and looked up at the global profile index `profile.evan`. The creation process and landmap looks like this: + +.. image:: https://user-images.githubusercontent.com/1394421/38298221-1938d006-37f7-11e8-9a84-abfd311c97f0.png + + + +-------------------------------------------------------------------------------- + +Basic Usage +================================================================================ + +.. code-block:: typescript + + // the bookmark we want to store + const sampleDesc = { + title: 'sampleTest', + description: 'desc', + img: 'img', + primaryColor: '#FFFFFF', + }; + + // create new profile, set private key and keyexchange partial key + await profile.createProfile(keyExchange.getDiffieHellmanKeys()); + + // add a bookmark + await profile.addDappBookmark('sample1.test', sampleDesc); + + // store tree to contract + await profile.storeForAccount(profile.treeLabels.bookmarkedDapps); + + + +-------------------------------------------------------------------------------- + +.. _profile_constructor: + +constructor +================================================================================ + +.. code-block:: typescript + + new Profile(options); + +Creates a new Profile instance. + +---------- +Parameters +---------- + +#. ``options`` - ``ProfileOptions``: options for Profile constructor + * ``accountId`` - ``string``: account, that is the profile owner + * ``contractLoader`` - |source contractLoader|_: |source contractLoader|_ instance + * ``dataContract`` - |source dataContract|_: |source dataContract|_ instance + * ``executor`` - |source executor|_: |source executor|_ instance + * ``ipld`` - |source ipld|_: |source ipld|_ instance + * ``nameResolver`` - |source nameResolver|_: |source nameResolver|_ instance + * ``defaultCryptoAlgo`` - ``string`` (optional): crypto algorith name from |source cryptoProvider|, defaults to ``aes`` + * ``log`` - ``Function`` (optional): function to use for logging: ``(message, level) => {...}`` + * ``logLevel`` - |source logLevel|_ (optional): messages with this level will be logged with ``log`` + * ``logLog`` - |source logLogInterface|_ (optional): container for collecting log messages + * ``logLogLevel`` - |source logLevel|_ (optional): messages with this level will be pushed to ``logLog`` + * ``trees`` - ``object`` (optional): precached profile data, defaults to ``{}`` + +------- +Returns +------- + +``Profile`` instance + +------- +Example +------- + +.. code-block:: typescript + + const profile = new Profile({ + accountId: accounts[0], + contractLoader, + dataContract, + executor, + ipld, + nameResolver, + }); + + + +------------------------------------------------------------------------------ + + + +.. _profile_createProfile: + +createProfile +================================================================================ + +.. code-block:: typescript + + profile.createProfile(keys) + +Create new profile, store it to profile index initialize addressBook and publicKey. + +---------- +Parameters +---------- + +#. ``keys`` - ``any``: diffie hell man keys for account, created by |source keyExchange_getDiffieHellmanKeys|_ + * ``privateKey`` - ``Buffer``: private key for key exchange + * ``publicKey`` - ``Buffer``: combination of shared secret and own private key + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await profile.createProfile(keyExchange.getDiffieHellmanKeys()); + + + +------------------------------------------------------------------------------ + +.. _profile_exists: + +exists +================================================================================ + +.. code-block:: typescript + + profile.exists(); + +Check if a profile has been stored for current account. + +---------- +Parameters +---------- + +#. ``options`` - ``object``: The options used for calling + +------- +Returns +------- + +``Promise`` returns ``void``: true if a contract was registered, false if not + +------- +Example +------- + +.. code-block:: typescript + + console.log(await profile.exists()); + // Output: + // true + + + +------------------------------------------------------------------------------ + +.. _profile_getContactKnownState: + +getContactKnownState +================================================================================ + +.. code-block:: typescript + + profile.getContactKnownState(accountId); + +Check, known state for given account. + +---------- +Parameters +---------- + +#. ``accountId`` - ``string``: account id of a contact + +------- +Returns +------- + +``Promise`` returns ``void``: true if known account + +------- +Example +------- + +.. code-block:: typescript + + console.log(await profile.getContactKnownState(accountId)); + // Output: + // true + + + +------------------------------------------------------------------------------ + +.. _profile_setContactKnownState: + +setContactKnownState +================================================================================ + +.. code-block:: typescript + + profile.setContactKnownState(accountId, contactKnown); + +Store given state for this account. + +---------- +Parameters +---------- + +#. ``accountId`` - ``string``: account id of a contact +#. ``contactKnown`` - ``boolean``: true if known, false if not + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + // mark accountId as a known contact + profile.setContactKnownState(accountId, true); + + + +------------------------------------------------------------------------------ + +.. _profile_loadForAccount: + +loadForAccount +================================================================================ + +.. code-block:: typescript + + profile.loadForAccount([tree]); + +Load profile for given account from global profile contract, if a tree is given, load that tree from ipld as well. + +---------- +Parameters +---------- + +#. ``tree`` - ``string`` (optional): tree to load ('bookmarkedDapps', 'contracts', ...), profile.treeLabels properties can be passed as arguments + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await profile.loadForAccount(profile.treeLabels.contracts); + + + +------------------------------------------------------------------------------ + +.. _profile_storeForAccount: + +storeForAccount +================================================================================ + +.. code-block:: typescript + + profile.storeForAccount(tree); + +Stores profile tree or given hash to global profile contract. + +---------- +Parameters +---------- + +#. ``tree`` - ``string``: tree to store ('bookmarkedDapps', 'contracts', ...) +#. ``ipldHash`` - ``string`` (optional): store this hash instead of the current tree for account + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await profile.storeForAccount(profile.treeLabels.contracts); + + + +------------------------------------------------------------------------------ + +.. _profile_loadFromIpld: + +loadFromIpld +================================================================================ + +.. code-block:: typescript + + profile.loadFromIpld(tree, ipldIpfsHash); + +Load profile from ipfs via ipld dag via ipfs file hash. + +---------- +Parameters +---------- + +#. ``tree`` - ``string``: tree to load ('bookmarkedDapps', 'contracts', ...) +#. ``ipldIpfsHash`` - ``string``: ipfs file hash that points to a file with ipld a hash + +------- +Returns +------- + +``Promise`` returns ``Profile``: this profile + +------- +Example +------- + +.. code-block:: typescript + + await profile.loadFromIpld(profile.treeLabels.contracts, ipldIpfsHash); + + + +------------------------------------------------------------------------------ + +.. _profile_storeToIpld: + +storeToIpld +================================================================================ + +.. code-block:: typescript + + profile.storeToIpld(tree); + +Store profile in ipfs as an ipfs file that points to a ipld dag. + +---------- +Parameters +---------- + +#. ``tree`` - ``string``: tree to store ('bookmarkedDapps', 'contracts', ...) + +------- +Returns +------- + +``Promise`` returns ``string``: hash of the ipfs file + +------- +Example +------- + +.. code-block:: typescript + + const storedHash = await profile.storeToIpld(profile.treeLabels.contracts); + + + += addressBook = +============================================================================== + +.. _profile_addContactKey: + +addContactKey +================================================================================ + +.. code-block:: typescript + + profile.addContactKey(address, context, key); + +Add a key for a contact to bookmarks. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: account key of the contact +#. ``context`` - ``string``: store key for this context, can be a contract, bc, etc. +#. ``key`` - ``string``: communication key to store + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await profile.addContactKey(accounts[0], 'context a', 'key 0x01_a'); + + + +------------------------------------------------------------------------------ + +.. _profile_addProfileKey: + +addProfileKey +================================================================================ + +.. code-block:: typescript + + profile.addProfileKey(address, key, value); + +Add a profile value to an account. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: account key of the contact +#. ``key`` - ``string``: store key for the account like alias, etc. +#. ``value`` - ``string``: value of the profile key + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await profile.addProfileKey(accounts[0], 'email', 'sample@example.org'); + await profile.addProfileKey(accounts[0], 'alias', 'Sample Example'); + + + +------------------------------------------------------------------------------ + +.. _profile_getAddressBookAddress: + +getAddressBookAddress +================================================================================ + +.. code-block:: typescript + + profile.getAddressBookAddress(address); + +Function description + +---------- +Parameters +---------- + +#. ``address`` - ``string``: contact address + +------- +Returns +------- + +``Promise`` returns ``any``: bookmark info + +------- +Example +------- + +.. code-block:: typescript + + await profile.getAddressBookAddress(accounts[0]); + + + +------------------------------------------------------------------------------ + +.. _profile_getAddressBook: + +getAddressBook +================================================================================ + +.. code-block:: typescript + + profile.getAddressBook(); + +Get the whole addressBook. + +---------- +Parameters +---------- + +(none) + +------- +Returns +------- + +``any``: entire address book + +------- +Example +------- + +.. code-block:: typescript + + await profile.getAddressBook(); + + + +------------------------------------------------------------------------------ + + +.. _profile_getContactKey: + +getContactKey +================================================================================ + +.. code-block:: typescript + + profile.getContactKey(address, context); + +Get a communication key for a contact from bookmarks. + +---------- +Parameters +---------- + +#. ``address`` - ``string```: account key of the contact +#. ``context`` - ``string```: store key for this context, can be a contract, bc, etc. + +------- +Returns +------- + +``Promise`` returns ``void``: matching key + +------- +Example +------- + +.. code-block:: typescript + + await profile.getContactKey(accounts[0], 'exampleContext'); + + + +------------------------------------------------------------------------------ + + +.. _profile_getProfileKey: + +getProfileKey +================================================================================ + +.. code-block:: typescript + + profile.getProfileKey(address, key); + +Get a key from an address in the address book. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: address to look up +#. ``key`` - ``string``: type of key to get + +------- +Returns +------- + +``Promise`` returns ``any``: key + +------- +Example +------- + +.. code-block:: typescript + + const alias = await profile.getProfileKey(accountId, 'alias'); + + + +------------------------------------------------------------------------------ + +.. _profile_removeContact: + +removeContact +================================================================================ + +.. code-block:: typescript + + profile.removeContact(address); + +Remove a contact from bookmarkedDapps. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: account key of the contact + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await profile.removeContact(address); + + + +------------------------------------------------------------------------------ + += bookmarkedDapps = +============================================================================== + + + +.. _profile_addDappBookmark: + +addDappBookmark +================================================================================ + +.. code-block:: typescript + + profile.addDappBookmark(address, description); + +Add a bookmark for a dapp. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: ENS name or contract address (if no ENS name is set) +#. ``description`` - ``DappBookmark``: description for bookmark + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const bookmark = { + "name": "taskboard", + "description": "Create todos and manage updates.", + "i18n": { + "description": { + "de": "Erstelle Aufgaben und überwache Änderungen", + "en": "Create todos and manage updates" + }, + "name": { + "de": "Task Board", + "en": "Task Board" + } + }, + "imgSquare": "...", + "standalone": true, + "primaryColor": "#e87e23", + "secondaryColor": "#fffaf5", + }; + await profile.addDappBookmark('sampletaskboard.evan', bookmark); + + + +------------------------------------------------------------------------------ + +.. _profile_getDappBookmark: + +getDappBookmark +================================================================================ + +.. code-block:: typescript + + profile.getDappBookmark(address); + +Get a bookmark for a given address if any. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: ENS name or contract address (if no ENS name is set) + +------- +Returns +------- + +``Promise`` returns ``any``: bookmark info + +------- +Example +------- + +.. code-block:: typescript + + await profile.getDappBookmark('sample1.evan'); + + + +-------------------------------------------------------------------------------- + +.. _profile_getBookmarkDefinition: + +getBookmarkDefinition +================================================================================ + +.. code-block:: typescript + + profile.getBookmarkDefinition(); + +Get all bookmarks for profile. + +---------- +Parameters +---------- + +(none) + +------- +Returns +------- + +``Promise`` returns ``any``: all bookmarks for profile + +------- +Example +------- + +.. code-block:: typescript + + await profile.getBookmarkDefinitions(); + + + +------------------------------------------------------------------------------ + +.. _profile_removeDappBookmark: + +removeDappBookmark +================================================================================ + +.. code-block:: typescript + + profile.removeDappBookmark(address); + +Remove a dapp bookmark from the bookmarkedDapps. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: address of the bookmark to remove + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await profile.removeDappBookmark(address); + + + +------------------------------------------------------------------------------ + +.. _profile_setDappBookmarks: + +setDappBookmarks +================================================================================ + +.. code-block:: typescript + + profile.setDappBookmarks(bookmarks); + +Set bookmarks with given value. + +---------- +Parameters +---------- + +#. ``bookmarks`` - ``any``: The options used for calling + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + const bookmarks = await profile.getBookmarkDefinitions(); + // update bookmarks + // ... + await profile.setDappBookmarks(bookmarks); + + + +------------------------------------------------------------------------------ + += contracts = +============================================================================== + + + +.. _profile_addContract: + +addContract +================================================================================ + +.. code-block:: typescript + + profile.addContract(address, data); + +Add a contract to the current profile. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: contact address +#. ``data`` - ``any``: bookmark metadata + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await profile.addBcContract('testbc.evan', '0x', contractDescription); + + + +------------------------------------------------------------------------------ + + +.. _profile_getContracts: + +getContracts +================================================================================ + +.. code-block:: typescript + + profile.getContracts(); + +Get all contracts for the current profile. + +---------- +Parameters +---------- + +(none) + +------- +Returns +------- + +``Promise`` returns ``any``: contracts info + +------- +Example +------- + +.. code-block:: typescript + + await profile.getContracts(); + + + +------------------------------------------------------------------------------ + +.. _profile_getContract: + +getContract +================================================================================ + +.. code-block:: typescript + + profile.getContract(address); + +Get a specific contract entry for a given address. + +---------- +Parameters +---------- + +#. ``address`` - ``string``: contact address + +------- +Returns +------- + +``Promise`` returns ``any``: bookmark info + +------- +Example +------- + +.. code-block:: typescript + + await profile.getContract('testbc.evan'); + + + +-------------------------------------------------------------------------------- + +.. _profile_addBccContract: + +addBccContract +================================================================================ + +.. code-block:: typescript + + profile.addBccContract(bc, address, data) + +Add a contract to a business center scope the current profile. + +---------- +Parameters +---------- + +#. ``bc`` - ``string``: business center ens address or contract address +#. ``address`` - ``string``: contact address +#. ``data`` - ``any``: bookmark metadata + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await profile.addBcContract('testbc.evan', '0x...', contractDescription); + + + +------------------------------------------------------------------------------ + +.. _profile_getBcContract: + +getBcContract +================================================================================ + +.. code-block:: typescript + + profile.getBcContract(bc, address); + +Get a specific contract entry for a given address. + +---------- +Parameters +---------- + +#. ``bcc`` - ``string``: business center ens address or contract address +#. ``address`` - ``string``: contact address + +------- +Returns +------- + +``Promise`` returns ``any``: bookmark info + +------- +Example +------- + +.. code-block:: typescript + + await profile.getBcContract('testbc.evan', '0x...'); + + + +------------------------------------------------------------------------------ + +.. _profile_getBcContracts: + +getBcContracts +================================================================================ + +.. code-block:: typescript + + profile.getBcContracts(bc, address); + +Get all contracts grouped under a business center. + +---------- +Parameters +---------- + +#. ``bcc`` - ``string``: business center ens address or contract address + +------- +Returns +------- + +``Promise`` returns ``any``: bookmark info + +------- +Example +------- + +.. code-block:: typescript + + await profile.getBcContracts('testbc.evan'); + + + +------------------------------------------------------------------------------ + += publicKey = +============================================================================== + + + +.. _profile_addPublicKey: + +addPublicKey +================================================================================ + +.. code-block:: typescript + + profile.addPublicKey(key); + +Add a key for a contact to bookmarks. + +---------- +Parameters +---------- + +#. ``key`` - ``string``: public Diffie Hellman key part to store + +------- +Returns +------- + +``Promise`` returns ``void``: resolved when done + +------- +Example +------- + +.. code-block:: typescript + + await profile.addPublicKey('...'); + + + +------------------------------------------------------------------------------ + +.. _profile_getPublicKey: + +getPublicKey +================================================================================ + +.. code-block:: typescript + + profile.getPublicKey(); + +Get public key of profiles. + +---------- +Parameters +---------- + +(none) + +------- +Returns +------- + +``Promise`` returns ``any``: public key + +------- +Example +------- + +.. code-block:: typescript + + const key = await profile.getPublicKey(); + + + +.. required for building markup + +.. |source contractLoader| replace:: ``ContractLoader`` +.. _source contractLoader: /contracts/contract-loader.html + +.. |source cryptoProvider| replace:: ``CryptoProvider`` +.. _source cryptoProvider: /encryption/crypto-provider.html + +.. |source dataContract| replace:: ``DataContract`` +.. _source dataContract: /contracts/data-contract.html + +.. |source executor| replace:: ``Executor`` +.. _source executor: /blockchain/executor.html + +.. |source ipld| replace:: ``Ipld`` +.. _source ipld: /dfs/ipld.html + +.. |source keyExchange_getDiffieHellmanKeys| replace:: ``KeyExchange`` +.. _source keyExchange_getDiffieHellmanKeys: /profile/key-exchange.html#getdiffiehellmankeys + +.. |source logLevel| replace:: ``LogLevel`` +.. _source logLevel: /common/logger.html#loglevel + +.. |source logLogInterface| replace:: ``LogLogInterface`` +.. _source logLogInterface: /common/logger.html#logloginterface + +.. |source nameResolver| replace:: ``NameResolver`` +.. _source nameResolver: /blockchain/name-resolver.html \ No newline at end of file diff --git a/docs/template.rst b/docs/template.rst new file mode 100644 index 00000000..5e383103 --- /dev/null +++ b/docs/template.rst @@ -0,0 +1,38 @@ + +.. _base-contract_changeContractStateSample: + +changeContractState +================================================================================ + +.. code-block:: typescript + + myContract.methods.myMethod([param1[, param2[, ...]]]).call(options[, callback]) + +Will call a "constant" method and execute its smart contract method in the EVM without sending any transaction. Note calling can not alter the smart contract state. + +---------- +Parameters +---------- + +#. ``options`` - ``object``: The options used for calling. + * ``from`` - ``string`` (optional): The address the call "transaction" should be made from. +#. ``callback`` - ``Function`` (optional): This callback will be fired.. +#. ``somethingElse`` - ``string`` (optional): this can be set if required, defaults to ``"latest"``. + +------- +Returns +------- + +``Promise`` returns ``any``: The return value(s) of the smart contract method. +If it returns a single value, it's returned as is. If it has multiple return values they are returned as an object with properties and indices: + +------- +Example +------- + +.. code-block:: typescript + + // ... + + +------------------------------------------------------------------------------ \ No newline at end of file diff --git a/docu/img/multikeys.png b/docu/img/multikeys.png new file mode 100644 index 00000000..10121c77 Binary files /dev/null and b/docu/img/multikeys.png differ diff --git a/docu/img/multikeys_lifetime.png b/docu/img/multikeys_lifetime.png new file mode 100644 index 00000000..e5011208 Binary files /dev/null and b/docu/img/multikeys_lifetime.png differ diff --git a/package.json b/package.json new file mode 100644 index 00000000..6ea0b10e --- /dev/null +++ b/package.json @@ -0,0 +1,101 @@ +{ + "license": "AGPL-3.0-only", + "author": "contractus", + "dependencies": { + "@evan.network/dbcp": "^1.0.3", + "@evan.network/smart-contracts-admin": "^1.0.0", + "@evan.network/smart-contracts-core": "git@github.com:evannetwork/smart-contracts-core.git#develop", + "@types/node": "8.0.53", + "ajv": "^5.5.1", + "babel-plugin-transform-es3-property-literals": "^6.22.0", + "bignumber.js": "3.0.1", + "bitcore-mnemonic": "^1.5.0", + "bs58": "4.0.1", + "crypto-browserify": "3.12.0", + "eth-lightwallet": "git+https://github.com/ConsenSys/eth-lightwallet.git#251eda97f705dbfae5ef099479aa19160028ae80", + "ethereumjs-tx": "1.3.1", + "gulp-replace": "^0.6.1", + "ipld-graph-builder": "1.3.7", + "lodash": "4.17.5", + "prottle": "^1.0.3" + }, + "description": "blockchain interaction core library", + "devDependencies": { + "@types/chai": "4.0.6", + "@types/chai-as-promised": "7.1.0", + "@types/mocha": "2.2.44", + "babel-core": "^6.26.0", + "babel-plugin-remove-comments": "^2.0.0", + "babel-plugin-transform-es3-member-expression-literals": "^6.22.0", + "babel-plugin-transform-flow-strip-types": "^6.22.0", + "babel-polyfill": "^6.26.0", + "babel-preset-env": "^1.6.1", + "babel-preset-es2015": "^6.24.1", + "babel-preset-stage-0": "^6.24.1", + "babelify": "^8.0.0", + "browserify": "^14.5.0", + "chai": "4.1.2", + "chai-as-promised": "7.1.1", + "common-shakeify": "^0.4.6", + "disc": "^1.3.3", + "env-cmd": "^7.0.0", + "gulp": "^3.9.1", + "gulp-open": "^2.0.0", + "gulp-sourcemaps": "^2.6.1", + "ipfs-api": "^17.5.0", + "mocha": "4.0.1", + "ts-node": "3.3.0", + "typescript": "2.6.2", + "vinyl-buffer": "^1.0.0", + "vinyl-source-stream": "^1.1.0", + "web3": "1.0.0-beta.27", + "web3-bzz": "1.0.0-beta.27", + "web3-core": "1.0.0-beta.27", + "web3-core-helpers": "1.0.0-beta.27", + "web3-core-method": "1.0.0-beta.27", + "web3-core-promievent": "1.0.0-beta.27", + "web3-core-requestmanager": "1.0.0-beta.27", + "web3-core-subscriptions": "1.0.0-beta.27", + "web3-eth": "1.0.0-beta.27", + "web3-eth-abi": "1.0.0-beta.27", + "web3-eth-accounts": "1.0.0-beta.27", + "web3-eth-contract": "1.0.0-beta.27", + "web3-eth-iban": "1.0.0-beta.27", + "web3-eth-personal": "1.0.0-beta.27", + "web3-net": "1.0.0-beta.27", + "web3-providers-http": "1.0.0-beta.27", + "web3-providers-ipc": "1.0.0-beta.27", + "web3-providers-ws": "1.0.0-beta.27", + "web3-shh": "1.0.0-beta.27", + "web3-utils": "1.0.0-beta.27" + }, + "homepage": "https://evannetwork.github.io/", + "keywords": [ + "blockchain", + "ethereum", + "smart-contracts", + "javascript", + "typescript", + "API" + ], + "main": "dist/index.js", + "name": "@evan.network/api-blockchain-core", + "repository": { + "type": "git", + "url": "git@github.com:evannetwork/api-blockchain-core.git" + }, + "scripts": { + "build": "tsc", + "build-all": "env-cmd ./.env.local npm run build-contracts && env-cmd ./.env.local npm run build-bundles", + "build-bundles": "env-cmd ./.env.local npm run build && gulp --preserve-symlinks --gulpfile scripts/bundle.js", + "build-contracts": "node scripts/buildContracts.js", + "discify": "npm run build && gulp --gulpfile scripts/discify.js", + "installl": "npm install && ./scripts/link.sh", + "test": "env-cmd ./.env.local npm run build && mocha --inspect -r ts-node/register src/**/*.spec.ts src/*.spec.ts", + "testunit": "env-cmd ./.env.local npm run build && mocha --inspect -r ts-node/register $*", + "testunitbail": "env-cmd ./.env.local npm run build && mocha -b --inspect -r ts-node/register $*", + "testunitbrk": "env-cmd ./.env.local npm run build && mocha --inspect-brk -r ts-node/register $*" + }, + "types": "./dist/index.d.ts", + "version": "1.0.2" +} diff --git a/scripts/buildContracts.js b/scripts/buildContracts.js new file mode 100644 index 00000000..96625ddf --- /dev/null +++ b/scripts/buildContracts.js @@ -0,0 +1,41 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +const smartContractsAdmin = require('@evan.network/smart-contracts-admin'); +const smartContractsCore = require('@evan.network/smart-contracts-core'); + + +const solc = new smartContractsCore.Solc({ + config: { compileContracts: false, }, + log: console.log, +}); + +try { + solc.ensureCompiled([smartContractsAdmin.getContractsPath()]); +} catch(ex) { + console.error(`building contracts failed with: ${ex.msg || ex}${ex.stack ? '; ' + ex.stack : ''}`); +} diff --git a/scripts/bundle.js b/scripts/bundle.js new file mode 100644 index 00000000..cd62a88c --- /dev/null +++ b/scripts/bundle.js @@ -0,0 +1,153 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +const fs = require('fs'); +const browserify = require('browserify'); +const gulp = require('gulp'); +const path = require('path'); +const sourcemaps = require('gulp-sourcemaps'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const commonShake = require('common-shakeify'); +const gulpReplace = require('gulp-replace'); +const bundles = require('./bundles.js'); + +const checkFolder = function(folder) { + try { + fs.mkdirSync(folder); + } catch (err) { } +}; + +const parseEnsName = function(ens) { + return ens.replace(/-/g, ''); +}; + +const browserifyFile = async function(bundleName) { + const srcFolder = `../src/bundles/${bundleName}`; + const distFolder = `../dist/bundles/${bundleName}`; + const bundleFolder = `../bundles/${parseEnsName(bundleName)}`; + + checkFolder(bundleFolder); + const ethjsUtils = require('../node_modules/ethjs-util/package.json'); + + await new Promise((resolve, reject) => + browserify(`../dist/bundles/bcc/bcc.js`, { + standalone: 'bcc', + debug: true, + }) + .exclude('bcc') + .transform("babelify", { + //parse all sub node_modules es5 to es6 + global: true, + + //important! + // underscore gets broken when we try to parse it + ignore: /underscore/, + + //use babel to transform es6 to es5 babel to transform es6 to es5 + presets: [ + "babel-preset-es2015", + "babel-preset-stage-0", + "babel-preset-env" + ].map(require.resolve), + + plugins: [ + "babel-plugin-transform-es2015-template-literals", + "babel-plugin-transform-es2015-literals", + "babel-plugin-transform-es2015-function-name", + "babel-plugin-transform-es2015-arrow-functions", + "babel-plugin-transform-es2015-block-scoped-functions", + "babel-plugin-transform-es2015-classes", + "babel-plugin-transform-es2015-object-super", + "babel-plugin-transform-es2015-shorthand-properties", + "babel-plugin-transform-es2015-computed-properties", + "babel-plugin-transform-es2015-for-of", + "babel-plugin-transform-es2015-sticky-regex", + "babel-plugin-transform-es2015-unicode-regex", + "babel-plugin-check-es2015-constants", + "babel-plugin-transform-es2015-spread", + "babel-plugin-transform-es2015-parameters", + "babel-plugin-transform-es2015-destructuring", + "babel-plugin-transform-es2015-block-scoping", + "babel-plugin-transform-object-rest-spread", + "babel-plugin-transform-es3-member-expression-literals", + "babel-plugin-transform-es3-property-literals", + "babel-plugin-remove-comments" + ].map(require.resolve) + }) + .plugin(commonShake, { /* options */ }) + .bundle() + .pipe(source(`bcc.js`)) + .pipe(buffer()) + .pipe(sourcemaps.init({loadMaps: true})) + .pipe(sourcemaps.write('./', { + sourceMappingURL: function(file) { + return 'http://localhost:3000/external/dbcpread/' + file.relative + '.map'; + } + })) + .pipe(gulp.dest(`${bundleFolder}`)) + .on('end', () => { + const dbcpPath = path.resolve(`${srcFolder}/dbcp.json`); + + gulp.src([ dbcpPath ]).pipe(gulp.dest(bundleFolder)); + gulp.src([ `../dist/config.js` ]).pipe(gulp.dest(bundleFolder)); + + const dbcp = { + dbcpPath: `${dbcpPath}` + }; + fs.writeFileSync( + `${bundleFolder}/dbcpPath.json`, + JSON.stringify(dbcp) + ); + resolve(); + }) + ) + + await new Promise((resolve, reject) => gulp + .src(`${bundleFolder}/${bundleName}.js`) + .pipe(gulpReplace('if (global._babelPolyfill) {', 'if (false) {')) + .pipe(gulpReplace('bitcore.versionGuard(global._bitcore)', 'bitcore.versionGuard()')) + .pipe(gulpReplace('/* common-shake removed: exports.createDecipher = */ void createDecipher', 'exports.createDecipher = createDecipher')) + .pipe(gulpReplace('/* common-shake removed: exports.createDecipheriv = */ void createDecipheriv', 'exports.createDecipheriv = createDecipheriv')) + .pipe(gulpReplace('/* common-shake removed: exports.createCipheriv = */ void createCipheriv', 'exports.createCipheriv = createCipheriv')) + .pipe(gulpReplace('/* common-shake removed: exports.createCipher = */ void createCipher', 'exports.createCipher = createCipher')) + .pipe(gulpReplace('exports.randomBytes = /* common-shake removed: exports.rng = */ void 0, /* common-shake removed: exports.pseudoRandomBytes = */ void 0, /* common-shake removed: exports.prng = */ require(\'randombytes\');', 'exports.randomBytes = require(\'randombytes\');')) + .pipe(gulpReplace('require("babel-polyfill");', '')) + .pipe(gulpReplace('var createBC = function () {', 'require("babel-polyfill");\nvar createBC = function () {')) + .pipe(gulp.dest(`${bundleFolder}`)) + .on('end', () => resolve()) + ) +} + +gulp.task('build-bundles', function(callback) { + checkFolder('../bundles'); + browserifyFile('bcc') + .then(() => callback()); + +}); + +gulp.task('default', ['build-bundles']); \ No newline at end of file diff --git a/scripts/bundles.js b/scripts/bundles.js new file mode 100644 index 00000000..c01ad29e --- /dev/null +++ b/scripts/bundles.js @@ -0,0 +1,30 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +module.exports = [ + 'bcc' +]; \ No newline at end of file diff --git a/scripts/discify.js b/scripts/discify.js new file mode 100644 index 00000000..bac2bd46 --- /dev/null +++ b/scripts/discify.js @@ -0,0 +1,139 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +const fs = require('fs'); +const browserify = require('browserify'); +const disc = require('disc'); +const gulp = require('gulp'); +const open = require('gulp-open'); +const os = require('os'); +const path = require('path'); +const sourcemaps = require('gulp-sourcemaps'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); + +const bundles = require('./bundles.js'); + +const checkFolder = function(folder) { + try { + fs.mkdirSync(folder); + } catch (err) { } +}; + +const parseEnsName = function(ens) { + return ens.replace(/-/g, ''); +}; + +const browserifyFile = function(bundleName) { + const srcFolder = `../src/bundles/${bundleName}`; + const distFolder = `../dist/bundles/${bundleName}`; + const bundleFolder = `../bundles/${parseEnsName(bundleName)}`; + + checkFolder(bundleFolder); + + return new Promise(function(resolve, reject) { + const ethjsUtils = require('../node_modules/ethjs-util/package.json'); + + return browserify(`${distFolder}/${bundleName}.js`, { + standalone: bundleName, + debug: true, + fullPaths: true, + ignore: '../core/core' + }) + .exclude('bcc-core') + .exclude('bcc-profile') + .exclude('bcc-bc') + .transform("babelify", { + //parse all sub node_modules es5 to es6 + global: true, + + //important! + // underscore gets broken when we try to parse it + ignore: /underscore/, + + //use babel to transform es6 to es5 babel to transform es6 to es5 + presets: [ + "babel-preset-es2015", + "babel-preset-stage-0", + "babel-preset-env" + ].map(require.resolve), + + plugins: [ + "babel-plugin-transform-es2015-template-literals", + "babel-plugin-transform-es2015-literals", + "babel-plugin-transform-es2015-function-name", + "babel-plugin-transform-es2015-arrow-functions", + "babel-plugin-transform-es2015-block-scoped-functions", + "babel-plugin-transform-es2015-classes", + "babel-plugin-transform-es2015-object-super", + "babel-plugin-transform-es2015-shorthand-properties", + "babel-plugin-transform-es2015-computed-properties", + "babel-plugin-transform-es2015-for-of", + "babel-plugin-transform-es2015-sticky-regex", + "babel-plugin-transform-es2015-unicode-regex", + "babel-plugin-check-es2015-constants", + "babel-plugin-transform-es2015-spread", + "babel-plugin-transform-es2015-parameters", + "babel-plugin-transform-es2015-destructuring", + "babel-plugin-transform-es2015-block-scoping", + "babel-plugin-transform-object-rest-spread", + "babel-plugin-transform-es3-member-expression-literals", + "babel-plugin-transform-es3-property-literals", + "babel-plugin-remove-comments" + ].map(require.resolve) + }) + .bundle() + .pipe(disc()) + .pipe(fs.createWriteStream(`../discify/${bundleName}.html`)) + }); +} +gulp.task('discify-bundles', function(callback) { + checkFolder('../discify'); + + Promise + .all( + bundles.map(bundleName => browserifyFile(bundleName)) + ) + .then(() => callback()); +}); + +gulp.task('open-discify-bundle', [ 'discify-bundles' ], function() { + var browser = os.platform() === 'linux' ? 'google-chrome' : ( + os.platform() === 'darwin' ? 'google chrome' : ( + os.platform() === 'win32' ? 'chrome' : 'firefox')); + + return bundles.map(bundleName => + gulp + .src(`../discify/${bundleName}.html`) + .pipe(open({app: browser})) + ) +}); + +gulp.task('default', [ + 'discify-bundles', + 'open-discify-bundle' +]); \ No newline at end of file diff --git a/scripts/link.sh b/scripts/link.sh new file mode 100755 index 00000000..0d7bd1c4 --- /dev/null +++ b/scripts/link.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +root="$(pwd)/../" + +# $1 is parent module +# $2 is child module +# $3 switches child npm linking on and off +linkIn () { + echo "--- $1 $2 $3 -------------------------" + if [ $3 = true ] ; then + cd $root/$2 && npm link + fi + MODULE=$2 + cd $root/$1 && npm link $MODULE + echo "" +} + +linkIn "blockchain-core" "@evan.network/dbcp" true +linkIn "blockchain-core" "smart-contracts" true diff --git a/src/bundles/bcc/bcc.ts b/src/bundles/bcc/bcc.ts new file mode 100644 index 00000000..f7877b37 --- /dev/null +++ b/src/bundles/bcc/bcc.ts @@ -0,0 +1,654 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +require('babel-polyfill'); + +// import to handle bundle from outside +import IpfsRemoteConstructor = require('ipfs-api'); +import keystore = require('eth-lightwallet/lib/keystore'); +import Mnemonic = require('bitcore-mnemonic'); +import Web3 = require('web3'); +import prottle = require('prottle'); + +// used for building bundle +import { + ContractLoader, + DfsInterface, + Envelope, + EventHub, + Executor, + Ipfs, + KeyProvider, + KeyProviderInterface, + Logger, + LogLevel, + NameResolver, + SignerExternal, + SignerInternal, + Unencrypted, + Validator, +} from '@evan.network/dbcp'; + +import { Aes } from '../../encryption/aes'; +import { AesBlob } from '../../encryption/aes-blob'; +import { AesEcb } from '../../encryption/aes-ecb'; +import { BusinessCenterProfile } from '../../profile/business-center-profile'; +import { CryptoProvider } from '../../encryption/crypto-provider'; +import { BaseContract } from '../../contracts/base-contract/base-contract'; +import { DataContract } from '../../contracts/data-contract/data-contract'; +import { Description } from '../../shared-description'; +import { Ipld } from '../../dfs/ipld'; +import { KeyExchange } from '../../keyExchange'; +import { Mailbox, Mail } from '../../mailbox'; +import { Onboarding } from '../../onboarding'; +import { Profile } from '../../profile/profile'; +import { RightsAndRoles } from '../../contracts/rights-and-roles'; +import { Sharing } from '../../contracts/sharing'; + +/**************************************************************************************************/ + +// runtime variables + +/** + * Is used to handle a basic blockchain-core loading without any account specific stuff + */ +let CoreRuntime: CoreInstance; + +/** + * uses the CoreRuntime to enhanche it with account specific stuff + */ +let ProfileRuntime: ProfileInstance; + +/** + * Use a ProfileRuntime to interact with Business centers. + */ +let BCRuntime; + +// used for global & shared available logLog +let logLog = [ ]; + +// push everything into the logLog +let logLogLevel = LogLevel.debug; + +// assign to export Buffer; +const buffer = Buffer; + +/**************************************************************************************************/ +// interfaces + +export interface SolcInterface { + getContracts(): any; +} + +export interface CoreBundle { + createCore: Function, + createAndSetCore: Function, + ContractLoader: ContractLoader, + CryptoProvider: CryptoProvider, + Description: Description, + DfsInterface: DfsInterface, + EventHub: EventHub, + Executor: Executor, + Ipfs: Ipfs, + NameResolver: NameResolver, + Unencrypted: Unencrypted, + CoreRuntime: CoreInstance, + isAccountOnboarded: Function, + IpfsRemoteConstructor: IpfsRemoteConstructor, + keystore: keystore, + Mnemonic: Mnemonic, + KeyProviderInterface: KeyProviderInterface, + KeyProvider: KeyProvider, +} + +export interface CoreBundleOptions { + web3: any; + solc: SolcInterface; + config: any; + executor?: Executor; + contractLoader?: ContractLoader; + description?: Description; + dfs?: DfsInterface; + dfsRemoteNode?: any; + nameResolver?: NameResolver; + ipfsCache?: any; +} + +export interface CoreInstance { + web3: any, + description: Description, + nameResolver: NameResolver, + dfs: Ipfs, + contractLoader: ContractLoader, + executor: Executor, + solc: SolcInterface, + contracts: any, + config: any +} + +export interface ProfileBundle { + create: Function, + createAndSet: Function, + ProfileRuntime: ProfileInstance, + Aes: Aes, + Ipld: Ipld, + KeyExchange: KeyExchange, + Logger: Logger, + Mailbox: Mailbox, + Onboarding: Onboarding, + Profile: Profile, + RightsAndRoles: RightsAndRoles, + Sharing: Sharing, + SignerExternal: SignerExternal, + SignerInternal: SignerInternal, +} + +export interface ProfileBundleOptions { + CoreBundle: CoreBundle; + coreOptions: CoreBundleOptions; + + accountId: string; + // 'internal' / 'external' + signer: SignerInternal | SignerExternal; + keyProvider: KeyProvider; +} + +export interface ProfileInstance { + // profile exports + ipldInstance: Ipld, + keyExchange: KeyExchange, + mailbox: Mailbox, + profile: Profile, + sharing: Sharing, + dataContract: DataContract, + keyProvider: KeyProvider, + + // core exports + coreInstance: CoreInstance, +} + +export interface BCBundleOptions { + ensDomain: string, + ProfileBundle: ProfileBundle +} + +export interface BCInstance { + ensDomain: string, + bcAddress: string, + businessCenter: any, + bcRoles: RightsAndRoles, + ipld: Ipld, + bcProfiles: BusinessCenterProfile, + description: any, + dataContract: DataContract +} + +/**************************************************************************************************/ +// Core stuff + +/** + * Creates a new CoreInstance + * + * @param {CoreBundleOptions} options core bundle options + * @return {CoreInstance} new Core instance + */ +const createCore = function(options: CoreBundleOptions): CoreInstance { + const web3 = options.web3; + + // contract loader + const solc = options.solc; + const contracts = solc.getContracts(); + const contractLoader = options.contractLoader || new ContractLoader({ + contracts, + web3, + logLog, + logLogLevel + }); + + // executor + const executor = options.executor || new Executor({ web3, logLog, logLogLevel }); + + // dfs + let dfs; + if (options.dfs) { + dfs = options.dfs; + } else if (options.dfsRemoteNode) { + dfs = new Ipfs({remoteNode: options.dfsRemoteNode, cache: options.ipfsCache, logLog, logLogLevel }); + // TODO cleanup after dbcp > 1.0.3 release + if(options.ipfsCache) { + dfs.cache = options.ipfsCache; + } + } else { + throw new Error('missing dfsNode or dfs instance in bundle creator'); + } + + // name resolver + let nameResolver; + if (options.nameResolver) { + nameResolver = options.nameResolver; + } else { + let nameResolverConfig; + if (options.config && options.config.nameResolver) { + nameResolverConfig = options.config.nameResolver; + } else { + throw new Error('missing options.config.nameResolver, config.nameResolver ' + + 'and nameResolver instance in bundle creator'); + } + nameResolver = new NameResolver({ + config: nameResolverConfig, + executor, + contractLoader, + web3, + logLog, + logLogLevel + }); + } + + // description + let unencrypted = new Unencrypted(); + let description; + if (options.description) { + description = options.description; + } else { + const cryptoProvider = new CryptoProvider({ unencrypted }); + + description = options.description || new Description({ + contractLoader, + cryptoProvider, + dfs, + executor, + nameResolver, + sharing: null, + web3, + logLog, + logLogLevel + }); + } + + const eventHub = new EventHub({ + config: options.config, + contractLoader: contractLoader, + nameResolver: nameResolver, + logLog, + logLogLevel + }); + + executor.eventHub = eventHub; + + return { + web3, + description, + nameResolver, + dfs, + contractLoader, + executor, + solc, + contracts, + config: options.config, + } +} + +/** + * Creates a new CoreInstance and update the CoreInstance export. + * + * @param {CoreBundleOptions} options core bundle options + * @return {CoreInstance} new Core instance + */ +let createAndSetCore = function(options: CoreBundleOptions): CoreInstance { + CoreRuntime = createCore(options); + + return CoreRuntime; +} + +/** + * Overwrite the current CoreInstance + * + * @param {CoreInstance} coreInstance CoreInstance to use + */ +let setCore = function(coreInstance: CoreInstance) { + CoreRuntime = coreInstance; +} + +/**************************************************************************************************/ +// profile stuff + +/** + * Creates a new ProfileInstance + * + * @param {BundleOptions} options core bundle options + * @return {ProfileInstance} new Core instance + */ +const create = function(options: ProfileBundleOptions): ProfileInstance { + const web3 = options.coreOptions.web3; + + // => correct executor + const executor = new Executor({ + config: options.coreOptions.config, + web3: web3, + signer: options.signer, + logLog, + logLogLevel + }); + + options.coreOptions.executor = executor; + + const coreInstance = options.CoreBundle.createAndSetCore(options.coreOptions); + + coreInstance.description.cryptoProvider = new CryptoProvider({ + unencrypted: new Unencrypted(), + aes: new Aes(), + aesBlob: new AesBlob({ + dfs: coreInstance.dfs + }), + 'aesEcb': new AesEcb(), + logLog, + logLogLevel + }); + + options.coreOptions.executor.init({ + eventHub: executor.eventHub + }); + + coreInstance.description.keyProvider = options.keyProvider; + + // update executor + coreInstance.description.executor = executor; + coreInstance.nameResolver.executor = executor; + + const ipldInstance = new Ipld({ + 'ipfs': coreInstance.dfs, + 'keyProvider': options.keyProvider, + 'cryptoProvider': coreInstance.description.cryptoProvider, + defaultCryptoAlgo: 'aes', + originator: coreInstance.nameResolver.soliditySha3(options.accountId), + nameResolver: coreInstance.nameResolver, + logLog, + logLogLevel + }); + + const sharing = new Sharing({ + contractLoader: coreInstance.contractLoader, + cryptoProvider: coreInstance.description.cryptoProvider, + description: coreInstance.description, + executor: coreInstance.executor, + dfs: coreInstance.dfs, + keyProvider: (options.keyProvider), + nameResolver: coreInstance.nameResolver, + defaultCryptoAlgo: 'aes', + logLog, + logLogLevel + }); + + const mailbox = new Mailbox({ + mailboxOwner: options.accountId, + nameResolver: coreInstance.nameResolver, + ipfs: coreInstance.dfs, + contractLoader: coreInstance.contractLoader, + cryptoProvider: coreInstance.description.cryptoProvider, + keyProvider: (options.keyProvider), + defaultCryptoAlgo: 'aes', + logLog, + logLogLevel + }); + + const keyExchange = new KeyExchange({ + mailbox: mailbox, + cryptoProvider: coreInstance.description.cryptoProvider, + defaultCryptoAlgo: 'aes', + account: options.accountId, + keyProvider: (options.keyProvider), + logLog, + logLogLevel + }); + + const dataContract = new DataContract({ + cryptoProvider: coreInstance.description.cryptoProvider, + dfs: coreInstance.dfs, + executor, + loader: coreInstance.contractLoader, + nameResolver: coreInstance.nameResolver, + sharing: sharing, + web3: coreInstance.web3, + description: coreInstance.description, + logLog, + logLogLevel + }); + + const profile = new Profile({ + ipld: ipldInstance, + nameResolver: coreInstance.nameResolver, + defaultCryptoAlgo: 'aes', + executor, + contractLoader: coreInstance.contractLoader, + accountId: options.accountId, + dataContract, + logLog, + logLogLevel + }); + + (options.keyProvider).origin.init(profile); + + coreInstance.description.sharing = sharing; + + return { + // profile exports + ipldInstance, + keyExchange, + mailbox, + profile, + sharing, + keyProvider: options.keyProvider, + dataContract, + // core exports + coreInstance: coreInstance + }; +} + +/** + * Create a new ProfileInstance and update the ProfileInstance export. + * + * @param {BundleOptions} options core bundle options + * @return {ProfileInstance} new Core instance + */ +let createAndSet = function(options: ProfileBundleOptions): ProfileInstance { + ProfileRuntime = create(options); + + return ProfileRuntime; +} + +/**************************************************************************************************/ +// bc stuff + +/** + * Create a new BCInstance. + * + * @param {BundleOptions} options bundle options + * @return {BCInstance} new BC instance + */ +async function createBC(options: BCBundleOptions) { + const ensDomain = options.ensDomain; + const ProfileRuntime = options.ProfileBundle.ProfileRuntime; + const CoreRuntime = ProfileRuntime.coreInstance; + + // if user entered ens address, resolve it + let bcAddress = ensDomain; + if (bcAddress.indexOf('0x') !== 0) { + bcAddress = await CoreRuntime.nameResolver.getAddress(ensDomain); + } + + const nameResolverConfig = JSON.parse(JSON.stringify(CoreRuntime.config.nameResolver)); + nameResolverConfig.labels.businessCenterRoot = ensDomain; + + const nameResolver = new NameResolver({ + config: nameResolverConfig, + executor: CoreRuntime.executor, + contractLoader: CoreRuntime.contractLoader, + web3: CoreRuntime.web3, + logLog, + logLogLevel + }); + + const businessCenter = CoreRuntime.contractLoader.loadContract('BusinessCenter', bcAddress); + const bcRoles = new RightsAndRoles({ + contractLoader: CoreRuntime.contractLoader, + executor: CoreRuntime.executor, + nameResolver: nameResolver, + web3: CoreRuntime.web3, + logLog, + logLogLevel + }); + + const ipld = new Ipld({ + ipfs: CoreRuntime.dfs, + keyProvider: ProfileRuntime.keyProvider, + cryptoProvider: CoreRuntime.description.cryptoProvider, + defaultCryptoAlgo: 'aes', + originator: nameResolver.soliditySha3(ensDomain), + nameResolver, + logLog, + logLogLevel + }); + + const bcProfiles = new BusinessCenterProfile({ + ipld: ipld, + nameResolver: nameResolver, + defaultCryptoAlgo: 'aes', + bcAddress: ensDomain, + cryptoProvider: CoreRuntime.description.cryptoProvider, + logLog, + logLogLevel + }); + + const dataContract = new DataContract({ + cryptoProvider: CoreRuntime.description.cryptoProvider, + dfs: CoreRuntime.dfs, + executor: CoreRuntime.executor, + loader: CoreRuntime.contractLoader, + nameResolver: nameResolver, + sharing: ProfileRuntime.sharing, + web3: CoreRuntime.web3, + description: CoreRuntime.description, + logLog, + logLogLevel + }); + + const description = await CoreRuntime.description.getDescriptionFromEns(ensDomain); + + return { + ensDomain, + bcAddress, + businessCenter, + bcRoles, + ipld, + bcProfiles, + description: (description), + dataContract + }; +} + +/** + * Creates and set BCInstance. + * + * @param {BundleOptions} options Bundle options + * @return {BCInstance} new BC Instance + */ +let createAndSetBC = function(options: BCBundleOptions): BCInstance { + BCRuntime = createBC(options); + + return BCRuntime; +} + +/** + * Check if a account is onboarded + * + * @param {string} account account id to test + * @return {boolean} True if account onboarded, False otherwise + */ +const isAccountOnboarded = async function(account: string): Promise { + try { + const ensName = CoreRuntime.nameResolver.getDomainName(CoreRuntime.nameResolver.config.domains.profile); + const address = await CoreRuntime.nameResolver.getAddress(ensName); + const contract = CoreRuntime.nameResolver.contractLoader.loadContract('ProfileIndex', address); + const hash = await CoreRuntime.nameResolver.executor.executeContractCall(contract, 'getProfile', account, { from: account, }); + + if (hash === '0x0000000000000000000000000000000000000000') { + return false; + } else { + return true; + } + } catch (ex) { + return false; + } +} + + +export { + Aes, + AesEcb, + BaseContract, + BCRuntime, + buffer, + ContractLoader, + CoreRuntime, + create, + createAndSet, + createAndSetBC, + createAndSetCore, + createBC, + createCore, + CryptoProvider, + DataContract, + Description, + DfsInterface, + EventHub, + Executor, + Ipfs, + IpfsRemoteConstructor, + Ipld, + isAccountOnboarded, + KeyExchange, + KeyProvider, + KeyProviderInterface, + keystore, + Logger, + LogLevel, + logLog, + logLogLevel, + Mailbox, + Mnemonic, + NameResolver, + Onboarding, + Profile, + ProfileRuntime, + prottle, + RightsAndRoles, + Sharing, + SignerExternal, + SignerInternal, + Unencrypted, + Web3, +}; diff --git a/src/bundles/bcc/dbcp.json b/src/bundles/bcc/dbcp.json new file mode 100644 index 00000000..54c2b037 --- /dev/null +++ b/src/bundles/bcc/dbcp.json @@ -0,0 +1,31 @@ +{ + "public": { + "author": "contractus", + "dapp": { + "dependencies": { + "smartcontracts": "^1.0.2" + }, + "entrypoint": "bcc.js", + "files": [ + "bcc.js" + ], + "origin": "QmeQP32Xdm5BxMTzH1SuN3qGdnLYNRLhS7jQmyUDqvcUqa", + "type": "library" + }, + "description": "Contractus for loading ens entries and it's data...", + "name": "bcc", + "tags": [ + "dapp", + "contractus", + "library" + ], + "version": "1.0.2", + "versions": { + "0.1.0": "QmTsY7ASbvxATLnEemHZQ8oQha64ujYe9VXbDCouDk7Xtp", + "0.9.0": "Qmbw3yX82TVN9WhsRyeaRpd89M9XXL1pds9GgqJL29ohar", + "1.0.0": "QmcECAYTqmKP5AQwkKaCiov77KhgpNLTawAN3tmBCHQeTi", + "1.0.1": "QmQNVUFewk95FFkuA62E2TVZNV9w3L6ybMK3b82wyJpg25", + "1.0.2": "QmdC3NTNRPCs2VTrEQx1RNL6LoaDzhQLpa4nsAMA61sM7i" + } + } +} \ No newline at end of file diff --git a/src/common/utils.ts b/src/common/utils.ts new file mode 100644 index 00000000..36163765 --- /dev/null +++ b/src/common/utils.ts @@ -0,0 +1,69 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +/** +* run given function from this, use function(error, result) {...} callback for promise resolve/reject +* can be used like: +* api.helpers +* .runFunctionAsPromise(fs, 'readFile', 'somefile.txt') +* .then(content => console.log('file content: ' + content)) +* ; +* +* @param {Object} funThis the functions 'this' object +* @param {string} functionName name of the contract function to call +* @return {Promise} resolves to: {Object} (the result from the function(error, result) {...} callback) +*/ +export async function promisify(funThis, functionName, ...args): Promise { + let functionArguments = args.slice(0); + + return new Promise(function(resolve, reject) { + try { + // add callback function to arguments + functionArguments.push(function(error, result) { + if (error) { + reject(error); + } else { + resolve(result); + } + }); + // run function + funThis[functionName].apply(funThis, functionArguments); + } catch (ex) { + reject(ex.message); + } + }); +}; + +/** + * obfuscates strings by replacing each character but the last two with 'x' + * + * @param {string} text text to obfuscate + * @return {string} obfuscated text + */ +export function obfuscate(text: string): string { + return text ? `${[...Array(text.length - 2)].map(() => 'x').join('')}${text.substr(text.length - 2)}` : text; +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 00000000..8f8ccdf3 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,60 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +const config = { + nameResolver: { + ensAddress: process.env.ENS_ADDRESS || '0x937bbC1d3874961CA38726E9cD07317ba81eD2e1', + ensResolver: process.env.ENS_RESOLVER || '0xDC18774FA2E472D26aB91deCC4CDd20D9E82047e', + labels: { + businessCenterRoot: process.env.BC_ROOT || 'testbc.evan', + ensRoot: process.env.ENS_ROOT || 'evan', + factory: 'factory', + admin: 'admin', + eventhub: 'eventhub', + profile: 'profile', + mailbox: 'mailbox' + }, + domains: { + root: ['ensRoot'], + factory: ['factory', 'businessCenterRoot'], + adminFactory: ['admin', 'factory', 'ensRoot'], + businessCenter: ['businessCenterRoot'], + eventhub: process.env.ENS_EVENTS || ['eventhub', 'ensRoot'], + profile: process.env.ENS_PROFILES || ['profile', 'ensRoot'], + profileFactory: ['profile', 'factory', 'ensRoot'], + mailbox: process.env.ENS_MAILBOX || ['mailbox', 'ensRoot'], + }, + }, + smartAgents: { + onboarding: { + accountId: '0x063fB42cCe4CA5448D69b4418cb89E663E71A139', + }, + }, + alwaysAutoGasLimit: 1.1, +} + +export { config } \ No newline at end of file diff --git a/src/contracts/base-contract/base-contract.spec.ts b/src/contracts/base-contract/base-contract.spec.ts new file mode 100644 index 00000000..8596b1ad --- /dev/null +++ b/src/contracts/base-contract/base-contract.spec.ts @@ -0,0 +1,291 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect, use } from 'chai'; +import chaiAsPromised = require('chai-as-promised'); + +import { + ContractLoader, + Ipfs, + EventHub, + Executor, + NameResolver, +} from '@evan.network/dbcp'; + +import { accounts } from '../../test/accounts'; +import { BaseContract, ConsumerState, ContractState } from './base-contract'; +import { config } from '../../config'; +import { Ipld } from '../../dfs/ipld'; +import { Profile } from '../../profile/profile'; +import { TestUtils } from '../../test/test-utils'; + +use(chaiAsPromised); + + +// test BaseContract functionalities with AssetContract +describe('BaseContract', function() { + this.timeout(60000); + let baseContract: BaseContract; + let contractFactory: any; + let executor: Executor; + let ipfs: Ipfs; + let ipld: Ipld; + let loader: ContractLoader; + let businessCenterDomain; + let eventHub: EventHub; + let nameResolver; + let profile: Profile; + let web3; + + before(async () => { + web3 = TestUtils.getWeb3(); + nameResolver = await TestUtils.getNameResolver(web3); + executor = await TestUtils.getExecutor(web3); + eventHub = await TestUtils.getEventHub(web3); + executor.eventHub = eventHub; + loader = await TestUtils.getContractLoader(web3); + baseContract = new BaseContract({ + executor, + loader, + log: TestUtils.getLogger(), + nameResolver, + }); + businessCenterDomain = nameResolver.getDomainName(config.nameResolver.domains.businessCenter); + const businessCenterAddress = await nameResolver.getAddress(businessCenterDomain); + const businessCenter = await loader.loadContract('BusinessCenter', businessCenterAddress); + ipfs = await TestUtils.getIpfs(); + ipld = await TestUtils.getIpld(ipfs); + ipld.originator = nameResolver.soliditySha3(accounts[1]); + profile = await TestUtils.getProfile(web3, ipfs); + await profile.loadForAccount(); + await profile.setContactKnownState(accounts[0], true); + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[0], { from: accounts[0], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[0], autoGas: 1.1, }); + } + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[1], { from: accounts[1], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[1], autoGas: 1.1, }); + } + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[2], autoGas: 1.1, }); + } + }); + + after(async() => { + // create new ipld handler on ipfs node + await ipfs.stop(); + }); + + it('can be created', async () => { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + businessCenterDomain); + expect(contractId).not.to.be.undefined; + }); + + it('can have new members invited to it by the owner', async () => { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + businessCenterDomain); + const contract = loader.loadContract('BaseContractInterface', contractId); + let isMember = await executor.executeContractCall(contract, 'isConsumer', accounts[1]); + expect(isMember).to.be.false; + await baseContract.inviteToContract( + businessCenterDomain, + contractId, + accounts[0], + accounts[1], + ); + isMember = await executor.executeContractCall(contract, 'isConsumer', accounts[1]); + expect(isMember).to.be.true; + }); + + it('cannot have new members invited to it by members that are not the owner ', async () => { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + businessCenterDomain); + const contract = loader.loadContract('BaseContractInterface', contractId); + await baseContract.inviteToContract( + businessCenterDomain, + contractId, + accounts[0], + accounts[1], + ); + const promise = baseContract.inviteToContract( + businessCenterDomain, + contractId, + accounts[1], + accounts[2], + ); + await expect(promise).to.be.rejected; + }); + + it('cannot have new members invited to it by users that are not in the contract ', async () => { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + businessCenterDomain); + const contract = loader.loadContract('BaseContractInterface', contractId); + let isMember = await executor.executeContractCall(contract, 'isConsumer', accounts[1]); + const promise = baseContract.inviteToContract( + businessCenterDomain, + contractId, + accounts[1], + accounts[2], + ); + await expect(promise).to.be.rejected; + }); + + it.skip('cannot have members invited, when the invitee doesn\'t know / ignores inviter', async () => { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + businessCenterDomain); + const contract = loader.loadContract('BaseContractInterface', contractId); + let isMember = await executor.executeContractCall(contract, 'isConsumer', accounts[1]); + expect(isMember).to.be.false; + await profile.setContactKnownState(accounts[0], false); + const invitePromise = baseContract.inviteToContract( + businessCenterDomain, + contractId, + accounts[0], + accounts[1], + ); + await expect(invitePromise).to.be.rejected; + await profile.setContactKnownState(accounts[0], true); + }); + + it('can have its state set by the owner', async () => { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + businessCenterDomain); + await baseContract.changeContractState(contractId, accounts[0], ContractState.PendingApproval); + }); + + it('cannot have its state set by members that are not the owner', async () => { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + businessCenterDomain); + await baseContract.inviteToContract( + businessCenterDomain, + contractId, + accounts[0], + accounts[1], + ); + const promise = baseContract.changeContractState(contractId, accounts[1], ContractState.PendingApproval); + await expect(promise).to.be.rejected; + }); + + it('can have its state set by users that are not in the contract', async () => { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + businessCenterDomain); + const promise = baseContract.changeContractState(contractId, accounts[1], 1); + await expect(promise).to.be.rejected; + }); + + it('can have the owner set its own state', async () => { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + businessCenterDomain); + const contract = loader.loadContract('BaseContract', contractId) + const cstate = await executor.executeContractCall(contract, 'consumerState', accounts[0]); + await baseContract.changeConsumerState(contractId, accounts[0], accounts[0], ConsumerState.Active); + }); + + describe('when used without a business center', () => { + it('can be created', async () => { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + null); + const contract = loader.loadContract('BaseContract', contractId) + const cstate = await executor.executeContractCall(contract, 'consumerState', accounts[0]); + await baseContract.changeConsumerState(contractId, accounts[0], accounts[0], ConsumerState.Active); + }); + + it('can have new members invited to it by the owner', async () => { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + null); + const contract = loader.loadContract('BaseContractInterface', contractId); + let isMember = await executor.executeContractCall(contract, 'isConsumer', accounts[1]); + expect(isMember).to.be.false; + await baseContract.inviteToContract( + null, + contractId, + accounts[0], + accounts[1], + ); + isMember = await executor.executeContractCall(contract, 'isConsumer', accounts[1]); + expect(isMember).to.be.true; + }); + + it('triggers contract events from on its own instead of letting the bc do this', async () => { + return new Promise(async (resolve, reject) => { + try { + const contractId = await baseContract.createUninitialized( + 'testdatacontract', + accounts[0], + null); + const contract = loader.loadContract('BaseContractInterface', contractId); + // reject on timeout + let resolved; + setTimeout(() => { + if (!resolved) { + reject('timeout during waiting for ContractEvent'); + } + }, 10000); + // if event is triggered, resolve test + eventHub.once('EventHub', null, 'ContractEvent', + (event) => { + const { sender, eventType } = event.returnValues; + return sender === contractId && eventType === '0'; + }, + (event) => { resolved = true; resolve(); } + ); + await baseContract.inviteToContract( + null, + contractId, + accounts[0], + accounts[1], + ); + } catch (ex) { + reject(ex); + } + }); + }); + }); +}); diff --git a/src/contracts/base-contract/base-contract.ts b/src/contracts/base-contract/base-contract.ts new file mode 100644 index 00000000..71b381d1 --- /dev/null +++ b/src/contracts/base-contract/base-contract.ts @@ -0,0 +1,214 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import { + ContractLoader, + Executor, + Logger, + LoggerOptions, + NameResolver +} from '@evan.network/dbcp'; + + +/** + * describes contracts overall state + */ +export enum ContractState { + Initial, + Error, + Draft, + PendingApproval, + Approved, + Active, + VerifyTerminated, + Terminated, +}; + +/** + * describes the state of a consumer or owner in a contract + */ +export enum ConsumerState { + Initial, + Error, + Draft, + Rejected, + Active, + Terminated +}; + +/** + * options for BaseContract constructor + */ +export interface BaseContractOptions extends LoggerOptions { + executor: Executor, + loader: ContractLoader, + nameResolver: NameResolver, +} + +/** + * wrapper for BaseContract interactions + * + * @class BaseContract (name) + */ +export class BaseContract extends Logger { + protected options: BaseContractOptions; + + constructor(optionsInput: BaseContractOptions) { + super(optionsInput); + this.options = optionsInput; + } + + /** + * create new contract but do not initialize it yet + * + * @param {string} factoryName contract factory name, used for ENS lookup + * @param {string} accountId Ethereum account id + * @param {string} businessCenterDomain business center in which the contract will + * be created; use null when working without + * business center + * @param {string} descriptionDfsHash bytes32 hash for description in dfs + * @return {Promise} Ethereum id of new contract + */ + public async createUninitialized( + factoryName: string, + accountId: string, + businessCenterDomain?: string, + descriptionDfsHash = '0x0000000000000000000000000000000000000000000000000000000000000000') + : Promise { + let factoryDomain; + if (factoryName.includes('.')) { + // full ens domain name + factoryDomain = factoryName; + } else { + // partial name, bc relative domain + factoryDomain = this.options.nameResolver.getDomainName( + this.options.nameResolver.config.domains.factory, factoryName); + } + const factoryAddress = await this.options.nameResolver.getAddress(factoryDomain); + if (!factoryAddress) { + throw new Error(`factory "${factoryName}" not found in "${this.options.nameResolver.config.labels.businessCenterRoot}"`); + } + const factory = this.options.loader.loadContract('BaseContractFactoryInterface', factoryAddress); + let businessCenterAddress; + if (businessCenterDomain) { + businessCenterAddress = await this.options.nameResolver.getAddress(businessCenterDomain); + } else { + businessCenterAddress = '0x0000000000000000000000000000000000000000'; + } + const contractId = await this.options.executor.executeContractTransaction( + factory, + 'createContract', { + from: accountId, + autoGas: 1.1, + event: { target: 'BaseContractFactoryInterface', eventName: 'ContractCreated', }, + getEventResult: (event, args) => args.newAddress, + }, + businessCenterAddress, + accountId, + descriptionDfsHash, + this.options.nameResolver.config.ensAddress, + ); + return contractId.toString(); + } + + /** + * invite user to contract + * + * @param {string} businessCenterDomain ENS domain name of the business center the + * contract was created in; use null when + * working without business center + * @param {string} contract Ethereum id of the contract + * @param {string} inviterId account id of inviting user + * @param {string} inviteeId account id of invited user + * @return {Promise} resolved when done + */ + public async inviteToContract( + businessCenterDomain: string, + contract: string, + inviterId: string, + inviteeId: string): Promise { + const baseContractInterface = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('BaseContractInterface', contract); + let businessCenterAddress; + if (businessCenterDomain) { + businessCenterAddress = await this.options.nameResolver.getAddress(businessCenterDomain); + } else { + businessCenterAddress = '0x0000000000000000000000000000000000000000'; + } + await this.options.executor.executeContractTransaction( + baseContractInterface, + 'inviteConsumer', + { from: inviterId, autoGas: 1.1, }, + inviteeId, + businessCenterAddress + ); + } + + /** + * set state of the contract + * + * @param {string|any} contract contract instance or contract id + * @param {string} accountId Ethereum account id + * @param {ContractState} state new state + * @return {Promise} resolved when done + */ + public async changeContractState(contract: string|any, accountId: string, state: ContractState): Promise { + const baseContractInterface = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('BaseContractInterface', contract); + await this.options.executor.executeContractTransaction( + baseContractInterface, + 'changeContractState', + { from: accountId, autoGas: 1.1, }, + state, + ); + } + + /** + * set state of a consumer + * + * @param {string|any} contract contract instance or contract id + * @param {string} accountId Ethereum account id + * @param {string} consumerId Ethereum account id + * @param {ConsumerState} state new state + * @return {Promise} resolved when done + */ + public async changeConsumerState( + contract: string|any, + accountId: string, + consumerId: string, + state: ConsumerState): Promise { + const baseContractInterface = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('BaseContractInterface', contract); + await this.options.executor.executeContractTransaction( + baseContractInterface, + 'changeConsumerState', + { from: accountId, autoGas: 1.1, }, + consumerId, + state, + ); + } +} diff --git a/src/contracts/business-center/business-center.spec.ts b/src/contracts/business-center/business-center.spec.ts new file mode 100644 index 00000000..987898ac --- /dev/null +++ b/src/contracts/business-center/business-center.spec.ts @@ -0,0 +1,340 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect, use } from 'chai'; +import chaiAsPromised = require('chai-as-promised'); + +import { + Executor, + NameResolver, +} from '@evan.network/dbcp'; + +import { accounts } from '../../test/accounts'; +import { config } from '../../config'; +import { TestUtils } from '../../test/test-utils'; + +use(chaiAsPromised); + + +describe('Business Center', function() { + this.timeout(60000); + let businessCenter; + let executor: Executor; + const empty = '0x0000000000000000000000000000000000000000000000000000000000000000'; + let loader; + let ensDomain; + let nameResolver: NameResolver; + let web3; + + before(() => { + web3 = TestUtils.getWeb3(); + }); + + after(() => { + web3.currentProvider.connection.close(); + }); + + /** + * create new busines center + * + * @param {number} joinSchema --> JoinBehavior { SelfJoin, AddOnly, Handshake } + * @return {Promise} contract instance + */ + async function createBusinessCenter(joinSchema: number): Promise { + const adminFactoryEnsDomain = nameResolver.getDomainName(config.nameResolver.domains.adminFactory); + const adminFactoryContractAddress = await nameResolver.getAddress(adminFactoryEnsDomain); + const adminFactory = await loader.loadContract('BusinessCenterFactory', adminFactoryContractAddress); + const address = await executor.executeContractTransaction( + adminFactory, + 'createContract', + { + from: accounts[0], + gas: 5000000, + event: { target: 'BusinessCenterFactory', eventName: 'ContractCreated', }, + getEventResult: (event, args) => args.newAddress, + }, + nameResolver.namehash(ensDomain), + config.nameResolver.ensAddress + ); + const bc = loader.loadContract('BusinessCenterInterface', address); + const storageAddress = '0x0000000000000000000000000000000000000000'; + await executor.executeContractTransaction( + bc, + 'init', + { from: accounts[0], autoGas: 1.1, }, + storageAddress, + joinSchema, + ); + await executor.executeContractTransaction(bc, 'join', { from: accounts[0], autoGas: 1.1, }); + return bc; + } + + before(async () => { + executor = await TestUtils.getExecutor(web3); + executor.eventHub = await TestUtils.getEventHub(web3); + loader = await TestUtils.getContractLoader(web3); + nameResolver = await TestUtils.getNameResolver(web3); + ensDomain = nameResolver.getDomainName(config.nameResolver.domains.businessCenter); + const bcAddress = await nameResolver.getAddress(ensDomain); + businessCenter = loader.loadContract('BusinessCenter', bcAddress); + let isMember = await executor.executeContractCall( + businessCenter, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + if (!isMember) { + await executor.executeContractTransaction( + businessCenter, 'join', { from: accounts[2], autoGas: 1.1, }); + } + }); + + describe('when working with a SelfJoin Business Center', async () => { + it('allows to join', async () => { + const selfJoinBc = await createBusinessCenter(0); + await executor.executeContractTransaction( + selfJoinBc, 'join', { from: accounts[2], autoGas: 1.1, }); + let isMember = await executor.executeContractCall( + selfJoinBc, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + expect(isMember).to.be.true; + }); + + it('rejects an invite of a member', async () => { + const selfJoinBc = await createBusinessCenter(0); + const invitePromise = executor.executeContractTransaction( + selfJoinBc, 'invite', accounts[2], { from: accounts[0], autoGas: 1.1, }); + expect(invitePromise).to.be.rejected; + }); + }); + + describe('when working with a InviteOnly Business Center', async () => { + it('rejects a join', async () => { + const inviteOnlyBc = await createBusinessCenter(1); + const joinPromise = executor.executeContractTransaction( + inviteOnlyBc, 'join', { from: accounts[2], autoGas: 1.1, }); + expect(joinPromise).to.be.rejected; + }); + + it('adds a member, when a member is invited', async () => { + const inviteOnlyBc = await createBusinessCenter(1); + await executor.executeContractTransaction( + inviteOnlyBc, 'invite', { from: accounts[0], autoGas: 1.1, }, accounts[2], ); + let isMember = await executor.executeContractCall(inviteOnlyBc, 'isMember', accounts[2]); + expect(isMember).to.be.true; + }); + }); + + describe('when working with a Handshake Business Center', async () => { + it('allows a join request, but does not add a member with only this', async () => { + const handshakeBc = await createBusinessCenter(2); + await executor.executeContractTransaction( + handshakeBc, 'join', { from: accounts[2], autoGas: 1.1, }); + }); + + it('allows sending invitations, but does not add a member with only this', async () => { + const handshakeBc = await createBusinessCenter(2); + await executor.executeContractTransaction( + handshakeBc, 'invite', { from: accounts[0], autoGas: 1.1, }, accounts[2]); + }); + + it('adds a member when invite and join have been called', async () => { + let isMember; + const handshakeBc = await createBusinessCenter(2); + await executor.executeContractTransaction( + handshakeBc, 'join', { from: accounts[2], autoGas: 1.1, }); + isMember = await executor.executeContractCall( + handshakeBc, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + expect(isMember).to.be.false; + await executor.executeContractTransaction( + handshakeBc, 'invite', { from: accounts[0], autoGas: 1.1, }, accounts[2]); + isMember = await executor.executeContractCall( + handshakeBc, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + expect(isMember).to.be.true; + }); + + it('adds a member when join and invite have been called (other order than last test', async () => { + let isMember; + const handshakeBc = await createBusinessCenter(2); + await executor.executeContractTransaction( + handshakeBc, 'invite', { from: accounts[0], autoGas: 1.1, }, accounts[2]); + isMember = await executor.executeContractCall( + handshakeBc, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + expect(isMember).to.be.false; + await executor.executeContractTransaction( + handshakeBc, 'join', { from: accounts[2], autoGas: 1.1, }); + isMember = await executor.executeContractCall( + handshakeBc, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + expect(isMember).to.be.true; + }); + }); + + describe('when changing the join schema', async () => { + let currentBc; + before(async () => { + currentBc = await createBusinessCenter(0); + }); + describe('when working with a SelfJoin Business Center', async () => { + it('allows to join', async () => { + await executor.executeContractTransaction( + currentBc, 'join', { from: accounts[2], autoGas: 1.1, }); + let isMember = await executor.executeContractCall( + currentBc, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + expect(isMember).to.be.true; + }); + + it('rejects an invite of a member', async () => { + const invitePromise = executor.executeContractTransaction( + currentBc, 'invite', accounts[1], { from: accounts[0], autoGas: 1.1, }); + expect(invitePromise).to.be.rejected; + }); + + it('allows to change the join Schema', async () => { + await executor.executeContractTransaction( + currentBc, 'setJoinSchema', { from: accounts[0], autoGas: 1.1, }, 1); + }); + + it('does not allow to change the join schema', async () => { + const setJoinSchemaPromise = executor.executeContractTransaction( + currentBc, 'setJoinSchema', { from: accounts[2], autoGas: 1.1, }, 1); + expect(setJoinSchemaPromise).to.be.rejected; + }); + + it('allows to leave the business center', async () => { + await executor.executeContractTransaction( + currentBc, 'cancel', { from: accounts[2], autoGas: 1.1, }); + const isMember = await executor.executeContractCall( + currentBc, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + expect(isMember).to.be.false; + }) + }); + + describe('when working with a InviteOnly Business Center', async () => { + it('rejects a join', async () => { + const joinPromise = executor.executeContractTransaction( + currentBc, 'join', { from: accounts[2], autoGas: 1.1, }); + expect(joinPromise).to.be.rejected; + }); + + it('adds a member, when a member is invited', async () => { + await executor.executeContractTransaction( + currentBc, 'invite', { from: accounts[0], autoGas: 1.1, }, accounts[1], ); + let isMember = await executor.executeContractCall(currentBc, 'isMember', accounts[1]); + expect(isMember).to.be.true; + }); + }); + }); + + it('allows to to cancel membership', async () => { + let isMember = await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + if (!isMember) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[2], autoGas: 1.1, }); + } + await executor.executeContractTransaction(businessCenter, 'cancel', { from: accounts[2], autoGas: 1.1, }); + isMember = await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + expect(isMember).to.be.false; + }); + + it('does not allow to cancel a membership if not joined', async () => { + let isMember = await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + if (isMember) { + await executor.executeContractTransaction(businessCenter, 'cancel', { from: accounts[2], autoGas: 1.1, }); + } + const promise = executor.executeContractTransaction(businessCenter, 'cancel', { from: accounts[2], autoGas: 1.1, }); + await expect(promise).to.be.rejected; + }); + + it('does not allow sending fake contract events', async () => { + let isMember = await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + if (isMember) { + await executor.executeContractTransaction(businessCenter, 'cancel', { from: accounts[2], autoGas: 1.1, }); + } + const promise = executor.executeContractTransaction( + businessCenter, + 'sendContractEvent', + { from: accounts[2], autoGas: 1.1, }, + 1, + empty, + accounts[2], + ); + await expect(promise).to.be.rejected; + }); + + it('allows members to set their own profile', async () => { + const sampleProfile = '0x1234000000000000000000000000000000000000000000000000000000000000'; + let isMember = await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + if (!isMember) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[2], autoGas: 1.1, }); + } + let profile = await executor.executeContractCall(businessCenter, 'getProfile', accounts[2]); + if (profile !== empty) { + await executor.executeContractTransaction(businessCenter, 'setMyProfile', { from: accounts[2], autoGas: 1.1, }, empty); + } + await executor.executeContractTransaction(businessCenter, 'setMyProfile', { from: accounts[2], autoGas: 1.1, }, sampleProfile); + profile = await executor.executeContractCall(businessCenter, 'getProfile', accounts[2]); + expect(profile).to.eq(sampleProfile); + }); + + it('removes a user profile when this user leaves', async () => { + const sampleProfile = '0x1234000000000000000000000000000000000000000000000000000000000000'; + let isMember = await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + if (!isMember) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[2], autoGas: 1.1, }); + } + await executor.executeContractTransaction(businessCenter, 'setMyProfile', { from: accounts[2], autoGas: 1.1, }, sampleProfile); + let profile = await executor.executeContractCall(businessCenter, 'getProfile', accounts[2]); + expect(profile).to.eq(sampleProfile); + await executor.executeContractTransaction(businessCenter, 'cancel', { from: accounts[2], autoGas: 1.1, }); + profile = await executor.executeContractCall(businessCenter, 'getProfile', accounts[2]); + expect(profile).to.eq(empty); + }); + + it('does not allow setting a profile when executing user is not a member ', async () => { + const sampleProfile = '0x1234000000000000000000000000000000000000000000000000000000000000'; + let isMember = await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + if (isMember) { + await executor.executeContractTransaction(businessCenter, 'cancel', { from: accounts[2], autoGas: 1.1, }); + } + const promise = executor.executeContractTransaction(businessCenter, 'setMyProfile', { from: accounts[2], autoGas: 1.1, }, sampleProfile); + await expect(promise).to.be.rejected; + }); + + it('allows members to update own profile', async () => { + const sampleProfile1 = '0x1234000000000000000000000000000000000000000000000000000000000000'; + const sampleProfile2 = '0x1234500000000000000000000000000000000000000000000000000000000000'; + let isMember = await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], gas: 3000000, }); + if (!isMember) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[2], autoGas: 1.1, }); + } + let profile = await executor.executeContractCall(businessCenter, 'getProfile', accounts[2]); + if (profile !== empty) { + await executor.executeContractTransaction(businessCenter, 'setMyProfile', { from: accounts[2], autoGas: 1.1, }, empty); + } + await executor.executeContractTransaction(businessCenter, 'setMyProfile', { from: accounts[2], autoGas: 1.1, }, sampleProfile1); + profile = await executor.executeContractCall(businessCenter, 'getProfile', accounts[2]); + expect(profile).to.eq(sampleProfile1); + await executor.executeContractTransaction(businessCenter, 'setMyProfile', { from: accounts[2], autoGas: 1.1, }, sampleProfile2); + profile = await executor.executeContractCall(businessCenter, 'getProfile', accounts[2]); + expect(profile).to.eq(sampleProfile2); + }); +}); diff --git a/src/contracts/data-contract/data-contract.spec.ts b/src/contracts/data-contract/data-contract.spec.ts new file mode 100644 index 00000000..1dcc4340 --- /dev/null +++ b/src/contracts/data-contract/data-contract.spec.ts @@ -0,0 +1,557 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect, use } from 'chai'; +import chaiAsPromised = require('chai-as-promised'); + +import { + ContractLoader, + Description, + DfsInterface, + Envelope, + EventHub, + Executor, + NameResolver, +} from '@evan.network/dbcp'; + +import { accounts } from '../../test/accounts'; +import { ConsumerState, ContractState } from '../base-contract/base-contract'; +import { config } from '../../config'; +import { CryptoProvider } from '../../encryption/crypto-provider'; +import { DataContract } from './data-contract'; +import { Sharing } from '../../contracts/sharing'; +import { TestUtils } from '../../test/test-utils'; + +use(chaiAsPromised); + + +describe('DataContract', function() { + this.timeout(60000); + let dc: DataContract; + let contractFactory: any; + let executor: Executor; + let loader: ContractLoader; + let businessCenterDomain; + let sharing: Sharing; + let dfs: DfsInterface; + let web3; + let cryptoProvider: CryptoProvider; + let nameResolver: NameResolver; + + const sampleValues = [ + '0x0000000000000000000000000000000000000000000000000000000000001234', + '0x0000000000000000000000000000000000000000000000000000000000011234', + '0x0000000000000000000000000000000000000000000000000000000000021234', + '0x0000000000000000000000000000000000000000000000000000000000031234', + '0x0000000000000000000000000000000000000000000000000000000000041234', + '0x0000000000000000000000000000000000000000000000000000000000051234', + ]; + /* tslint:disable:quotemark */ + const sampleDescription: Envelope = { + "public": { + "name": "Data Contract Sample", + "description": "reiterance oxynitrate sat alternize acurative", + "version": "0.1.0", + "author": "contractus", + } + }; + /* tslint:enable:quotemark */ + + before(async () => { + web3 = TestUtils.getWeb3(); + nameResolver = await TestUtils.getNameResolver(web3); + executor = await TestUtils.getExecutor(web3); + executor.eventHub = await TestUtils.getEventHub(web3); + loader = await TestUtils.getContractLoader(web3); + dfs = await TestUtils.getIpfs(); + sharing = await TestUtils.getSharing(web3, dfs); + cryptoProvider = TestUtils.getCryptoProvider(); + sampleDescription.cryptoInfo = cryptoProvider.getCryptorByCryptoAlgo('aes').getCryptoInfo(nameResolver.soliditySha3(accounts[0])); + dc = new DataContract({ + cryptoProvider, + dfs, + executor, + loader, + log: TestUtils.getLogger(), + nameResolver, + sharing, + web3: TestUtils.getWeb3(), + description: await TestUtils.getDescription(web3, dfs), + }); + businessCenterDomain = nameResolver.getDomainName(config.nameResolver.domains.businessCenter); + + const businessCenterAddress = await nameResolver.getAddress(businessCenterDomain); + const businessCenter = await loader.loadContract('BusinessCenter', businessCenterAddress); + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[0], { from: accounts[0], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[0], autoGas: 1.1, }); + } + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[1], { from: accounts[1], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[1], autoGas: 1.1, }); + } + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[2], autoGas: 1.1, }); + } + }); + + after(async () => { + await dfs.stop(); + }); + + async function createContract(addSharing = false, schema?) { + let description; + if (schema) { + description = JSON.parse(JSON.stringify(sampleDescription)); + description.public.dataSchema = schema; + description.cryptoInfo = cryptoProvider.getCryptorByCryptoAlgo('aes').getCryptoInfo(nameResolver.soliditySha3(accounts[0])); + } + const contract = await dc.create('testdatacontract', accounts[0], businessCenterDomain, description); + await dc.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[1]); + if (addSharing) { + const blockNr = await web3.eth.getBlockNumber(); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', blockNr); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', blockNr, contentKey); + } + return contract; + } + + async function runTestSubset(useClassicRawMode) { + const [ storeInDfs, encryptHashes ] = [...Array(2)].map(() => !useClassicRawMode); + describe('when working with entries', async () => { + describe('that can only be set by the owner', async () => { + it('allows the owner to add and get entries', async () => { + const contract = await createContract(storeInDfs); + await dc.setEntry(contract, 'entry_settable_by_owner', sampleValues[0], accounts[0], storeInDfs, encryptHashes); + const retrieved = await dc.getEntry(contract, 'entry_settable_by_owner', accounts[0], storeInDfs, encryptHashes); + expect(retrieved).to.eq(sampleValues[0]); + }); + it('does not allow the member to add and get entries', async () => { + const contract = await createContract(storeInDfs); + const promise = dc.setEntry(contract, 'entry_settable_by_owner', sampleValues[0], accounts[1], storeInDfs, encryptHashes); + await expect(promise).to.be.rejected; + }); + it('can retrieve a schema from its description', async () => { + const contract = await createContract(storeInDfs); + await dc.setEntry(contract, 'entry_settable_by_owner', sampleValues[0], accounts[0], storeInDfs, encryptHashes); + }); + it('can use different crypto algorithms for encryption', async () => { + const contract = await createContract(storeInDfs); + await dc.setEntry(contract, 'entry_settable_by_owner', sampleValues[0], accounts[0], storeInDfs, encryptHashes); + const retrievedDefaultAlgo = await dc.getEntry(contract, 'entry_settable_by_owner', accounts[0], storeInDfs, encryptHashes); + const encryptedHash = await executor.executeContractCall(contract, 'getEntry', nameResolver.sha3('entry_settable_by_owner')); + if (!storeInDfs) { + expect(encryptedHash).to.eq(sampleValues[0]); + } else { + const unencryptedHash = await dc.decryptHash(encryptedHash, contract, accounts[0]); + const envelope = JSON.parse((await dfs.get(unencryptedHash)).toString()); + expect(envelope.cryptoInfo.algorithm).to.eq('aes-256-cbc'); + const data = Buffer.from(envelope.private, 'hex').toString('utf-8'); + expect(data).to.not.eq(sampleValues[0]); + } + + await dc.setEntry(contract, 'entry_settable_by_owner', sampleValues[0], accounts[0], storeInDfs, encryptHashes, 'aes'); + const retrievedAes = await dc.getEntry(contract, 'entry_settable_by_owner', accounts[0], storeInDfs, encryptHashes); + const hashAes = await executor.executeContractCall(contract, 'getEntry', nameResolver.sha3('entry_settable_by_owner')); + if (!storeInDfs) { + expect(hashAes).to.eq(sampleValues[0]); + } else { + const unencryptedHash = await dc.decryptHash(hashAes, contract, accounts[0]); + const envelope = JSON.parse((await dfs.get(unencryptedHash)).toString()); + expect(envelope.cryptoInfo.algorithm).to.eq('aes-256-cbc'); + const data = Buffer.from(envelope.private, 'hex').toString('utf-8'); + expect(data).to.not.eq(sampleValues[0]); + } + + await dc.setEntry(contract, 'entry_settable_by_owner', sampleValues[0], accounts[0], storeInDfs, encryptHashes, 'unencrypted'); + const retrievedUnencrypted = await dc.getEntry(contract, 'entry_settable_by_owner', accounts[0], storeInDfs, encryptHashes); + const hashRaw = await executor.executeContractCall(contract, 'getEntry', nameResolver.sha3('entry_settable_by_owner')); + if (!storeInDfs) { + expect(hashRaw).to.eq(sampleValues[0]); + } else { + const unencryptedHash = await dc.decryptHash(hashRaw, contract, accounts[0]); + const envelope = JSON.parse((await dfs.get(unencryptedHash)).toString()); + expect(envelope.cryptoInfo.algorithm).to.eq('unencrypted'); + const data = JSON.parse(Buffer.from(envelope.private, 'hex').toString('utf-8')); + expect(data).to.eq(sampleValues[0]); + } + }); + }); + describe('that can be set by owner and member', async () => { + it('allows the owner to add and get entries', async () => { + const contract = await createContract(storeInDfs); + await dc.setEntry(contract, 'entry_settable_by_member', sampleValues[0], accounts[0], storeInDfs, encryptHashes); + const retrieved = await dc.getEntry(contract, 'entry_settable_by_member', accounts[0], storeInDfs, encryptHashes); + expect(retrieved).to.eq(sampleValues[0]); + }); + it('allows the member to add and get entries', async () => { + const contract = await createContract(storeInDfs); + await dc.setEntry(contract, 'entry_settable_by_member', sampleValues[0], accounts[1], storeInDfs, encryptHashes); + const retrieved = await dc.getEntry(contract, 'entry_settable_by_member', accounts[1], storeInDfs, encryptHashes); + expect(retrieved).to.eq(sampleValues[0]); + }); + }); + }); + describe('when working with lists', async () => { + describe('that allows only the owner to add items', async () => { + it('allows the owner to add and get entries', async () => { + const contract = await createContract(storeInDfs); + await dc.addListEntries(contract, 'list_settable_by_owner', sampleValues, accounts[0], storeInDfs, encryptHashes); + const retrieved = await dc.getListEntries(contract, 'list_settable_by_owner', accounts[0], storeInDfs, encryptHashes); + for (let i = 0; i < sampleValues.length; i++) { + expect(retrieved[i]).to.eq(sampleValues[i]); + } + }); + it('does not allow the member to add entries', async () => { + const contract = await createContract(storeInDfs); + const promise = dc.addListEntries(contract, 'list_settable_by_owner', sampleValues, accounts[1], storeInDfs, encryptHashes); + await expect(promise).to.be.rejected; + }); + it('allows to retrieve the items with an item limit', async () => { + const contract = await createContract(storeInDfs); + await dc.addListEntries(contract, 'list_settable_by_owner', sampleValues, accounts[0], storeInDfs, encryptHashes); + const retrieved = await dc.getListEntries(contract, 'list_settable_by_owner', accounts[0], storeInDfs, encryptHashes, 2); + expect(retrieved.length).to.eq(2); + for (let i = 0; i < 2; i++) { + expect(retrieved[i]).to.eq(sampleValues[i]); + } + }); + it('allows to retrieve the items with an offset', async () => { + const contract = await createContract(storeInDfs); + await dc.addListEntries(contract, 'list_settable_by_owner', sampleValues, accounts[0], storeInDfs, encryptHashes); + const retrieved = await dc.getListEntries(contract, 'list_settable_by_owner', accounts[0], storeInDfs, encryptHashes, 10, 2); + expect(retrieved.length).to.eq(sampleValues.length - 2); + for (let i = 0; i < retrieved.length; i++) { + expect(retrieved[i]).to.eq(sampleValues[i + 2]); + } + }); + it('allows to retrieve the items in reverse order', async () => { + const contract = await createContract(storeInDfs); + await dc.addListEntries(contract, 'list_settable_by_owner', sampleValues, accounts[0], storeInDfs, encryptHashes); + const retrieved = await dc.getListEntries(contract, 'list_settable_by_owner', accounts[0], storeInDfs, encryptHashes, 10, 0, true); + const reverseSamples = sampleValues.reverse(); + for (let i = 0; i < retrieved.length; i++) { + expect(retrieved[i]).to.eq(reverseSamples[i]); + } + }); + it('allows to retrieve the items with a combination of paging arguments', async () => { + const contract = await createContract(storeInDfs); + await dc.addListEntries(contract, 'list_settable_by_owner', sampleValues, accounts[0], storeInDfs, encryptHashes); + const retrieved = await dc.getListEntries(contract, 'list_settable_by_owner', accounts[0], storeInDfs, encryptHashes, 2, 1, true); + const reverseSamples = sampleValues.reverse(); + for (let i = 0; i < 2; i++) { + expect(retrieved[i]).to.eq(reverseSamples[i + 1]); + } + }); + }); + describe('that allows owner and member to add items', async () => { + it('allows the owner to add and get entries', async () => { + const contract = await createContract(storeInDfs); + await dc.addListEntries(contract, 'list_settable_by_member', sampleValues, accounts[0], storeInDfs, encryptHashes); + const retrieved = await dc.getListEntries(contract, 'list_settable_by_member', accounts[0], storeInDfs, encryptHashes); + for (let i = 0; i < sampleValues.length; i++) { + expect(retrieved[i]).to.eq(sampleValues[i]); + } + }); + it('does allow the member to add entries', async () => { + const contract = await createContract(storeInDfs); + await dc.addListEntries(contract, 'list_settable_by_member', sampleValues, accounts[1], storeInDfs, encryptHashes); + const retrieved = await dc.getListEntries(contract, 'list_settable_by_member', accounts[1], storeInDfs, encryptHashes); + for (let i = 0; i < sampleValues.length; i++) { + expect(retrieved[i]).to.eq(sampleValues[i]); + } + }); + }); + describe('allows the owner to remove list entries', async () => { + it('allows the owner to remove list entries', async () => { + const contract = await createContract(storeInDfs); + await dc.addListEntries(contract, 'list_removable_by_owner', sampleValues, accounts[0], storeInDfs, encryptHashes); + let retrieved = await dc.getListEntries(contract, 'list_removable_by_owner', accounts[0], storeInDfs, encryptHashes); + for (let i = 0; i < sampleValues.length; i++) { + expect(retrieved[i]).to.eq(sampleValues[i]); + } + await dc.removeListEntry(contract, 'list_removable_by_owner', 2, accounts[0]); + retrieved = await dc.getListEntries(contract, 'list_removable_by_owner', accounts[0], storeInDfs, encryptHashes); + for (let i = 0; i < (sampleValues.length - 1) ; i++) { + expect(retrieved[i]).to.eq(sampleValues[i]); + } + }); + it('does not allow the member to remove list entries', async () => { + const contract = await createContract(storeInDfs); + await dc.addListEntries(contract, 'list_removable_by_owner', sampleValues, accounts[0], storeInDfs, encryptHashes); + let retrieved = await dc.getListEntries(contract, 'list_removable_by_owner', accounts[0], storeInDfs, encryptHashes); + for (let i = 0; i < sampleValues.length; i++) { + expect(retrieved[i]).to.eq(sampleValues[i]); + } + const promise = dc.removeListEntry(contract, 'list_removable_by_owner', 2, accounts[1]); + await expect(promise).to.be.rejected; + }); + }); + describe('when working with descriptions', async () => { + /* tslint:disable:quotemark */ + const testSchema = { + list_settable_by_member: { + "$id": "list_settable_by_member_schema", + "type": "object", + "additionalProperties": false, + "properties": { + "foo": { "type": "string" }, + "bar": { "type": "integer" } + } + }, + entry_settable_by_member: { + "$id": "entry_settable_by_member_schema", + "type": "integer", + } + }; + /* tslint:enable:quotemark */ + it('allows adding entries matching the field schema', async () => { + const contract = await createContract(!storeInDfs, testSchema); + const values = [ !storeInDfs ? sampleValues[0] : { + foo: 'sample', + bar: 123, + }]; + const promise = dc.addListEntries(contract, 'list_settable_by_member', values, accounts[0], storeInDfs, encryptHashes); + await expect(promise).to.be.fulfilled; + }); + it('forbids adding entries not matching the field schema', async () => { + const contract = await createContract(!storeInDfs, testSchema); + const values = [ !storeInDfs ? sampleValues[0] : { + foo: 'sample', + bar: 'totally not a number', + barz: 123, + }]; + const promise = dc.addListEntries(contract, 'list_settable_by_member', values, accounts[0], storeInDfs, encryptHashes); + if (!storeInDfs) { + await expect(promise).to.be.fulfilled; + } else { + await expect(promise).to.be.rejected; + } + }); + it('forbids adding entries not matching their type', async () => { + const contract = await createContract(!storeInDfs, testSchema); + const values = [ !storeInDfs ? sampleValues[0] : { + foo: 'sample', + bar: '123', + }]; + const promise = dc.addListEntries(contract, 'list_settable_by_member', values, accounts[0], storeInDfs, encryptHashes); + if (!storeInDfs) { + await expect(promise).to.be.fulfilled; + } else { + await expect(promise).to.be.rejected; + } + }); + it('forbids adding entries with more properties than defined', async () => { + const contract = await createContract(!storeInDfs, testSchema); + const values = [ !storeInDfs ? sampleValues[0] : { + foo: 'sample', + bar: 123, + barz: 123, + }]; + const promise = dc.addListEntries(contract, 'list_settable_by_member', values, accounts[0], storeInDfs, encryptHashes); + if (!storeInDfs) { + await expect(promise).to.be.fulfilled; + } else { + await expect(promise).to.be.rejected; + } + }); + it('fallbacks to accept any if no schema was found', async () => { + const contract = await createContract(storeInDfs); + const values = [ !storeInDfs ? sampleValues[0] : { + foo: 'sample', + barz: 123, + }]; + const promise = dc.addListEntries(contract, 'list_settable_by_member', values, accounts[0], storeInDfs, encryptHashes); + await expect(promise).to.be.fulfilled; + }); + it('allows setting entries matching the field schema', async () => { + const contract = await createContract(!storeInDfs, testSchema); + const value = !storeInDfs ? sampleValues[0] : 123; + const promise = dc.setEntry(contract, 'entry_settable_by_member', value, accounts[0], storeInDfs, encryptHashes); + await expect(promise).to.be.fulfilled; + }); + it('forbids setting entries not matching the field schema', async () => { + const contract = await createContract(!storeInDfs, testSchema); + const value = !storeInDfs ? sampleValues[0] : 'totally not an integer'; + const promise = dc.setEntry(contract, 'entry_settable_by_member', value, accounts[0], storeInDfs, encryptHashes); + if (!storeInDfs) { + await expect(promise).to.be.fulfilled; + } else { + await expect(promise).to.be.rejected; + } + }); + }); + describe('when working with multiple lists at a time', async () => { + it('allows to add entries to multiple lists', async () => { + const contract = await createContract(storeInDfs); + await dc.addListEntries(contract, ['list_settable_by_owner', 'list_settable_by_member'], sampleValues, accounts[0], storeInDfs, encryptHashes); + let retrieved = await dc.getListEntries(contract, 'list_settable_by_owner', accounts[0], storeInDfs, encryptHashes); + for (let i = 0; i < sampleValues.length; i++) { + expect(retrieved[i]).to.eq(sampleValues[i]); + } + retrieved = await dc.getListEntries(contract, 'list_settable_by_member', accounts[0], storeInDfs, encryptHashes); + for (let i = 0; i < sampleValues.length; i++) { + expect(retrieved[i]).to.eq(sampleValues[i]); + } + }); + it('does not allow the member to add entries in all lists if not permitted to access one of them', async () => { + const contract = await createContract(storeInDfs); + const promise = dc.addListEntries(contract, ['list_settable_by_owner', 'list_settable_by_member'], sampleValues, accounts[1], storeInDfs, encryptHashes); + await expect(promise).to.be.rejected; + }); + it('allows to move an entry from one list to another', async () => { + const contract = await createContract(storeInDfs); + await dc.addListEntries(contract, ['list_removable_by_owner'], sampleValues, accounts[0], storeInDfs, encryptHashes); + expect(await dc.getListEntryCount(contract, 'list_removable_by_owner')).to.eq(3); + expect(await dc.getListEntryCount(contract, 'list_settable_by_member')).to.eq(0); + expect(await dc.getListEntry(contract, 'list_removable_by_owner', 1, accounts[0], storeInDfs, encryptHashes)).to.eq(sampleValues[1]); + // move item + await dc.moveListEntry(contract, 'list_removable_by_owner', 1, ['list_settable_by_member'], accounts[0]); + expect(await dc.getListEntryCount(contract, 'list_removable_by_owner')).to.eq(2); + expect(await dc.getListEntryCount(contract, 'list_settable_by_member')).to.eq(1); + // former last elements should have been moved to removed position + expect(await dc.getListEntry(contract, 'list_removable_by_owner', 1, accounts[0], storeInDfs, encryptHashes)).to.eq(sampleValues[2]); + // new entry should have been added + expect(await dc.getListEntry(contract, 'list_settable_by_member', 0, accounts[0], storeInDfs, encryptHashes)).to.eq(sampleValues[1]); + }); + it('allows to move an entry from one list to multiple lists', async () => {}); + }); + }); + describe('when working with mappings', async() => { + // key types are basically irrelevant, will be hashed anyway + const sampleMappingKeys = [...accounts]; + describe('that can only be set by the owner', async () => { + it('allows the owner to add and get entries', async () => { + const contract = await createContract(storeInDfs); + await dc.setMappingValue(contract, 'mapping_settable_by_owner', sampleMappingKeys[0], sampleValues[0], accounts[0], storeInDfs, encryptHashes); + await dc.setMappingValue(contract, 'mapping_settable_by_owner', sampleMappingKeys[1], sampleValues[1], accounts[0], storeInDfs, encryptHashes); + await dc.setMappingValue(contract, 'mapping_settable_by_owner', sampleMappingKeys[2], sampleValues[2], accounts[0], storeInDfs, encryptHashes); + expect(await dc.getMappingValue(contract, 'mapping_settable_by_owner', sampleMappingKeys[0], accounts[0], storeInDfs, encryptHashes)).to.eq(sampleValues[0]); + expect(await dc.getMappingValue(contract, 'mapping_settable_by_owner', sampleMappingKeys[1], accounts[0], storeInDfs, encryptHashes)).to.eq(sampleValues[1]); + expect(await dc.getMappingValue(contract, 'mapping_settable_by_owner', sampleMappingKeys[2], accounts[0], storeInDfs, encryptHashes)).to.eq(sampleValues[2]); + }); + it('does not allow the member to add and get entries', async () => { + const contract = await createContract(storeInDfs); + const promise = dc.setMappingValue(contract, 'mapping_settable_by_owner', sampleMappingKeys[0], sampleValues[0], accounts[1], storeInDfs, encryptHashes); + await expect(promise).to.be.rejected; + }); + }); + describe('that can be set by owner and member', async () => { + it('allows the owner to add and get entries', async () => { + const contract = await createContract(storeInDfs); + await dc.setMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[0], sampleValues[0], accounts[0], storeInDfs, encryptHashes); + await dc.setMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[1], sampleValues[1], accounts[0], storeInDfs, encryptHashes); + await dc.setMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[2], sampleValues[2], accounts[0], storeInDfs, encryptHashes); + expect(await dc.getMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[0], accounts[0], storeInDfs, encryptHashes)).to.eq(sampleValues[0]); + expect(await dc.getMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[1], accounts[0], storeInDfs, encryptHashes)).to.eq(sampleValues[1]); + expect(await dc.getMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[2], accounts[0], storeInDfs, encryptHashes)).to.eq(sampleValues[2]); + }); + it('allows the member to add and get entries', async () => { + const contract = await createContract(storeInDfs); + await dc.setMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[0], sampleValues[0], accounts[1], storeInDfs, encryptHashes); + await dc.setMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[1], sampleValues[1], accounts[1], storeInDfs, encryptHashes); + await dc.setMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[2], sampleValues[2], accounts[1], storeInDfs, encryptHashes); + expect(await dc.getMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[0], accounts[1], storeInDfs, encryptHashes)).to.eq(sampleValues[0]); + expect(await dc.getMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[1], accounts[1], storeInDfs, encryptHashes)).to.eq(sampleValues[1]); + expect(await dc.getMappingValue(contract, 'mapping_settable_by_member', sampleMappingKeys[2], accounts[1], storeInDfs, encryptHashes)).to.eq(sampleValues[2]); + }); + }); + }); + } + + it('can be created', async () => { + const contract = await createContract(); + expect(contract).to.be.ok; + }); + describe('when working encrypted DFS files', async () => { + runTestSubset(false); + it('allows the item creator to decrypt values', async () => { + const contract = await createContract(true); + await dc.setEntry(contract, 'entry_settable_by_owner', sampleValues[0], accounts[0]); + const retrieved = await dc.getEntry(contract, 'entry_settable_by_owner', accounts[0]); + expect(retrieved).to.eq(sampleValues[0]); + }); + it('allows an invited user to decrypt values', async () => { + const contract = await createContract(true); + await dc.setEntry(contract, 'entry_settable_by_owner', sampleValues[0], accounts[0]); + const retrieved = await dc.getEntry(contract, 'entry_settable_by_owner', accounts[1]); + expect(retrieved).to.eq(sampleValues[0]); + }); + it('does not allow an uninvited user to decrypt values', async () => { + const contract = await createContract(true); + await dc.setEntry(contract, 'entry_settable_by_owner', sampleValues[0], accounts[0]); + const promise = dc.getEntry(contract, 'entry_settable_by_owner', accounts[2]); + await expect(promise).to.be.rejected; + }); + }); + describe('when working with raw values', async () => { + runTestSubset(true); + }); + describe('when changing the contract state', async () => { + it('allows to change the state with a configured transition', async () => { + const contract = await createContract(true); + // contract is created and then set to Draft during creation logic, + // and updating from Draf to to PendingApproval is allowed + await dc.changeContractState(contract, accounts[0], ContractState.PendingApproval); + }); + it('does not allow to change the state with not a configured transition', async () => { + const contract = await createContract(true); + const promise = dc.changeContractState(contract, accounts[0], ContractState.Approved); + await expect(promise).to.be.rejected; + }); + it('does not allow to change the state with a user without contract state update permission', async () => { + const contract = await createContract(true); + const promise = dc.changeContractState(contract, accounts[1], ContractState.PendingApproval); + await expect(promise).to.be.rejected; + }); + }); + describe('when changing own member state', async () => { + it('allows to change the member state with a configured transition', async () => { + const contract = await createContract(true); + // owners current state is 'Draft', so going to 'Active' is allowed + await dc.changeConsumerState(contract, accounts[0], accounts[0], ConsumerState.Active); + }); + it('does not allow to change the member state with not a configured transition', async () => { + const contract = await createContract(true); + // owners current state is 'Draft', so going to 'Terminated' is not allowed + const promise = dc.changeConsumerState(contract, accounts[0], accounts[0], ConsumerState.Terminated); + await expect(promise).to.be.rejected; + }); + }); + describe('when changing other members states', async () => { + it('allows to change the member state with a configured transition', async () => { + const contract = await createContract(true); + // members current state is 'Draft', owner can set its state to 'Terminated' + await dc.changeConsumerState(contract, accounts[0], accounts[1], ConsumerState.Terminated); + }); + it('does not allow to change the member state with not a configured transition', async () => { + const contract = await createContract(true); + // members current state is 'Draft', owner can set its state 'Active' + const promise = dc.changeConsumerState(contract, accounts[0], accounts[1], ConsumerState.Active); + await expect(promise).to.be.rejected; + }); + }); +}); diff --git a/src/contracts/data-contract/data-contract.ts b/src/contracts/data-contract/data-contract.ts new file mode 100644 index 00000000..0ff154a6 --- /dev/null +++ b/src/contracts/data-contract/data-contract.ts @@ -0,0 +1,736 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import crypto = require('crypto'); +import prottle = require('prottle'); + +import { + Description, + DfsInterface, + Envelope, + Logger, + Validator, +} from '@evan.network/dbcp'; + +import { BaseContract, BaseContractOptions } from '../base-contract/base-contract'; +import { CryptoProvider } from '../../encryption/crypto-provider'; +import { Sharing } from '../sharing'; + + +const requestWindowSize = 10; + + +/** + * options for DataContract constructor + */ +export interface DataContractOptions extends BaseContractOptions { + cryptoProvider: CryptoProvider, + dfs: DfsInterface, + sharing: Sharing, + web3: any, + defaultCryptoAlgo?: string, + description: Description, +} + +/** + * helper class for AssetDataContracts + * + * @class AssetDataContract (name) + */ +export class DataContract extends BaseContract { + protected options: DataContractOptions; + private readonly encodingUnencrypted = 'utf-8'; + private readonly encodingEncrypted = 'hex'; + private readonly encodingUnencryptedHash = 'hex'; + private readonly cryptoAlgorithHashes = 'aesEcb'; + + constructor(optionsInput: DataContractOptions) { + super(optionsInput as BaseContractOptions); + this.options = optionsInput; + if (!this.options.defaultCryptoAlgo) { + this.options.defaultCryptoAlgo = 'aes'; + } + } + + /** + * create and initialize new contract + * + * @param {string} factoryName factory to use for creating contract (without + * the business center suffix) + * @param {string} accountId owner of the new contract and transaction + * executor + * @param {string} businessCenterDomain ENS domain name of the business center + * @param {string|any} contractDescription bytes32 hash of DBCP description or a schema + * object + * @param {bool} allowConsumerInvite true if consumers are allowed to invite other + * consumers + * @param {string} sharingsHash sharings hash to use + * @return {Promise} contract instance + */ + public async create( + factoryName: string, + accountId: string, + businessCenterDomain?: string, + contractDescription = '0x0000000000000000000000000000000000000000000000000000000000000000', + allowConsumerInvite = true, + sharingsHash = null, + ): Promise { + const contractP = (async () => { + const descriptionHash = (typeof contractDescription === 'object') ? + '0x0000000000000000000000000000000000000000000000000000000000000000' : contractDescription; + const contractId = await super.createUninitialized( + factoryName, accountId, businessCenterDomain, descriptionHash); + const contractInterface = this.options.loader.loadContract('DataContractInterface', contractId); + const rootDomain = this.options.nameResolver.namehash( + this.options.nameResolver.getDomainName(this.options.nameResolver.config.domains.root)); + await this.options.executor.executeContractTransaction( + contractInterface, + 'init', + { from: accountId, autoGas: 1.1, }, + rootDomain, + allowConsumerInvite, + ); + return contractInterface; + })(); + const [contract, sharingInfo] = await Promise.all([contractP, sharingsHash ? { sharingsHash, } : this.createSharing(accountId)]); + await this.options.executor.executeContractTransaction( + contract, 'setSharing', { from: accountId, autoGas: 1.1, }, sharingInfo.sharingsHash); + if (typeof contractDescription === 'object') { + await this.options.description.setDescriptionToContract(contract.options.address, contractDescription, accountId); + } + return contract; + } + + /** + * create initial sharing for contract + * + * @param {string} accountId owner of the new contract + * @return {Promise} sharing info with { contentKey, hashKey, sharings, sharingsHash, } + */ + public async createSharing(accountId): Promise { + // create sharing key for owner + const cryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.options.defaultCryptoAlgo); + const hashCryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.cryptoAlgorithHashes); + const [contentKey, hashKey, blockNr] = await Promise.all( + [cryptor.generateKey(), hashCryptor.generateKey(), this.options.web3.eth.getBlockNumber()]); + + const sharings = {}; + await this.options.sharing.extendSharings(sharings, accountId, accountId, '*', blockNr, contentKey); + await this.options.sharing.extendSharings(sharings, accountId, accountId, '*', 'hashKey', hashKey); + + const sharingsHash = await this.options.dfs.add('sharing', Buffer.from(JSON.stringify(sharings), this.encodingUnencrypted)); + + return { + contentKey, + hashKey, + sharings, + sharingsHash, + }; + } + + /** + * add list entries to lists + * + * @param {object|string} contract contract or contractId + * @param {string} listName name of the list in the data contract + * @param {any} values values to add + * @param {string} accountId Ethereum account id + * @param {boolean} dfsStorage store values in dfs + * @param {boolean} encryptedHashes encrypt hashes from values + * @param {string} encryption encryption algorithm to use + * @return {Promise} resolved when done + */ + public async addListEntries( + contract: any|string, + listName: string|string[], + values: any[], + accountId: string, + dfsStorage = true, + encryptedHashes = true, + encryption: string = this.options.defaultCryptoAlgo): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + const listNames = Array.isArray(listName) ? listName : [listName]; + + let hashes = values; + if (!dfsStorage) { + if (encryptedHashes) { + hashes = await Promise.all(hashes.map(hash => this.encryptHash(hash, dataContract, accountId))); + } + // store as is + console.dir(hashes); + await this.options.executor.executeContractTransaction( + dataContract, + 'addListEntries', + { from: accountId, autoGas: 1.1, }, + listNames.map(name => this.options.web3.utils.sha3(name)), + hashes, + ); + } else { + // upload to ipfs + const [ description, blockNr ] = await Promise.all([ + this.options.description.getDescriptionFromContract(dataContract.options.address, accountId), + this.options.web3.eth.getBlockNumber(), + ]); + await Promise.all((listNames).map(name => this.validate(description, name, hashes))); + // get all keys and check if they differ + const keys = await Promise.all(listNames.map(name => this.options.sharing.getKey(dataContract.options.address, accountId, name, blockNr))); + const groupedKeys = {}; + keys.forEach((key, index) => { + if (groupedKeys[key]) { + groupedKeys[key].push(listNames[index]); + } else { + groupedKeys[key] = [ listNames[index] ]; + } + }); + // push grouped by key + for (let key of Object.keys(groupedKeys)) { + const ipfsFiles = []; + for (let value of hashes) { + const encrypted = await this.encrypt({private: value}, dataContract, accountId, groupedKeys[key][0], blockNr, encryption); + const stateMd5 = crypto.createHash('md5').update(encrypted).digest('hex'); + ipfsFiles.push({ + path: stateMd5, + content: Buffer.from(encrypted), + }); + }; + hashes = await this.options.dfs.addMultiple(ipfsFiles); + if (encryptedHashes) { + hashes = await Promise.all(hashes.map(hash => this.encryptHash(hash, dataContract, accountId))); + } + await this.options.executor.executeContractTransaction( + dataContract, + 'addListEntries', + { from: accountId, autoGas: 1.1, }, + groupedKeys[key].map(name => this.options.web3.utils.sha3(name)), + hashes, + ); + } + } + } + + /** + * decrypt input envelope return decrypted envelope + * + * @param {string} toDecrypt data to decrypt + * @param {any} contract contract instance or contract id + * @param {string} accountId account id that decrypts the data + * @param {string} propertyName property in contract that is decrypted + * @return {Promise} decrypted envelope + */ + public async decrypt(toDecrypt: string, contract: any, accountId: string, propertyName: string): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + // decode envelope + const envelope: Envelope = JSON.parse(toDecrypt); + if (envelope.cryptoInfo) { + const cryptor = this.options.cryptoProvider.getCryptorByCryptoInfo(envelope.cryptoInfo); + const contentKey = await this.options.sharing.getKey(dataContract.options.address, accountId, propertyName, envelope.cryptoInfo.block); + if (!contentKey) { + throw new Error(`no content key found for contract "${dataContract.options.address}" and account "${accountId}"`); + } + const decryptedBuffer = await cryptor.decrypt( + Buffer.from(envelope.private, this.encodingEncrypted), { key: contentKey, }); + envelope.private = decryptedBuffer; + } + return envelope; + } + + /** + * decrypt input hash, return decrypted hash + * + * @param {string} toDecrypt hash to decrypt + * @param {any} contract contract instance or contract id + * @param {string} accountId account id that decrypts the data + * @return {Promise} decrypted hash + */ + public async decryptHash(toDecrypt: string, contract: any, accountId: string): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + // decode hash + const cryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.cryptoAlgorithHashes); + const hashKey = await this.options.sharing.getHashKey(dataContract.options.address, accountId); + + if (!hashKey) { + throw new Error(`no hashKey key found for contract "${dataContract.options.address}" and account "${accountId}"`); + } + const decryptedBuffer = await cryptor.decrypt( + Buffer.from(toDecrypt.substr(2), this.encodingEncrypted), { key: hashKey, }); + return `0x${decryptedBuffer.toString(this.encodingUnencryptedHash)}`; + } + + /** + * encrypt incoming envelope + * + * @param {Envelope} toEncrypt envelope with data to encrypt + * @param {any} contract contract instance or contract id + * @param {string} accountId encrypting account + * @param {string} propertyName property in contract, the data is encrypted for + * @param {number} block block the data belongs to + * @param {string} encryption encryption name + * @return {Promise} encrypted envelope or hash as string + */ + public async encrypt( + toEncrypt: Envelope, + contract: any, + accountId: string, + propertyName: string, + block: number, + encryption: string = this.options.defaultCryptoAlgo): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + + // get content key from contract + const contentKey = await this.options.sharing.getKey(dataContract.options.address, accountId, propertyName, block); + + if (!contentKey) { + throw new Error(`no content key found for contract "${dataContract.options.address}" and account "${accountId}"`); + } + // encrypt with content key + const cryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(encryption); + const encryptedBuffer = await cryptor.encrypt(toEncrypt.private, { key: contentKey, }); + const encrypted = encryptedBuffer.toString(this.encodingEncrypted); + const envelope: Envelope = { + private: encrypted, + cryptoInfo: cryptor.getCryptoInfo(this.options.nameResolver.soliditySha3(dataContract.options.address)), + }; + envelope.cryptoInfo.block = block; + if (toEncrypt.public) { + envelope.public = toEncrypt.public; + } + return JSON.stringify(envelope); + } + + /** + * encrypt incoming hash + * + * @param {string} toEncrypt hash to encrypt + * @param {any} contract contract to encrypt data for + * @param {string} accountId encrypting account + * @return {Promise} encrypted hash as string + */ + public async encryptHash(toEncrypt: string, contract: any, accountId: string): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + + // get hash key from contract + const hashKey = await this.options.sharing.getHashKey(dataContract.options.address, accountId); + + if (!hashKey) { + throw new Error(`no hashKey found for contract "${dataContract.options.address}" and account "${accountId}"`); + } + // encrypt with hashKkey + const cryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.cryptoAlgorithHashes); + const encryptedBuffer = await cryptor.encrypt(Buffer.from(toEncrypt.substr(2), this.encodingUnencryptedHash), { key: hashKey, }); + return `0x${encryptedBuffer.toString(this.encodingEncrypted)}`; + } + + /** + * return entry from contract + * + * @param {object|string} contract contract or contractId + * @param {string} entryName entry name + * @param {string} accountId Ethereum account id + * @param {boolean} dfsStorage store values in dfs + * @param {boolean} encryptedHashes decrypt hashes from values + * @return {Promise} list entries + */ + public async getEntry( + contract: any|string, + entryName: string, + accountId: string, + dfsStorage = true, + encryptedHashes = true): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + const entryRaw = await this.options.executor.executeContractCall( + dataContract, + 'getEntry', + this.options.web3.utils.sha3(entryName), + ); + // if no entry / empty entry was returned, skip further processing + if (entryRaw === '0x0000000000000000000000000000000000000000000000000000000000000000') { + return entryRaw; + } + let hash = entryRaw; + if (encryptedHashes) { + hash = await this.decryptHash(entryRaw, dataContract, accountId); + } + if (!dfsStorage) { + return hash; + } else { + let hash = entryRaw; + if (encryptedHashes) { + hash = await this.decryptHash(entryRaw, dataContract, accountId); + } + const encryptedContent = (await this.options.dfs.get(hash)).toString('utf-8'); + const decrypted = await this.decrypt( + encryptedContent, + dataContract, + accountId, + entryName + ); + return decrypted.private; + } + } + + /** + * return a value from a mapping + * + * @param {object|string} contract contract or contractId + * @param {string} mappingName name of a data contracts mapping property + * @param {string} entryName entry name + * @param {string} accountId Ethereum account id + * @param {boolean} dfsStorage store values in dfs + * @param {boolean} encryptedHashes encrypt hashes from values + * @return {Promise} mappings value for given key + */ + public async getMappingValue( + contract: any|string, + mappingName: string, + entryName: string, + accountId: string, + dfsStorage = true, + encryptedHashes = true): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + const entryRaw = await this.options.executor.executeContractCall( + dataContract, + 'getMappingValue', + this.options.web3.utils.sha3(mappingName), + this.options.web3.utils.sha3(entryName), + ); + let hash = entryRaw; + if (encryptedHashes) { + hash = await this.decryptHash(entryRaw, dataContract, accountId); + } + if (!dfsStorage) { + return entryRaw; + } else { + const encryptedContent = (await this.options.dfs.get(hash)).toString('utf-8'); + const decrypted = await this.decrypt( + encryptedContent, + dataContract, + accountId, + mappingName + ); + return decrypted.private; + } + } + + /** + * return list entries from contract + * + * @param {object|string} contract contract or contractId + * @param {string} listName name of the list in the data contract + * @param {string} accountId Ethereum account id + * @param {boolean} dfsStorage store values in dfs + * @param {boolean} encryptedHashes encrypt hashes from values + * @param {number} count number of elements to retrieve (page size) + * @param {number} offset skip this many elements when retrieving + * @param {boolean} reverse reverse order of entries + * @return {Promise} list entries + */ + public async getListEntries( + contract: any|string, + listName: string, + accountId: string, + dfsStorage = true, + encryptedHashes = true, + count = 10, + offset = 0, + reverse = false): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + const listKey = this.options.web3.utils.sha3(listName); + + const elements = await this.options.nameResolver.getArrayFromUintMapping( + dataContract, + () => this.options.executor.executeContractCall(dataContract, 'getListEntryCount', listKey), + (i) => this.options.executor.executeContractCall(dataContract, 'getListEntry', listKey, i), + count, + offset, + reverse, + ); + if (!elements.length) { + // skip processing if no results returned + return elements; + } + let hashes = elements; + if (encryptedHashes) { + hashes = await Promise.all(elements.map(element => this.decryptHash(element, dataContract, accountId))); + } + if (!dfsStorage) { + return hashes; + } else { + const envelopes = await prottle(requestWindowSize, hashes.map((hash) => async () => { + const decrypted = await this.decrypt( + (await this.options.dfs.get(hash)).toString('utf-8'), + dataContract, + accountId, + listName + ); + return decrypted; + })); + return envelopes.map(envelope => envelope.private); + } + } + + /** + * return a single list entry from contract + * + * @param {object|string} contract contract or contractId + * @param {string} listName name of the list in the data contract + * @param {number} index list entry id to retrieve + * @param {string} accountId Ethereum account id + * @param {boolean} dfsStorage store values in dfs + * @param {boolean} encryptedHashes encrypt hashes from values + * @return {Promise} list entry + */ + public async getListEntry( + contract: any|string, + listName: string, + index: number, + accountId: string, + dfsStorage = true, + encryptedHashes = true): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + const listKey = this.options.web3.utils.sha3(listName); + const entryRaw = await this.options.executor.executeContractCall(dataContract, 'getListEntry', listKey, index); + let hash = entryRaw; + if (encryptedHashes) { + hash = await this.decryptHash(entryRaw, dataContract, accountId); + } + if (!dfsStorage) { + return hash; + } else { + const decrypted = await this.decrypt( + (await this.options.dfs.get(hash)).toString('utf-8'), + dataContract, + accountId, + listName + ); + return decrypted.private; + } + } + + /** + * return number of entries in the list + * + * @param {object|string} contract contract or contractId + * @param {string} listName name of the list in the data contract + * @return {Promise} list entry count + */ + public async getListEntryCount( + contract: any|string, + listName: string): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + const listKey = this.options.web3.utils.sha3(listName); + return parseInt(await this.options.executor.executeContractCall(dataContract, 'getListEntryCount', listKey), 10); + } + + /** + * move one list entry to one or more lists + * + * @param {object|string} contract contract or contractId + * @param {string} listNameFrom origin list + * @param {number} entryIndex index of the entry to move in the origin list + * @param {string[]} listNamesTo lists to move data into + * @param {string} accountId Ethereum account id + * @return {Promise} resolved when done + */ + public async moveListEntry( + contract: any|string, + listNameFrom: string, + entryIndex: number, + listNamesTo: string[], + accountId: string): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + await this.options.executor.executeContractTransaction( + dataContract, + 'moveListEntry', + { from: accountId, gas: 2000000, }, + this.options.web3.utils.sha3(listNameFrom), + entryIndex, + listNamesTo.map(name => this.options.web3.utils.sha3(name)), + ); + } + + /** + * remove list entry from list; will reposition last list entry into emptied slot + * + * @param {object|string} contract contract or contractId + * @param {string} listName name of the list in the data contract + * @param {number} entryIndex index of list entry + * @param {string} accountId Ethereum account id + * @return {Promise} resolved when done + */ + public async removeListEntry( + contract: any|string, + listName: string, + entryIndex: number, + accountId: string): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + await this.options.executor.executeContractTransaction( + dataContract, + 'removeListEntry', + { from: accountId, gas: 2000000, }, + this.options.web3.utils.sha3(listName), + entryIndex, + ); + } + + /** + * set entry for a key + * + * @param {object|string} contract contract or contractId + * @param {string} entryName entry name + * @param {any} value value to add + * @param {string} accountId Ethereum account id + * @param {boolean} dfsStorage store values in dfs + * @param {boolean} encryptedHashes encrypt hashes from values + * @param {string} encryption encryption algorithm to use + * @return {Promise} resolved when done + */ + public async setEntry( + contract: any|string, + entryName: string, + value: any, + accountId: string, + dfsStorage = true, + encryptedHashes = true, + encryption: string = this.options.defaultCryptoAlgo): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + let toSet; + + if (!dfsStorage) { + // store as is + toSet = value; + } else { + const [ description, blockNr ] = await Promise.all([ + this.options.description.getDescriptionFromContract(dataContract.options.address, accountId), + this.options.web3.eth.getBlockNumber(), + ]); + await this.validate(description, entryName, value); + const encrypted = await this.encrypt({ private: value }, dataContract, accountId, entryName, blockNr, encryption); + const stateMd5 = crypto.createHash('md5').update(encrypted).digest('hex'); + toSet = await this.options.dfs.add(stateMd5, Buffer.from(encrypted)); + } + if (encryptedHashes) { + toSet = await this.encryptHash(toSet, dataContract, accountId); + } + await this.options.executor.executeContractTransaction( + dataContract, + 'setEntry', + { from: accountId, autoGas: 1.1, }, + this.options.web3.utils.sha3(entryName), + toSet, + ); + } + + /** + * set entry for a key in a mapping + * + * @param {object|string} contract contract or contractId + * @param {string} mappingName name of a data contracts mapping property + * @param {string} entryName entry name (property in the mapping) + * @param {any} value value to add + * @param {string} accountId Ethereum account id + * @param {boolean} dfsStorage store values in dfs + * @param {boolean} encryptedHashes encrypt hashes from values + * @param {string} encryption encryption algorith (key provider property) + * @return {Promise} resolved when done + */ + public async setMappingValue( + contract: any|string, + mappingName: string, + entryName: string, + value: any, + accountId: string, + dfsStorage = true, + encryptedHashes = true, + encryption: string = this.options.defaultCryptoAlgo): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + let toSet; + + if (!dfsStorage) { + // store as is + toSet = value; + } else { + const [ description, blockNr ] = await Promise.all([ + this.options.description.getDescriptionFromContract(contract.options.address, accountId), + this.options.web3.eth.getBlockNumber(), + ]); + await this.validate(description, mappingName, value); + const encrypted = await this.encrypt({ private: value }, dataContract, accountId, mappingName, blockNr, encryption); + const stateMd5 = crypto.createHash('md5').update(encrypted).digest('hex'); + toSet = await this.options.dfs.add(stateMd5, Buffer.from(encrypted)); + } + if (encryptedHashes) { + toSet = await this.encryptHash(toSet, dataContract, accountId); + } + await this.options.executor.executeContractTransaction( + dataContract, + 'setMappingValue', + { from: accountId, autoGas: 1.1, }, + this.options.web3.utils.sha3(mappingName), + this.options.web3.utils.sha3(entryName), + toSet, + ); + } + + private async validate(description: any, fieldName: string, toCheck: any[]) { + // get merged description + if (!description) { + return true; + } + const merged = {...description.public, ...description.private}; + if (merged.dataSchema && merged.dataSchema[fieldName]) { + // check values if description found + const validator = new Validator({ schema: merged.dataSchema[fieldName] }); + let values; + if (Array.isArray(toCheck)) { + values = toCheck; + } else { + values = [toCheck]; + } + const checkFails = values + .map(value => validator.validate(value)) + .filter(result => result !== true) + ; + if (checkFails.length) { + throw new Error(`validation of input values failed with: ${JSON.stringify(checkFails)}`); + } + } + } +} diff --git a/src/contracts/rights-and-roles.spec.ts b/src/contracts/rights-and-roles.spec.ts new file mode 100644 index 00000000..9fb6c27b --- /dev/null +++ b/src/contracts/rights-and-roles.spec.ts @@ -0,0 +1,159 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect, use } from 'chai'; +import chaiAsPromised = require('chai-as-promised'); + +import { + ContractLoader, + Executor, + NameResolver, +} from '@evan.network/dbcp'; + +import { accounts } from '../test/accounts'; +import { config } from '../config'; +import { DataContract } from './data-contract/data-contract'; +import { RightsAndRoles, ModificationType, PropertyType } from './rights-and-roles'; +import { ServiceContract } from './service-contract/service-contract'; +import { TestUtils } from '../test/test-utils' + +use(chaiAsPromised); + + +describe('Rights and Roles handler', function() { + this.timeout(300000); + let sc: ServiceContract; + let executor; + let rar: RightsAndRoles; + let businessCenterDomain; + let businessCenter; + let ipfs; + let web3; + let dc: DataContract; + let sharing; + + before(async () => { + ipfs = await TestUtils.getIpfs(); + web3 = TestUtils.getWeb3(); + executor = await TestUtils.getExecutor(web3); + rar = await TestUtils.getRightsAndRoles(web3); + sc = await TestUtils.getServiceContract(web3, ipfs); + const loader = await TestUtils.getContractLoader(web3); + const nameResolver = await TestUtils.getNameResolver(web3); + businessCenterDomain = nameResolver.getDomainName(config.nameResolver.domains.businessCenter); + const businessCenterAddress = await nameResolver.getAddress(businessCenterDomain); + businessCenter = await loader.loadContract('BusinessCenter', businessCenterAddress); + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[0], { from: accounts[0], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[0], autoGas: 1.1, }); + } + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[1], { from: accounts[1], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[1], autoGas: 1.1, }); + } + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[2], autoGas: 1.1, }); + } + dc = await TestUtils.getDataContract(web3, ipfs); + sharing = await TestUtils.getSharing(web3, ipfs); + }); + + after(async () => { + await ipfs.stop(); + web3.currentProvider.connection.close(); + // wait 5s before continuing + await new Promise((resolve) => { setTimeout(() => { resolve(); }, 5000); }); + }); + + it('should be able to retrieve all members', async () => { + const contract = await sc.create(accounts[0], businessCenterDomain, ''); + await sc.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[1]); + await sc.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[2]); + const contractParticipants = await rar.getMembers(contract); + const members = contractParticipants[1]; + expect(members.length).to.eq(3); + expect(members[0]).to.eq(accounts[0]); + expect(members[1]).to.eq(accounts[1]); + expect(members[2]).to.eq(accounts[2]); + }); + + it('should be able to retrieve members from a business center', async () => { + const bcMembers = await rar.getMembers(businessCenter); + expect(Object.keys(bcMembers).length).to.eq(5); + }); + + describe('when updating permissions', async() => { + const samples = [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000002', + '0x0000000000000000000000000000000000000000000000000000000000000003', + ]; + const memberRole = 1; + async function createSampleContract(): Promise { + const blockNr = await web3.eth.getBlockNumber(); + // create sample contract, invite second user, add sharing for this user + const contract = await dc.create('testdatacontract', accounts[0], businessCenterDomain); + await dc.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[1]); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', blockNr); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', blockNr, contentKey); + return contract; + } + + it('should be able to grant operation permissions to an existing contract', async () => { + const contract = await createSampleContract(); + await expect(dc.addListEntries(contract, 'new_entry', [samples[0]], accounts[1])).to.be.rejected; + await rar.setOperationPermission(contract, accounts[0], memberRole, 'new_entry', PropertyType.ListEntry, ModificationType.Set, true); + await expect(dc.addListEntries(contract, 'new_entry', [samples[0]], accounts[1])).to.be.fulfilled; + }); + + it('should be able to revoke operation permissions to an existing contract', async () => { + const contract = await createSampleContract(); + await expect(dc.addListEntries(contract, 'list_settable_by_member', [samples[0]], accounts[1])).to.be.fulfilled; + await rar.setOperationPermission(contract, accounts[0], memberRole, 'list_settable_by_member', PropertyType.ListEntry, ModificationType.Set, false); + await expect(dc.addListEntries(contract, 'list_settable_by_member', [samples[0]], accounts[1])).to.be.rejected; + }); + + it('should be able to grant function permissions to an existing contract', async () => { + const contract = await createSampleContract(); + await dc.addListEntries(contract, 'list_settable_by_member', [samples[0]], accounts[1]); + await dc.addListEntries(contract, 'list_settable_by_member', [samples[1]], accounts[1]); + await dc.addListEntries(contract, 'list_settable_by_member', [samples[2]], accounts[1]); + await expect(dc.removeListEntry(contract, 'list_settable_by_member', 2, accounts[1])).to.be.rejected; + + await rar.setFunctionPermission(contract, accounts[0], memberRole, 'removeListEntry(bytes32,uint256)', true) + await rar.setOperationPermission(contract, accounts[0], memberRole, 'list_settable_by_member', PropertyType.ListEntry, ModificationType.Remove, true); + await expect(dc.removeListEntry(contract, 'list_settable_by_member', 2, accounts[1])).to.be.fulfilled; + }); + + it('should be able to revoke function permissions to an existing contract', async () => { + const contract = await createSampleContract(); + await expect(dc.addListEntries(contract, 'list_settable_by_member', [samples[0]], accounts[1])).to.be.fulfilled; + + await rar.setFunctionPermission(contract, accounts[0], memberRole, 'addListEntries(bytes32[],bytes32[])', false); + await expect(dc.addListEntries(contract, 'list_settable_by_member', [samples[1]], accounts[1])).to.be.rejected; + }); + }); +}); diff --git a/src/contracts/rights-and-roles.ts b/src/contracts/rights-and-roles.ts new file mode 100644 index 00000000..68200a15 --- /dev/null +++ b/src/contracts/rights-and-roles.ts @@ -0,0 +1,246 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import prottle = require('prottle'); + +import { + ContractLoader, + Executor, + NameResolver, + Logger, + LoggerOptions, +} from '@evan.network/dbcp'; + + +const simultaneousRolesProcessed = 2; + +/** + * type of modification in contraction + */ +export enum ModificationType { + Set = '0xd2f67e6aeaad1ab7487a680eb9d3363a597afa7a3de33fa9bf3ae6edcb88435d', // web3.sha3('set') + Remove = '0x8dd27a19ebb249760a6490a8d33442a54b5c3c8504068964b74388bfe83458be', // web3.sha3('remove') +} + +/** + * property to set in contract + */ +export enum PropertyType { + Entry = '0x84f3db82fb6cd291ed32c6f64f7f5eda656bda516d17c6bc146631a1f05a1833', // web3.sha3('entry') + ListEntry = '0x7da2a80303fd8a8b312bb0f3403e22702ece25aa85a5e213371a770a74a50106', // web3.sha3('listentry') +} + +export interface RightsAndRolesOptions extends LoggerOptions { + contractLoader: ContractLoader; + executor: Executor; + nameResolver: NameResolver; + web3: any; +} + +/** + * Rights and Roles helper for managing contract permissions + * + * @class RightsAndRoles (name) + */ +export class RightsAndRoles extends Logger { + options: RightsAndRolesOptions; + + constructor(options: RightsAndRolesOptions) { + super(options); + this.options = Object.assign({}, options); + } + + /** + * returns all roles with all members + * + * @param {any|string} contract contractId or contract instance + * @return {Promise} Object with mapping roleId -> [accountId, accountId,...] + */ + public async getMembers(contract: any|string): Promise { + const result = {}; + const contractInstance = (typeof contract === 'object') ? + contract : this.options.contractLoader.loadContract('BaseContractInterface', contract); + const rolesAddress = await this.options.executor.executeContractCall(contractInstance, 'authority'); + const rolesContract = this.options.contractLoader.loadContract('DSRolesPerContract', rolesAddress); + const roleCount = await this.options.executor.executeContractCall(rolesContract, 'roleCount'); + // array of functions that retrieve an element as a promise + const retrievals = [...Array(parseInt(roleCount, 10))].map( + (_, role) => role).reverse().map(role => async () => { + result[role] = await this.options.nameResolver.getArrayFromUintMapping( + rolesContract, + () => this.options.executor.executeContractCall(rolesContract, 'role2userCount', role), + (i) => this.options.executor.executeContractCall(rolesContract, 'role2index2user', role, i + 1), + ); + }); + // run these function windowed, chain .then()s, return result array + await prottle(simultaneousRolesProcessed, retrievals); + return result; + } + + /** + * allows or denies contract function for the accountId + * + * @param {string|any} contract contractId or contract instance + * @param {string} accountId executing accountId + * @param {number} role roleid + * @param {string} functionSignature 4 Bytes function signature + * @param {boolean} allow allow or deny function + * @return {Promise} resolved when done + */ + public async setFunctionPermission( + contract: string|any, accountId: string, role: number, functionSignature: string, allow: boolean) { + const contractId = typeof contract === 'string' ? contract : contract.options.address; + const auth = this.options.contractLoader.loadContract('DSAuth', contractId); + const dsRolesAddress = await this.options.executor.executeContractCall(auth, 'authority'); + const dsRolesContract = this.options.contractLoader.loadContract('DSRolesPerContract', dsRolesAddress); + const keccak256 = this.options.web3.utils.soliditySha3; + const bytes4 = input => input.substr(0, 10); + const permissionHash = bytes4(keccak256(functionSignature)); + await this.options.executor.executeContractTransaction( + dsRolesContract, 'setRoleCapability', { from: accountId, autoGas: 1.1, }, + role, 0, permissionHash, allow); + } + + /** + * allows or denies setting properties on a contract + * + * @param {string|any} contract contractId or contract instance + * @param {string} accountId executing accountId + * @param {number} role roleId + * @param {string} propertyName target property name + * @param {PropertyType} propertyType list or entry + * @param {ModificationType} modificationType set or remove + * @param {boolean} allow allow or deny + * @return {Promise} resolved when done + */ + public async setOperationPermission( + contract: string|any, + accountId: string, + role: number, + propertyName: string, + propertyType: PropertyType, + modificationType: ModificationType, + allow: boolean) { + const contractId = typeof contract === 'string' ? contract : contract.options.address; + const auth = this.options.contractLoader.loadContract('DSAuth', contractId); + const dsRolesAddress = await this.options.executor.executeContractCall(auth, 'authority'); + const dsRolesContract = this.options.contractLoader.loadContract('DSRolesPerContract', dsRolesAddress); + const keccak256 = this.options.web3.utils.soliditySha3; + const permissionHash = keccak256(keccak256(propertyType, keccak256(propertyName)), modificationType) + await this.options.executor.executeContractTransaction( + dsRolesContract, 'setRoleOperationCapability', { from: accountId, autoGas: 1.1, }, + role, 0, permissionHash, allow); + } + + /** + * adds the target account to a specific role + * + * @param {string|any} contract contractId or contract instance + * @param {string} accountId executing accountId + * @param {string} targetAccountId target accountId + * @param {number} role roleId + * @return {Promise} resolved when done + */ + public async addAccountToRole( + contract: string|any, + accountId: string, + targetAccountId: string, + role: number) { + const contractId = typeof contract === 'string' ? contract : contract.options.address; + const auth = this.options.contractLoader.loadContract('DSAuth', contractId); + const dsRolesAddress = await this.options.executor.executeContractCall(auth, 'authority'); + const dsRolesContract = this.options.contractLoader.loadContract('DSRolesPerContract', dsRolesAddress); + await this.options.executor.executeContractTransaction( + dsRolesContract, 'setUserRole', { from: accountId, autoGas: 1.1, }, + targetAccountId, role, true); + } + + /** + * returns true or false, depending on if the account has the specific role + * @deprecated second argument "accountId" will be dropped, as it isnt' required anymore + * + * @param {string|any} contract contractId or contract instance + * @param {string} accountId executing accountId + * @param {string} targetAccountId to be checked accountId + * @param {number} role roleId + * @return {Promise} true is given user as specified role + */ + public async hasUserRole( + contract: string|any, + accountId: string, + targetAccountId: string, + role: number) { + const contractId = typeof contract === 'string' ? contract : contract.options.address; + const auth = this.options.contractLoader.loadContract('DSAuth', contractId); + const dsRolesAddress = await this.options.executor.executeContractCall(auth, 'authority'); + const dsRolesContract = this.options.contractLoader.loadContract('DSRolesPerContract', dsRolesAddress); + return this.options.executor.executeContractCall(dsRolesContract, 'hasUserRole', targetAccountId, role) + } + + /** + * removes target account from a specific role + * + * @param {string|any} contract contractId or contract instance + * @param {string} accountId executing accountId + * @param {string} targetAccountId target accountId + * @param {number} role roleId + * @return {Promise} resolved when done + */ + public async removeAccountFromRole( + contract: string|any, + accountId: string, + targetAccountId: string, + role: number) { + const contractId = typeof contract === 'string' ? contract : contract.options.address; + const auth = this.options.contractLoader.loadContract('DSAuth', contractId); + const dsRolesAddress = await this.options.executor.executeContractCall(auth, 'authority'); + const dsRolesContract = this.options.contractLoader.loadContract('DSRolesPerContract', dsRolesAddress); + await this.options.executor.executeContractTransaction( + dsRolesContract, 'setUserRole', { from: accountId, autoGas: 1.1, }, + targetAccountId, role, false); + } + + /** + * transfer ownership of a contract and its authority to another account + * + * @param {string|any} contract contractId or contract instance + * @param {string} accountId executing accountId + * @param {string} targetAccountId target accountId + * @return {Promise} resolved when done + */ + public async transferOwnership(contract: string|any, accountId: string, targetAccountId: string + ): Promise { + const contractId = typeof contract === 'string' ? contract : contract.options.address; + const auth = this.options.contractLoader.loadContract('DSAuth', contractId); + const dsRolesAddress = await this.options.executor.executeContractCall(auth, 'authority'); + const dsRolesContract = this.options.contractLoader.loadContract('DSRolesPerContract', dsRolesAddress); + await this.options.executor.executeContractTransaction( + auth, 'setOwner', { from: accountId, autoGas: 1.1, }, targetAccountId); + await this.options.executor.executeContractTransaction( + dsRolesContract, 'setOwner', { from: accountId, autoGas: 1.1, }, targetAccountId); + } +} diff --git a/src/contracts/service-contract/service-contract.spec.ts b/src/contracts/service-contract/service-contract.spec.ts new file mode 100644 index 00000000..e9adf4c3 --- /dev/null +++ b/src/contracts/service-contract/service-contract.spec.ts @@ -0,0 +1,712 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect, use } from 'chai'; +import chaiAsPromised = require('chai-as-promised'); + +import { + ContractLoader, + EventHub, + Executor, + Ipfs, + KeyProvider, + NameResolver, +} from '@evan.network/dbcp'; + +import { accounts } from '../../test/accounts'; +import { ContractState } from '../base-contract/base-contract'; +import { ServiceContract } from './service-contract'; +import { config } from '../../config'; +import { TestUtils } from '../../test/test-utils'; + +use(chaiAsPromised); + + +describe('ServiceContract', function() { + this.timeout(60000); + let sc0: ServiceContract; + let sc2: ServiceContract; + let contractFactory: any; + let executor: Executor; + let loader: ContractLoader; + let businessCenterDomain; + let ipfs: Ipfs; + let nameResolver: NameResolver; + let sharing; + let web3; + const sampleService1 = { + serviceName: 'serviceContractService1', + endPoint: 'serviceContractService1', + requestParameters: { + required: [ + 'callName', + ], + properties: { + callName: { + type: 'string', + }, + tags: { + type: 'string', + }, + endDate: { + type: 'integer', + }, + allowMultipleAnswers: { + type: 'boolean', + }, + amount: { + type: 'integer', + }, + articleNumber: { + type: 'string', + }, + possibleWeek: { + type: 'integer', + }, + note: { + type: 'string', + }, + }, + }, + responseParameters: { + properties: { + possibleAmount: { + type: 'integer', + }, + price: { + type: 'integer', + }, + possibleDeliveryWeek: { + type: 'integer', + }, + note: { + type: 'string', + }, + }, + }, + updateService: true, + }; + const sampleService2 = { + serviceName: 'serviceContractService2', + endPoint: 'serviceContractService2', + requestParameters: { + required: [ + 'callName', + ], + properties: { + callName: { + type: 'string', + }, + tags: { + type: 'string', + }, + endDate: { + type: 'integer', + }, + allowMultipleAnswers: { + type: 'boolean', + }, + amount: { + type: 'integer', + }, + articleNumber: { + type: 'string', + }, + possibleWeek: { + type: 'integer', + }, + note: { + type: 'string', + }, + }, + }, + responseParameters: { + properties: { + possibleAmount: { + type: 'integer', + }, + price: { + type: 'integer', + }, + possibleDeliveryWeek: { + type: 'integer', + }, + note: { + type: 'string', + }, + }, + }, + updateService: true, + } + const sampleCall = { + metadata: { + author: accounts[0], + }, + payload: { + callName: 'sampleCall', + tags: 'sample, call', + endDate: 0, + allowMultipleAnswers: true, + amount: 1, + articleNumber: 'ABC-123-456', + possibleWeek: 5, + note: 'this is a sample call', + }, + }; + const sampleAnswer = { + metadata: { + author: accounts[2], + }, + payload: { + possibleAmount: 1, + price: 300, + possibleDeliveryWeek: 5, + note: 'this is an answer to a sample call', + }, + }; + + before(async () => { + web3 = TestUtils.getWeb3(); + nameResolver = await TestUtils.getNameResolver(web3); + executor = await TestUtils.getExecutor(web3); + executor.eventHub = await TestUtils.getEventHub(web3); + loader = await TestUtils.getContractLoader(web3); + ipfs = await TestUtils.getIpfs(); + const defaultKeys = TestUtils.getKeys(); + const keys0 = {}; + const requiredKeys0 = [ + // own 'self' key + web3.utils.soliditySha3(accounts[0]), + // own edge key + web3.utils.soliditySha3.apply(web3.utils.soliditySha3, + [web3.utils.soliditySha3(accounts[0]), web3.utils.soliditySha3(accounts[0])].sort()), + // key with account 0 + web3.utils.soliditySha3.apply(web3.utils.soliditySha3, + [web3.utils.soliditySha3(accounts[0]), web3.utils.soliditySha3(accounts[2])].sort()), + ]; + requiredKeys0.forEach((key) => { keys0[key] = defaultKeys[key]; }); + // sc = await TestUtils.getServiceContract(web3, ipfs); + sc0 = new ServiceContract({ + cryptoProvider: TestUtils.getCryptoProvider(), + dfs: ipfs, + executor, + keyProvider: new KeyProvider({ keys: keys0, }), + loader, + nameResolver, + sharing: await TestUtils.getSharing(web3, ipfs), + web3, + }); + const keys2 = {}; + const requiredKeys2 = [ + // own 'self' key + web3.utils.soliditySha3(accounts[2]), + // own edge key + web3.utils.soliditySha3.apply(web3.utils.soliditySha3, + [web3.utils.soliditySha3(accounts[2]), web3.utils.soliditySha3(accounts[2])].sort()), + // key with account 0 + web3.utils.soliditySha3.apply(web3.utils.soliditySha3, + [web3.utils.soliditySha3(accounts[0]), web3.utils.soliditySha3(accounts[2])].sort()), + ]; + requiredKeys2.forEach((key) => { keys2[key] = defaultKeys[key]; }); + sc2 = new ServiceContract({ + cryptoProvider: TestUtils.getCryptoProvider(), + dfs: ipfs, + executor, + keyProvider: new KeyProvider({ keys: keys2, }), + loader, + nameResolver, + sharing: await TestUtils.getSharing(web3, ipfs), + web3, + }); + businessCenterDomain = nameResolver.getDomainName(config.nameResolver.domains.businessCenter); + sharing = await TestUtils.getSharing(web3, ipfs); + + const businessCenterAddress = await nameResolver.getAddress(businessCenterDomain); + const businessCenter = await loader.loadContract('BusinessCenter', businessCenterAddress); + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[0], { from: accounts[0], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[0], autoGas: 1.1, }); + } + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[1], { from: accounts[1], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[1], autoGas: 1.1, }); + } + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[2], { from: accounts[2], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[2], autoGas: 1.1, }); + } + }); + + after(async () => { + await ipfs.stop(); + web3.currentProvider.connection.close(); + }); + + it('can be created', async () => { + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + expect(contract).to.be.ok; + }); + + it('can store a service', async() => { + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + const service = await sc0.getService(contract, accounts[0]); + expect(service).to.deep.eq(sampleService1); + }); + + it('can update a service', async() => { + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + await sc0.setService(contract, accounts[0], sampleService2, businessCenterDomain); + const service = await sc0.getService(contract, accounts[0]); + expect(service).to.deep.eq(sampleService2); + }); + + it('can send a service message', async() => { + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + const callId = await sc0.sendCall(contract, accounts[0], sampleCall); + const call = await sc0.getCall(contract, accounts[0], callId); + expect(call).to.deep.eq(sampleCall); + }); + + it('can send an answer to a service message', async() => { + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + await sc0.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[2]); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', 0); + await sharing.addSharing(contract.options.address, accounts[0], accounts[2], '*', 0, contentKey); + const callId = await sc0.sendCall(contract, accounts[0], sampleCall, [accounts[2]]); + const call = await sc2.getCall(contract, accounts[0], callId); + const answerId = await sc2.sendAnswer(contract, accounts[2], sampleAnswer, callId, call.metadata.author); + const answer = await sc2.getAnswer(contract, accounts[2], callId, answerId); + expect(answer).to.deep.eq(sampleAnswer); + }); + + it('can hold multiple calls', async() => { + const sampleCalls = [Math.random(), Math.random(), Math.random()].map((rand) => { + const currentSample = JSON.parse(JSON.stringify(sampleCall)); + currentSample.payload.note += rand; + return currentSample; + }); + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + for (let currentSample of sampleCalls) { + await sc0.sendCall(contract, accounts[0], currentSample); + } + expect(await sc0.getCall(contract, accounts[0], 0)).to.deep.eq(sampleCalls[0]); + expect(await sc0.getCall(contract, accounts[0], 1)).to.deep.eq(sampleCalls[1]); + expect(await sc0.getCall(contract, accounts[0], 2)).to.deep.eq(sampleCalls[2]); + }); + + it('does not allow calls to be read by every contract member without extending the sharing', async() => { + const blockNr = await web3.eth.getBlockNumber(); + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + await sc0.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[2]); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', blockNr); + await sharing.addSharing(contract.options.address, accounts[0], accounts[2], '*', blockNr, contentKey); + await sc0.sendCall(contract, accounts[0], sampleCall); + const callPromise = sc2.getCall(contract, accounts[2], 0); + await expect(callPromise).to.be.rejected; + }); + + it('allows calls to be read, when added to a calls sharing', async() => { + const blockNr = await web3.eth.getBlockNumber(); + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + await sc0.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[2]); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', blockNr); + await sharing.addSharing(contract.options.address, accounts[0], accounts[2], '*', blockNr, contentKey); + const callId = await sc0.sendCall(contract, accounts[0], sampleCall); + await sc0.addToCallSharing(contract, accounts[0], callId, [accounts[2]]); + const call = await sc2.getCall(contract, accounts[2], 0); + expect(call).to.deep.eq(sampleCall); + }); + + it('does not allow answers to be read by other members than the original caller', async() => { + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', 0); + await sc0.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[1]); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', 0, contentKey); + await sc0.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[2]); + await sharing.addSharing(contract.options.address, accounts[0], accounts[2], '*', 0, contentKey); + await sc0.sendCall(contract, accounts[0], sampleCall, [accounts[2]]); + const call = await sc2.getCall(contract, accounts[0], 0); + await sc2.sendAnswer(contract, accounts[2], sampleAnswer, 0, call.metadata.author); + + // create second service contract helper with fewer keys + const limitedKeyProvider = TestUtils.getKeyProvider([ + nameResolver.soliditySha3.apply(nameResolver, + [nameResolver.soliditySha3(accounts[0]), nameResolver.soliditySha3(accounts[1])].sort()), + ]); + const limitedSc = await TestUtils.getServiceContract(web3, ipfs, limitedKeyProvider); + const answerPromise = limitedSc.getAnswer(contract, accounts[1], 0, 0); + await expect(answerPromise).to.be.rejected; + }); + + it('can retrieve the count for calls', async() => { + const sampleCalls = [Math.random(), Math.random(), Math.random()].map((rand) => { + const currentSample = JSON.parse(JSON.stringify(sampleCall)); + currentSample.payload.note += rand; + return currentSample; + }); + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + for (let currentSample of sampleCalls) { + await sc0.sendCall(contract, accounts[0], currentSample); + } + expect(await sc0.getCallCount(contract)).to.eq(sampleCalls.length); + }); + + it('can retrieve the count for answers', async() => { + const sampleAnswers = [Math.random(), Math.random(), Math.random()].map((rand) => { + const currentSample = JSON.parse(JSON.stringify(sampleAnswer)); + currentSample.payload.note += rand; + return currentSample; + }); + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + await sc0.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[2]); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', 0); + await sharing.addSharing(contract.options.address, accounts[0], accounts[2], '*', 0, contentKey); + await sc0.sendCall(contract, accounts[0], sampleCall, [accounts[2]]); + const call = await sc2.getCall(contract, accounts[2], 0); + for (let currentSample of sampleAnswers) { + await sc2.sendAnswer(contract, accounts[2], currentSample, 0, call.metadata.author); + } + const answerCount = await sc0.getAnswerCount(contract, 0); + expect(answerCount).to.eq(sampleAnswers.length); + }); + + it('can retrieve all answers', async() => { + const sampleAnswers = [Math.random(), Math.random(), Math.random()].map((rand) => { + const currentSample = JSON.parse(JSON.stringify(sampleAnswer)); + currentSample.payload.note += rand; + return currentSample; + }); + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + await sc0.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[2]); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', 0); + await sharing.addSharing(contract.options.address, accounts[0], accounts[2], '*', 0, contentKey); + await sc0.sendCall(contract, accounts[0], sampleCall, [accounts[2]]); + const call = await sc2.getCall(contract, accounts[0], 0); + for (let currentSample of sampleAnswers) { + await sc2.sendAnswer(contract, accounts[2], currentSample, 0, call.metadata.author); + } + const answers = await sc0.getAnswers(contract, accounts[0], 0); + expect(answers.length).to.eq(3); + const reversed = answers.reverse(); + expect(answers[0]).to.deep.eq(reversed[0]); + expect(answers[1]).to.deep.eq(reversed[1]); + expect(answers[2]).to.deep.eq(reversed[2]); + }); + + it('can create answers and read and answer them with another user', async() => { + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + await sc0.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[2]); + const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', 0); + await sharing.addSharing(contract.options.address, accounts[0], accounts[2], '*', 0, contentKey); + const callId = await sc0.sendCall(contract, accounts[0], sampleCall, [accounts[2]]); + + // retrieve call with other account, create answer + const call = await sc2.getCall(contract, accounts[2], callId); + expect(call).to.deep.eq(sampleCall); + const answerId = await sc2.sendAnswer(contract, accounts[2], sampleAnswer, callId, call.metadata.author); + + // retrieve answer with first account + const answer = await sc2.getAnswer(contract, accounts[0], callId, answerId); + expect(answer).to.deep.eq(sampleAnswer); + }); + + describe('when paging through answers and answers', () => { + let contract; + let sampleCalls; + let sampleAnswers; + const anweredCallId = 6; + const anwersCount = 27; + const callCount = 23; + + before(async () => { + sampleCalls = [...Array(callCount)].map(() => Math.random()).map((rand, i) => {; + const currentSample = JSON.parse(JSON.stringify(sampleCall)); + currentSample.payload.note += i; + return currentSample; + }); + sampleAnswers = []; + for (let i = 0; i < anwersCount; i++) { + const answer = JSON.parse(JSON.stringify(sampleAnswer)); + answer.payload.note += i; + sampleAnswers.push(answer); + } + + // if using existing contract + contract = loader.loadContract('ServiceContractInterface', '0xdeDca22030f95488E7db80A3aF26A1C122aeCa17'); + + // // if creating new contract + // contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + // await sc0.inviteToContract(businessCenterDomain, contract.options.address, accounts[0], accounts[2]); + // const contentKey = await sharing.getKey(contract.options.address, accounts[0], '*', 0); + // await sharing.addSharing(contract.options.address, accounts[0], accounts[2], '*', 0, contentKey); + // let callIndex = 0; + // for (let currentSample of sampleAnswers) { + // console.log(`send test call ${callIndex++}`); + // await sc0.sendCall(contract, accounts[0], currentSample, [accounts[2]]); + // } + // let answerIndex = 0; + // for (let answer of sampleAnswers) { + // console.log(`send test answer ${answerIndex++}`); + // await sc2.sendAnswer(contract, accounts[2], answer, anweredCallId, accounts[0]) + // } + // console.log(contract.options.address); + }); + + describe('when retrieving calls', () => { + it('can retrieve calls', async() => { + const calls = await sc0.getCalls(contract, accounts[0]); + expect(calls.length).to.eq(Math.min(sampleCalls.length, 10)); + expect(calls.length).to.eq(10); + for (let i = 0; i < calls.length; i++) { + expect(calls[i]).to.deep.eq(sampleCalls[i]); + } + }); + + it('can retrieve calls with a limited page size', async() => { + const count = 2; + const calls = await sc0.getCalls(contract, accounts[0], count); + expect(calls.length).to.eq(Math.min(sampleCalls.length, count)); + for (let i = 0; i < calls.length; i++) { + expect(calls[i]).to.deep.eq(sampleCalls[i]); + } + }); + + it('can retrieve calls with offset that results in a in a full page', async() => { + const offset = 7; + const calls = await sc0.getCalls(contract, accounts[0], 10, offset); + expect(calls.length).to.eq(Math.min(sampleCalls.length - offset, 10)); + for (let i = 0; i < calls.length; i++) { + expect(calls[i]).to.deep.eq(sampleCalls[i + offset]); + } + }); + + it('can retrieve calls with offset that doesn\'t result not full page', async() => { + const offset = 17; + const calls = await sc0.getCalls(contract, accounts[0], 10, offset); + expect(calls.length).to.eq(Math.min(sampleCalls.length - offset, 10)); + for (let i = 0; i < calls.length; i++) { + expect(calls[i]).to.deep.eq(sampleCalls[i + offset]); + } + }); + + it('can retrieve calls with limited page size and offset', async() => { + const count = 2; + const offset = 17; + const calls = await sc0.getCalls(contract, accounts[0], 2, offset); + expect(calls.length).to.eq(Math.min(sampleCalls.length - offset, count)); + for (let i = 0; i < calls.length; i++) { + expect(calls[i]).to.deep.eq(sampleCalls[i + offset]); + } + }); + + it('can retrieve calls in reverse order', async() => { + const calls = await sc0.getCalls(contract, accounts[0], 10, 0, true); + expect(calls.length).to.eq(10); + for (let i = 0; i < calls.length; i++) { + expect(calls[i]).to.deep.eq(sampleCalls[sampleCalls.length - 1 - i]); + } + }); + + it('can retrieve calls in reverse order with a limited page size', async() => { + const count = 2; + const calls = await sc0.getCalls(contract, accounts[0], count, 0, true); + expect(calls.length).to.eq(Math.min(sampleCalls.length, count)); + for (let i = 0; i < calls.length; i++) { + expect(calls[i]).to.deep.eq(sampleCalls[sampleCalls.length - 1 - i]); + } + }); + + it('can retrieve calls in reverse order with offset that results in a full page', async() => { + const offset = 7; + const calls = await sc0.getCalls(contract, accounts[0], 10, offset, true); + expect(calls.length).to.eq(Math.min(sampleCalls.length - offset, 10)); + for (let i = 0; i < calls.length; i++) { + expect(calls[i]).to.deep.eq(sampleCalls[sampleCalls.length - 1 - i - offset]); + } + }); + + it('can retrieve calls with offset that doesn\'t result not full page', async() => { + const offset = 17; + const calls = await sc0.getCalls(contract, accounts[0], 10, offset, true); + expect(calls.length).to.eq(Math.min(sampleCalls.length - offset, 10)); + for (let i = 0; i < calls.length; i++) { + expect(calls[i]).to.deep.eq(sampleCalls[sampleCalls.length - 1 - i - offset]); + } + }); + + it('can retrieve calls with limited page size and offset', async() => { + const count = 2; + const offset = 17; + const calls = await sc0.getCalls(contract, accounts[0], 2, offset, true); + expect(calls.length).to.eq(Math.min(sampleCalls.length - offset, count)); + for (let i = 0; i < calls.length; i++) { + expect(calls[i]).to.deep.eq(sampleCalls[sampleCalls.length - 1 - i - offset]); + } + }); + }); + + describe('when retrieving answers', () => { + it('can retrieve answers', async() => { + const answers = await sc0.getAnswers(contract, accounts[0], anweredCallId); + expect(answers.length).to.eq(Math.min(sampleAnswers.length, 10)); + for (let i = 0; i < answers.length; i++) { + expect(answers[i]).to.deep.eq(sampleAnswers[i]); + } + }); + + it('can retrieve answers with a limited page size', async() => { + const count = 2; + const answers = await sc0.getAnswers(contract, accounts[0], anweredCallId, count); + expect(answers.length).to.eq(Math.min(sampleAnswers.length, count)); + for (let i = 0; i < answers.length; i++) { + expect(answers[i]).to.deep.eq(sampleAnswers[i]); + } + }); + + it('can retrieve answers with offset that results in a in a full page', async() => { + const offset = 7; + const answers = await sc0.getAnswers(contract, accounts[0], anweredCallId, 10, offset); + expect(answers.length).to.eq(Math.min(sampleAnswers.length - offset, 10)); + for (let i = 0; i < answers.length; i++) { + expect(answers[i]).to.deep.eq(sampleAnswers[i + offset]); + } + }); + + it('can retrieve answers with offset that doesn\'t result not full page', async() => { + const offset = 17; + const answers = await sc0.getAnswers(contract, accounts[0], anweredCallId, 10, offset); + expect(answers.length).to.eq(Math.min(sampleAnswers.length - offset, 10)); + for (let i = 0; i < answers.length; i++) { + expect(answers[i]).to.deep.eq(sampleAnswers[i + offset]); + } + }); + + it('can retrieve answers with limited page size and offset', async() => { + const count = 2; + const offset = 17; + const answers = await sc0.getAnswers(contract, accounts[0], anweredCallId, 2, offset); + expect(answers.length).to.eq(Math.min(sampleAnswers.length - offset, count)); + for (let i = 0; i < answers.length; i++) { + expect(answers[i]).to.deep.eq(sampleAnswers[i + offset]); + } + }); + + it('can retrieve answers in reverse order', async() => { + const answers = await sc0.getAnswers(contract, accounts[0], anweredCallId, 10, 0, true); + expect(answers.length).to.eq(10); + for (let i = 0; i < answers.length; i++) { + expect(answers[i]).to.deep.eq(sampleAnswers[sampleAnswers.length - 1 - i]); + } + }); + + it('can retrieve answers in reverse order with a limited page size', async() => { + const count = 2; + const answers = await sc0.getAnswers(contract, accounts[0], anweredCallId, count, 0, true); + expect(answers.length).to.eq(Math.min(sampleAnswers.length, count)); + for (let i = 0; i < answers.length; i++) { + expect(answers[i]).to.deep.eq(sampleAnswers[sampleAnswers.length - 1 - i]); + } + }); + + it('can retrieve answers in reverse order with offset that results in a full page', async() => { + const offset = 7; + const answers = await sc0.getAnswers(contract, accounts[0], anweredCallId, 10, offset, true); + expect(answers.length).to.eq(Math.min(sampleAnswers.length - offset, 10)); + for (let i = 0; i < answers.length; i++) { + expect(answers[i]).to.deep.eq(sampleAnswers[sampleAnswers.length - 1 - i - offset]); + } + }); + + it('can retrieve answers with offset that doesn\'t result not full page', async() => { + const offset = 17; + const answers = await sc0.getAnswers(contract, accounts[0], anweredCallId, 10, offset, true); + expect(answers.length).to.eq(Math.min(sampleAnswers.length - offset, 10)); + for (let i = 0; i < answers.length; i++) { + expect(answers[i]).to.deep.eq(sampleAnswers[sampleAnswers.length - 1 - i - offset]); + } + }); + + it('can retrieve answers with limited page size and offset', async() => { + const count = 2; + const offset = 17; + const answers = await sc0.getAnswers(contract, accounts[0], anweredCallId, 2, offset, true); + expect(answers.length).to.eq(Math.min(sampleAnswers.length - offset, count)); + for (let i = 0; i < answers.length; i++) { + expect(answers[i]).to.deep.eq(sampleAnswers[sampleAnswers.length - 1 - i - offset]); + } + }); + }); + }); + + it('can send and read service message with nested encryption', async () => { + // create call with a custom property, that contains cryptoInfo and private + const contract = await sc0.create(accounts[0], businessCenterDomain, sampleService1); + const callForNesting = JSON.parse(JSON.stringify(sampleCall)); + + // get cryptor for annotating encryption of properties + const cryptor = sc0.options.cryptoProvider.getCryptorByCryptoAlgo(sc0.options.defaultCryptoAlgo); + callForNesting.payload.privateData = { + private: { + someNumber: Math.random(), + someText: `I like randomNumbers in payload, for example: ${Math.random()}`, + }, + cryptoInfo: cryptor.getCryptoInfo(sc0.options.nameResolver.soliditySha3(contract.options.address)), + }; + callForNesting.metadata.privateData = { + private: `I like randomNumbers in metadata, for example: ${Math.random()}`, + cryptoInfo: cryptor.getCryptoInfo(sc0.options.nameResolver.soliditySha3(contract.options.address)), + }; + // send it as usual (to-encrypt properties are encrypted automatically); invite participant + const callId = await sc0.sendCall(contract, accounts[0], callForNesting, [accounts[2]]); + + // fetch with creator + let call = await sc0.getCall(contract, accounts[0], callId); + expect(call).to.deep.eq(callForNesting); + + // fetch with participant + call = await sc2.getCall(contract, accounts[2], callId); + // participant can read 'outer' properties + expect(call.metadata.author).to.eq(callForNesting.metadata.author); + expect(call.payload.callName).to.eq(callForNesting.payload.callName); + expect(call.payload.tags).to.eq(callForNesting.payload.tags); + expect(call.payload.endDate).to.eq(callForNesting.payload.endDate); + expect(call.payload.allowMultipleAnswers).to.eq(callForNesting.payload.allowMultipleAnswers); + expect(call.payload.amount).to.eq(callForNesting.payload.amount); + expect(call.payload.articleNumber).to.eq(callForNesting.payload.articleNumber); + expect(call.payload.possibleWeek).to.eq(callForNesting.payload.possibleWeek); + expect(call.payload.note).to.eq(callForNesting.payload.note); + + // participant cannot read 'inner' properties + expect(call.metadata.privateData).not.to.eq(callForNesting.metadata.privateData); + expect(call.payload.privateData).not.to.eq(callForNesting.payload.privateData); + + // add sharing for participent + await sc0.addToCallSharing(contract, accounts[0], callId, [accounts[2]], null, null, 'privateData'); + // fetch again + sc2.options.sharing.clearCache(); + call = await sc2.getCall(contract, accounts[2], callId); + expect(call).to.deep.eq(callForNesting); + }); +}); diff --git a/src/contracts/service-contract/service-contract.ts b/src/contracts/service-contract/service-contract.ts new file mode 100644 index 00000000..c5eafa62 --- /dev/null +++ b/src/contracts/service-contract/service-contract.ts @@ -0,0 +1,796 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import crypto = require('crypto'); +import prottle = require('prottle'); + +import { + ContractLoader, + DfsInterface, + Envelope, + KeyProviderInterface, + Logger, +} from '@evan.network/dbcp'; + +import { BaseContract, BaseContractOptions, } from '../base-contract/base-contract'; +import { CryptoProvider } from '../../encryption/crypto-provider'; +import { Sharing } from '../sharing'; + +const requestWindowSize = 10; + + +/** + * options for ServiceContract constructor + */ +export interface ServiceContractOptions extends BaseContractOptions { + cryptoProvider: CryptoProvider, + dfs: DfsInterface, + keyProvider: KeyProviderInterface, + sharing: Sharing, + web3: any, + defaultCryptoAlgo?: string, +} + +/** + * helper class for ServiceContracts + * + * @class ServiceContract (name) + */ +export class ServiceContract extends BaseContract { + public options: ServiceContractOptions; + private readonly encodingUnencrypted = 'utf-8'; + private readonly encodingEncrypted = 'hex'; + private readonly encodingUnencryptedHash = 'hex'; + private readonly cryptoAlgorithHashes = 'aesEcb' + + constructor(optionsInput: ServiceContractOptions) { + super(optionsInput as BaseContractOptions); + this.options = optionsInput; + if (!this.options.defaultCryptoAlgo) { + this.options.defaultCryptoAlgo = 'aes'; + } + } + + /** + * adds list of accounts to a calls sharings list + * + * @param {any|string} contract contract instance or contract id + * @param {string} accountId account id of sharing user + * @param {number} callId id of the call to extend sharings for + * @param {string[]} to list of account ids + * @return {Promise} resolved when done + */ + public async addToCallSharing( + contract: any|string, + accountId: string, + callId: number, + to: string[], + hashKey?: string, + contentKey?: string, + section = '*'): Promise { + const serviceContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('ServiceContractInterface', contract); + const callIdHash = this.numberToBytes32(callId); + const [blockNr, hashKeyToShare] = await Promise.all([ + this.options.web3.eth.getBlockNumber(), + hashKey || this.options.sharing.getHashKey(serviceContract.options.address, accountId, callIdHash), + ]); + const contentKeyToShare = contentKey || + (await this.options.sharing.getKey(serviceContract.options.address, accountId, section, blockNr, callIdHash)); + for (let target of to) { + await this.options.sharing.ensureHashKey( + contract.options.address, accountId, target, hashKeyToShare, null, callIdHash); + await this.options.sharing.addSharing( + contract.options.address, accountId, target, section, 0, contentKeyToShare, null, false, callIdHash); + } + } + + /** + * create and initialize new contract + * + * @param {string} accountId owner of the new contract and transaction + * executor + * @param {string} businessCenterDomain ENS domain name of the business center + * @param {any} service service definition + * @param {string} descriptionDfsHash bytes32 hash of DBCP description + * @return {Promise} contract instance + */ + public async create( + accountId: string, + businessCenterDomain: string, + service: any, + descriptionDfsHash = '0x0000000000000000000000000000000000000000000000000000000000000000', + ): Promise { + const contractP = (async () => { + const contractId = await super.createUninitialized( + 'servizz', accountId, businessCenterDomain, descriptionDfsHash); + return this.options.loader.loadContract('ServiceContractInterface', contractId); + })(); + + // create sharing key for owner + const cryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.options.defaultCryptoAlgo); + const hashCryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.cryptoAlgorithHashes); + const [contentKey, hashKey, contract] = await Promise.all( + [cryptor.generateKey(), hashCryptor.generateKey(), contractP]); + await this.options.sharing.addSharing(contract.options.address, accountId, accountId, '*', 0, contentKey); + await this.options.sharing.ensureHashKey(contract.options.address, accountId, accountId, hashKey); + + // add service after sharing has been added + await this.setService(contract, accountId, service, businessCenterDomain); + return contract; + } + + /** + * retrieve a single answer + * + * @param {any|string} contract smart contract instance or contract ID + * @param {string} accountId Ethereum account ID + * @param {number} callId index of the call to which the answer was created + * @param {number} answerIndex index of the answer in the call (starts from 0 for + * every call) + * @return {Promise} the answer + */ + public async getAnswer( + contract: any|string, accountId: string, callId: number, answerIndex: number): Promise { + const serviceContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('ServiceContractInterface', contract); + const encryptedHash = await this.options.executor.executeContractCall(serviceContract, 'answersPerCall', callId, answerIndex); + const decryptedHash = await this.decryptHash(encryptedHash, serviceContract, accountId, this.numberToBytes32(callId)); + const decrypted = await this.decrypt( + (await this.options.dfs.get(decryptedHash)).toString('utf-8'), + serviceContract, + accountId, + '*' + ); + return decrypted; + } + + /** + * retrieves number of answers for a given call + * + * @param {any|string} contract smart contract instance or contract ID + * @param {number} callId call index + * @return {Promise} number of answers + */ + public async getAnswerCount(contract: any|string, callId: number): Promise { + const serviceContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('ServiceContractInterface', contract); + const count = await this.options.executor.executeContractCall(serviceContract, 'answersCountPerCall', callId); + return parseInt(count, 10); + } + + /** + * gets answers for a given call + * + * @param {any|string} contract smart contract instance or contract ID + * @param {string} accountId Ethereum account ID + * @param {number} callId index of the call to which the answer was created + * @param {number} count number of elements to retrieve + * @param {number} offset skip this many elements + * @param {boolean} reverse retrieve last elements first + * @return {Promise} the calls + */ + public async getAnswers( + contract: any|string, + accountId: string, + callId: number, + count = 10, + offset = 0, + reverse = false): Promise { + const serviceContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('ServiceContractInterface', contract); + + // get entries + const { entries, indices } = await this.getEntries(serviceContract, 'answers', callId, count, offset, reverse); + + // decrypt contents + // answer hashes are encrypted with calls hash key + const callIdString = this.numberToBytes32(callId); + const tasks = indices.map((index) => async () => { + const decryptedHash = await this.decryptHash(entries[index].encryptedHash, serviceContract, accountId, callIdString); + return this.decrypt( + (await this.options.dfs.get(decryptedHash)).toString('utf-8'), + serviceContract, + accountId, + '*', + callIdString, + ); + }); + + const decrypted = tasks.length ? await prottle(requestWindowSize, tasks) : []; + + return decrypted; + } + + /** + * get a call from a contract + * + * @param {any|string} contract smart contract instance or contract ID + * @param {string} accountId Ethereum account ID + * @param {number} callId index of the call to retrieve + * @return {Promise} the call + */ + public async getCall(contract: any|string, accountId: string, callId: number): Promise { + const serviceContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('ServiceContractInterface', contract); + const callIdString = this.numberToBytes32(callId); + const encryptedHash = await this.options.executor.executeContractCall( + serviceContract, 'calls', callIdString); + const decryptedHash = await this.decryptHash(encryptedHash, serviceContract, accountId, callIdString); + const decrypted = await this.decrypt( + (await this.options.dfs.get(decryptedHash)).toString('utf-8'), + serviceContract, + accountId, + '*', + callIdString, + ); + return decrypted; + } + + /** + * get all calls from a contract + * + * @param {any|string} contract smart contract instance or contract ID + * @param {string} accountId Ethereum account ID + * @param {number} count number of elements to retrieve + * @param {number} offset skip this many elements + * @param {boolean} reverse retrieve last elements first + * @return {Promise} the calls + */ + public async getCalls( + contract: any|string, + accountId: string, + count = 10, + offset = 0, + reverse = false): Promise { + const serviceContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('ServiceContractInterface', contract); + + // get entries + const { entries, indices } = await this.getEntries(serviceContract, 'calls', null, count, offset, reverse); + + // add sharings hashes to sharing module cache + indices.forEach((index) => { + this.options.sharing.addHashToCache(contract.options.address, entries[index].sharing, this.numberToBytes32(index)); + }); + + // decrypt contents + const tasks = indices.map((index) => async () => { + const callIdString = this.numberToBytes32(index); + const decryptedHash = await this.decryptHash(entries[index].encryptedHash, serviceContract, accountId, callIdString); + return this.decrypt( + (await this.options.dfs.get(decryptedHash)).toString('utf-8'), + serviceContract, + accountId, + '*', + callIdString, + ); + }); + + const decrypted = tasks.length ? await prottle(requestWindowSize, tasks) : []; + + return decrypted; + } + + /** + * get number of calls of a contract + * + * @param {any|string} contract smart contract instance or contract ID + * @return {Promise} number of calls + */ + public async getCallCount(contract: any|string): Promise { + const serviceContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('ServiceContractInterface', contract); + const count = await this.options.executor.executeContractCall(serviceContract, 'callCount'); + return parseInt(count, 10); + } + + /** + * gets the service of a service contract + * + * @param {any|string} contract smart contract instance or contract ID + * @param {string} accountId Ethereum account ID + * @return {Promise} service description + */ + public async getService(contract: any|string, accountId: string): Promise { + const serviceContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('ServiceContractInterface', contract); + const encryptedHash = await this.options.executor.executeContractCall(serviceContract, 'service'); + const decryptedHash = await this.decryptHash(encryptedHash, serviceContract, accountId); + const decrypted = await this.decrypt( + (await this.options.dfs.get(decryptedHash)).toString('utf-8'), + serviceContract, + accountId, + '*' + ); + return decrypted; + } + + /** + * send answer to service contract call + * + * @param {any|string} contract smart contract instance or contract ID + * @param {string} accountId Ethereum account ID + * @param {any} answer answer to send + * @param {number} callId index of the call to which the answer was created + * @param {string} callAuthor Ethereum account ID of the creator of the initial call + * @return {Promise} resolved when done + */ + public async sendAnswer( + contract: any|string, + accountId: string, + answer: any, + callId: number, + callAuthor: string): Promise { + const serviceContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('ServiceContractInterface', contract); + const blockNr = 0; // will be ignored as callAuthor is set + const encrypted = await this.encrypt(answer, serviceContract, accountId, '*', blockNr, callAuthor); + const stateMd5 = crypto.createHash('md5').update(encrypted).digest('hex'); + const answerHash = await this.options.dfs.add(stateMd5, Buffer.from(encrypted)); + const hashKey = await this.options.sharing.getHashKey(contract.options.address, accountId, this.numberToBytes32(callId)); + const encryptdHash = await this.encryptHash(answerHash, serviceContract, accountId, hashKey); + const answerId = await this.options.executor.executeContractTransaction( + contract, + 'sendAnswer', { + from: accountId, + autoGas: 1.1, + event: { + target: 'ServiceContractInterface', + eventName: 'ServiceContractEvent', + }, + getEventResult: (event, args) => args.entryId, + }, + encryptdHash, + callId, + ); + return parseInt(answerId, 16); + }; + + /** + * send a call to a service + * + * @param {any|string} contract smart contract instance or contract ID + * @param {string} accountId Ethereum account ID + * @param {any} call call to send + * @return {Promise} returns id of new call + */ + public async sendCall( + contract: any|string, + accountId: string, + call: any, + to: string[] = []): Promise { + const serviceContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('ServiceContractInterface', contract); + + // create local copy of call for encryption + const callCopy = JSON.parse(JSON.stringify(call)); + + // get block number for cryptoInfos + const blockNr = await this.options.web3.eth.getBlockNumber(); + + // subproperties metadata.fnord and payload.fnord use the same key, + // so track keys for subproperties and cryptors like 'fnord' here + const innerEncryptionData = {}; + const innerPropertiesToEncrpt = {}; + // encrypt properties + const generateKeys = async (property) => { + innerPropertiesToEncrpt[property] = []; + if (callCopy[property]) { + for (let key of Object.keys(callCopy[property])) { + if (callCopy[property][key].hasOwnProperty('private') && + callCopy[property][key].hasOwnProperty('cryptoInfo') && + !innerPropertiesToEncrpt[property][key]) { + innerPropertiesToEncrpt[property].push(key); + innerEncryptionData[key] = {}; + innerEncryptionData[key].cryptor = this.options.cryptoProvider.getCryptorByCryptoInfo( + callCopy[property][key].cryptoInfo); + innerEncryptionData[key].key = await innerEncryptionData[key].cryptor.generateKey(); + } + } + } + }; + // run once for metadata and once for payload, await them sequentially to track already generated keys + await Object.keys(callCopy).reduce((chain, key) => chain.then(() => { generateKeys(key) }), Promise.resolve()); + + // create keys for new call (outer properties) + const cryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.options.defaultCryptoAlgo); + const hashCryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.cryptoAlgorithHashes); + const [contentKey, hashKey] = await Promise.all([cryptor.generateKey(), hashCryptor.generateKey()]); + + // use key to encrypt message (outer properties) + const encrypted = await this.encrypt( + callCopy, + serviceContract, + accountId, + '*', + blockNr, + null, + contentKey, + innerPropertiesToEncrpt, + innerEncryptionData, + ); + + // store enc message and sharing to contract + const stateMd5 = crypto.createHash('md5').update(encrypted).digest('hex'); + const serviceHash = await this.options.dfs.add(stateMd5, Buffer.from(encrypted)); + const encryptdHash = await this.encryptHash(serviceHash, serviceContract, accountId, hashKey); + const callIdUint256 = parseInt(await this.options.executor.executeContractTransaction( + contract, + 'sendCall', { + from: accountId, + autoGas: 1.1, + event: { + target: 'ServiceContractInterface', + eventName: 'ServiceContractEvent', + }, + getEventResult: (event, args) => args.entryId, + }, + encryptdHash, + ), 10); + + // put key in sharings, requires msg to be stored + // add hash key + const callId = this.numberToBytes32(callIdUint256); + // keep keys for owner + await this.options.sharing.ensureHashKey( + contract.options.address, accountId, accountId, hashKey, null, callId); + await this.options.sharing.addSharing( + contract.options.address, accountId, accountId, '*', 0, contentKey, null, false, callId); + // if subproperties were encryted, keep them for owner as well + for (let propertyName of Object.keys(innerEncryptionData)) { + await this.options.sharing.addSharing( + contract.options.address, + accountId, + accountId, + propertyName, + 0, + innerEncryptionData[propertyName].key, + null, + false, + callId + ); + } + // for each to, add sharing keys + await this.addToCallSharing(contract, accountId, callIdUint256, to, hashKey, contentKey); + + // return id of new call + return parseInt(callId, 16); + } + + /** + * set service description + * + * @param {any|string} contract smart contract instance or contract ID + * @param {string} accountId Ethereum account ID + * @param {any} service service to set + * @param {string} businessCenterDomain domain of the business the service contract + * belongs to + * @return {Promise} resolved when done + */ + public async setService( + contract: any|string, + accountId: string, + service: any, + businessCenterDomain: string): Promise { + const serviceContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('ServiceContractInterface', contract); + const blockNr = await this.options.web3.eth.getBlockNumber(); + const serviceHashP = (async () => { + const encrypted = await this.encrypt(service, serviceContract, accountId, '*', blockNr); + const stateMd5 = crypto.createHash('md5').update(encrypted).digest('hex'); + const serviceHash = await this.options.dfs.add(stateMd5, Buffer.from(encrypted)); + return await this.encryptHash(serviceHash, serviceContract, accountId); + })(); + const [businessCenterAddress, encryptdHash] = await Promise.all([ + this.options.nameResolver.getAddress(businessCenterDomain), + serviceHashP, + ]); + await this.options.executor.executeContractTransaction( + contract, + 'setService', {from: accountId, autoGas: 1.1, }, + businessCenterAddress, + encryptdHash, + ); + } + + /** + * decrypt message + * + * @param {string} toDecrypt message to decrypt + * @param {any} contract contract the message belongs to + * @param {string} accountId account, that decrypts + * @param {string} propertyName name of the property to decrypt + * @param {string} callId (optional) if a call, id of the call to decrypt + * @return {Promise} decrypted message + */ + private async decrypt( + toDecrypt: string, + contract: any, + accountId: string, + propertyName: string, + callId?: string): Promise { + const envelope: Envelope = JSON.parse(toDecrypt); + const cryptor = this.options.cryptoProvider.getCryptorByCryptoInfo(envelope.cryptoInfo); + // check if directed message, encrypted with comm key + let contentKey = await this.options.keyProvider.getKey(envelope.cryptoInfo); + if (!contentKey) { + // check if encrypted via sharing + contentKey = await this.options.sharing.getKey(contract.options.address, accountId, propertyName, envelope.cryptoInfo.block, callId); + } + if (!contentKey) { + throw new Error(`could not decrypt data, no content key found for contract "${contract.options.address}" and account "${accountId}"`); + } + const decryptedObject = await cryptor.decrypt( + Buffer.from(envelope.private, this.encodingEncrypted), { key: contentKey, }); + + await Promise.all(Object.keys(decryptedObject).map(async (property) => { + await Promise.all(Object.keys(decryptedObject[property]).map(async (key) => { + if (decryptedObject[property][key].hasOwnProperty('private') && + decryptedObject[property][key].hasOwnProperty('cryptoInfo')) { + try { + const envelopeInner = decryptedObject[property][key]; + const contentKeyInner = await this.options.sharing.getKey( + contract.options.address, accountId, key, envelopeInner.cryptoInfo.block, callId); + decryptedObject[property][key] = await cryptor.decrypt( + Buffer.from(envelopeInner.private, this.encodingEncrypted), { key: contentKeyInner, }); + } catch (ex) { + this.log(`could not decrypt inner service message part ${property}/${key}; ${ex.message || ex}`, 'info') + } + } + })); + })); + + return decryptedObject; + } + + /** + * decrypt input hash, return decrypted hash + * + * @param {string} toDecrypt data to decrypt + * @param {any} contract contract instance or contract id + * @param {string} accountId account id that decrypts the data + * @param {string} callId (optional) if a call should be decrypted, id of the call + * @return {Promise} decrypted envelope + */ + private async decryptHash( + toDecrypt: string, contract: any, accountId: string, callId?: string): Promise { + const dataContract = (typeof contract === 'object') ? + contract : this.options.loader.loadContract('DataContractInterface', contract); + // decode hash + const cryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.cryptoAlgorithHashes); + const hashKey = await this.options.sharing.getHashKey(dataContract.options.address, accountId, callId); + if (!hashKey) { + throw new Error(`no hashKey key found for contract "${dataContract.options.address}" and account "${accountId}"`); + } + const decryptedBuffer = await cryptor.decrypt( + Buffer.from(toDecrypt.substr(2), this.encodingEncrypted), { key: hashKey, }); + return `0x${decryptedBuffer.toString(this.encodingUnencryptedHash)}`; + } + + /** + * encrypt message + * + * @param {string} toEncrypt message to encrypt + * @param {any} contract contract to encrypt message for + * @param {string} from encrypting account + * @param {string} propertyName property, that is encrypted + * @param {number} block current block + * @param {to} to (optional) target of message (if encrypting a + * call, target is none (no specific ENCRYPTION + * target, keys wrapped in multi sharings)) + * @param {Buffer} key (optional) key to use, if no key is provided, + * sharings is used to look for a key + * @return {Promise} stringified {Envelope} + */ + private async encrypt( + toEncrypt: any, + contract: any, + from: string, + propertyName: string, + block: number, + to?: string, + key?: Buffer, + innerPropertiesToEncrpt?: any, + innerEncryptionData?: any): Promise { + // helper for encrypting properties + const encryptSubProperties = async (property) => { + if (innerPropertiesToEncrpt[property]) { + await Promise.all(innerPropertiesToEncrpt[property].map(async (keyInner) => { + if (innerPropertiesToEncrpt[property].includes(keyInner) && + toEncrypt[property][keyInner]) { + // encrypt with content key + const encryptedBufferInner = await innerEncryptionData[keyInner].cryptor.encrypt( + toEncrypt[property][keyInner], { key: innerEncryptionData[keyInner].key, }); + const encryptedProperty = encryptedBufferInner.toString(this.encodingEncrypted); + const envelopeInner: Envelope = { + private: encryptedProperty, + cryptoInfo: toEncrypt[property][keyInner].cryptoInfo, + }; + envelopeInner.cryptoInfo.block = block; + toEncrypt[property][keyInner] = envelopeInner; + } + })); + } + }; + // encrypt properties + if (innerPropertiesToEncrpt) { + await Promise.all(Object.keys(toEncrypt).map(async (toEncryptKey) => encryptSubProperties(toEncryptKey))); + } + + // get content key from contract + const cryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.options.defaultCryptoAlgo); + let cryptoInfo; + let contentKey; + if (to) { + // directed message, encrypted with comm key + const fromHash = this.options.nameResolver.soliditySha3(from); + const toHash = this.options.nameResolver.soliditySha3(to); + const combinedHash = this.options.nameResolver.soliditySha3.apply(this.options.nameResolver, [fromHash, toHash].sort()); + cryptoInfo = cryptor.getCryptoInfo(combinedHash); + if (key) { + contentKey = key; + } else { + contentKey = await this.options.keyProvider.getKey(cryptoInfo); + } + } else { + // group message, scoped to contract + cryptoInfo = cryptor.getCryptoInfo(this.options.nameResolver.soliditySha3(contract.options.address)); + if (key) { + // encrpted with calls data key from argument + contentKey = key; + } else { + // retrieve calls data key from sharings + contentKey = await this.options.sharing.getKey(contract.options.address, from, propertyName, block); + } + } + if (!contentKey) { + throw new Error(`no content key found for contract "${contract.options.address}" and account "${from}"`); + } + // encrypt with content key + const encryptedBuffer = await cryptor.encrypt(toEncrypt, { key: contentKey, }); + const encrypted = encryptedBuffer.toString(this.encodingEncrypted); + const envelope: Envelope = { + private: encrypted, + cryptoInfo, + }; + envelope.cryptoInfo.block = block; + return JSON.stringify(envelope); + } + + /** + * encrypt incoming hash + * + * @param {string} toEncrypt hash to encrypt + * @param {any} contract contract to encrypt data for + * @param {string} accountId encrypting account + * @param {Buffer} key key to use (if no key is provided, use key from + * sharings) + * @return {Promise} encrypted envelope or hash as string + */ + private async encryptHash( + toEncrypt: string, contract: any, accountId: string, key?: Buffer): Promise { + // get hashKkey from contract + let hashKey; + if (key) { + hashKey = key; + } else { + hashKey = await this.options.sharing.getHashKey(contract.options.address, accountId); + } + + if (!hashKey) { + throw new Error(`no hashKey found for contract "${contract.options.address}" and account "${accountId}"`); + } + // encrypt with hashKkey + const cryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.cryptoAlgorithHashes); + const encryptedBuffer = await cryptor.encrypt(Buffer.from(toEncrypt.substr(2), this.encodingUnencryptedHash), { key: hashKey, }); + return `0x${encryptedBuffer.toString(this.encodingEncrypted)}`; + } + + /** + * gets calls or answers from service contract + * + * @param {any} serviceContract smart contract instance or contract ID + * @param {string} type 'calls' or 'answers' + * @param {number} callId id of parent call (if retrieving answers) or null + * @param {number} count number of items to retrieve + * @param {number} offset skip this many entries + * @param {boolean} reverse fetch entries, starting with last entry + * @return {Promise} object with result info + */ + private async getEntries( + serviceContract: any, + type = 'calls' || 'answers', + callId?: number, + count = 10, + offset = 0, + reverse = false): Promise { + let result = { + entries: {}, + indices: [], + }; + if (type !== 'calls' && type !== 'answers') { + throw new Error(`unsupported service contract entry type: ${type}`); + } + let entryCount; + let queryOffset = offset; + if (reverse) { + entryCount = await (type === 'calls' ? this.getCallCount(serviceContract) : this.getAnswerCount(serviceContract, callId)); + queryOffset = Math.max(entryCount - offset - count, 0); + } + let itemsRetrieved = 0; + const resultsPerPage = 10; + const getResults = async (singleQueryOffset) => { + let queryResult; + if (type === 'calls') { + queryResult = await this.options.executor.executeContractCall(serviceContract, 'getCalls', singleQueryOffset); + } else { + queryResult = await this.options.executor.executeContractCall(serviceContract, 'getAnswers', callId, singleQueryOffset); + } + itemsRetrieved += resultsPerPage; + + for (let i = 0; i < queryResult.page.length; i++) { + const resultId = i + singleQueryOffset; + result.indices.push(resultId); + result.entries[resultId] = { + encryptedHash: queryResult.page[i], + }; + if (type === 'calls') { + result.entries[resultId].sharing = queryResult.sharings[i]; + } + } + + if (typeof entryCount === 'undefined') { + entryCount = parseInt(queryResult.totalCount, 10); + } + if (itemsRetrieved < count && itemsRetrieved < entryCount) { + // continue if items remaining + await getResults(queryOffset + resultsPerPage); + } + }; + await getResults(queryOffset); + // trim unneccessary or empty results + const limit = Math.min(count, entryCount - offset); + if (limit < result.indices.length) { + result.indices = result.indices.slice(0, limit); + } + if (reverse) { + result.indices.reverse(); + } + return result; + } + + /** + * convert number bytes32 string + * + * @param {number} number number to convert + * @return {string} bytes32 string with '0x' prefix + */ + private numberToBytes32(number: number): string { + return `0x${(number).toString(16).padStart(64, '0')}`; + } +} diff --git a/src/contracts/sharing.spec.ts b/src/contracts/sharing.spec.ts new file mode 100644 index 00000000..acd0a3dc --- /dev/null +++ b/src/contracts/sharing.spec.ts @@ -0,0 +1,463 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect, use } from 'chai'; +import chaiAsPromised = require('chai-as-promised'); + +import { + DfsInterface, + Executor, + Ipfs, + NameResolver, +} from '@evan.network/dbcp'; + +import { accounts } from '../test/accounts'; +import { config } from '../config'; +import { CryptoProvider } from '../encryption/crypto-provider'; +import { Sharing } from './sharing'; +import { sampleContext, TestUtils } from '../test/test-utils'; + +use(chaiAsPromised); + + +describe('Sharing handler', function() { + this.timeout(300000); + let executor: Executor; + let dfs: DfsInterface; + let nameResolver: NameResolver; + let sharing: Sharing; + let testAddress: string; + let web3; + let description; + let cryptoProvider: CryptoProvider; + + before(async () => { + web3 = TestUtils.getWeb3(); + executor = await TestUtils.getExecutor(web3); + dfs = await TestUtils.getIpfs(); + nameResolver = await TestUtils.getNameResolver(web3); + cryptoProvider = TestUtils.getCryptoProvider(); + description = await TestUtils.getDescription(web3, dfs); + sharing = new Sharing({ + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider, + description, + executor, + dfs, + keyProvider: TestUtils.getKeyProvider(), + nameResolver: nameResolver, + defaultCryptoAlgo: 'aes', + }); + testAddress = `barfoo.${nameResolver.getDomainName(config.nameResolver.domains.root)}`; + }); + + after(async () => { + await dfs.stop(); + web3.currentProvider.connection.close(); + }); + + + function runContractTests(isMultiShared) { + const contractName = !isMultiShared ? 'Shared' : 'MultiSharedTest'; + const sharingId = !isMultiShared ? null : `0x${Math.floor(Math.random() * 255 * 255 * 255).toString(16).padStart(64, '0')}`; + + it('should be able to add a sharing', async () => { + const randomSecret = `super secret; ${Math.random()}`; + const contract = await executor.createContract( + contractName, [], { from: accounts[0], gas: 500000, }); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', 0, randomSecret, null, false, sharingId); + }); + + it('should be able to get a sharing key', async () => { + const randomSecret = `super secret; ${Math.random()}`; + const contract = await executor.createContract( + contractName, [], { from: accounts[0], gas: 500000, }); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', 0, randomSecret, null, false, sharingId); + const key = await sharing.getKey(contract.options.address, accounts[1], '*', 0, sharingId); + expect(key).to.eq(randomSecret); + }); + + it('should be able to list all sharings', async () => { + const randomSecret = `super secret; ${Math.random()}`; + const contract = await executor.createContract( + contractName, [], { from: accounts[0], gas: 500000, }); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', 0, randomSecret, null, false, sharingId); + const sharings = await sharing.getSharings(contract.options.address, null, null, null, sharingId); + expect(sharings).not.to.be.undefined; + }); + + it('should be able to remove a sharing', async () => { + const randomSecret = `super secret; ${Math.random()}`; + const contract = await executor.createContract( + contractName, [], { from: accounts[0], gas: 500000, }); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', 0, randomSecret, null, false, sharingId); + let sharings = await sharing.getSharings(contract.options.address, null, null, null, sharingId); + expect(Object.keys(sharings).length).to.eq(1); + expect(Object.keys(sharings[nameResolver.soliditySha3(accounts[1])]).length).to.eq(1); + expect(Object.keys(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('*')]).length).to.eq(1); + expect(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('*')][0]).to.eq(randomSecret); + + await sharing.removeSharing(contract.options.address, accounts[0], accounts[1], '*', sharingId); + sharings = await sharing.getSharings(contract.options.address, null, null, null, sharingId); + expect(Object.keys(sharings).length).to.eq(0); + const key1 = await sharing.getKey(contract.options.address, accounts[1], '*', 0, sharingId); + expect(key1).to.be.undefined; + }); + + it('should be able to store sharings under a given context', async () => { + const randomSecret = `super secret; ${Math.random()}`; + const contract = await executor.createContract( + contractName, [], { from: accounts[0], gas: 500000, }); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', 0, randomSecret, sampleContext, false, sharingId); + const key = await sharing.getKey(contract.options.address, accounts[1], '*', 0, sharingId); + expect(key).to.eq(randomSecret); + + const contract2 = await executor.createContract( + contractName, [], { from: accounts[0], gas: 500000, }); + const unknownContext = 'I have not been added to any config'; + let err; + try { + await sharing.addSharing(contract2.options.address, accounts[0], accounts[1], '*', 0, randomSecret, unknownContext, false, sharingId); + const notWorkingKey = await sharing.getKey(contract2.options.address, accounts[1], '*', 0, sharingId); + } catch (ex) { + err = ex; + } + expect(err).to.be.an('error'); + }); + + it('should be able to set different keys for different properties', async () => { + const randomSecret = [ + `super secret; ${Math.random()}`, + `super secret; ${Math.random()}`, + `super secret; ${Math.random()}`, + ]; + const contract = await executor.createContract(contractName, [], { from: accounts[0], gas: 500000, }); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], 'sectionOne', 0, randomSecret[0], null, false, sharingId); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], 'sectionTwo', 0, randomSecret[1], null, false, sharingId); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], 'sectionThree', 0, randomSecret[2], null, false, sharingId); + let sharings = await sharing.getSharings(contract.options.address, null, null, null, sharingId); + // object checks + expect(Object.keys(sharings)).to.deep.eq([nameResolver.soliditySha3(accounts[1])]); + expect(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('sectionOne')]).to.deep.eq({ '0': randomSecret[0], }); + expect(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('sectionTwo')]).to.deep.eq({ '0': randomSecret[1], }); + expect(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('sectionThree')]).to.deep.eq({ '0': randomSecret[2], }); + // getKey checks + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 1000, sharingId)).to.eq(randomSecret[0]); + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionTwo', 1000, sharingId)).to.eq(randomSecret[1]); + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionThree', 1000, sharingId)).to.eq(randomSecret[2]); + }); + + it('should be able to set different keys for different block numbers', async () => { + const randomSecret = [ + `super secret; ${Math.random()}`, + `super secret; ${Math.random()}`, + ]; + const contract = await executor.createContract(contractName, [], { from: accounts[0], gas: 500000, }); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], 'sectionOne', 100, randomSecret[0], null, false, sharingId); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], 'sectionOne', 200, randomSecret[1], null, false, sharingId); + let sharings = await sharing.getSharings(contract.options.address, null, null, null, sharingId); + // object checks + expect(Object.keys(sharings)).to.deep.eq([nameResolver.soliditySha3(accounts[1])]); + expect(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('sectionOne')]['100']).to.eq(randomSecret[0]); + expect(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('sectionOne')]['200']).to.eq(randomSecret[1]); + // getKey checks + // exactly in block 0 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 0, sharingId)).to.eq(undefined); + // between before block 100 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 12, sharingId)).to.eq(undefined); + // exactly in block 100 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 100, sharingId)).to.eq(randomSecret[0]); + // between block 100 and block 200 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 123, sharingId)).to.eq(randomSecret[0]); + // exactly in block 200 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 200, sharingId)).to.eq(randomSecret[1]); + // after block 200 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 234, sharingId)).to.eq(randomSecret[1]); + }); + + it('should not be possible to get keys of another user', async () => { + const randomSecret = [ + `super secret; ${Math.random()}`, + `super secret; ${Math.random()}`, + ]; + const contract = await executor.createContract(contractName, [], { from: accounts[0], gas: 500000, }); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], 'sectionOne', 0, randomSecret[0], null, false, sharingId); + await sharing.addSharing(contract.options.address, accounts[0], accounts[0], 'sectionOne', 0, randomSecret[1], null, false, sharingId); + let sharings = await sharing.getSharings(contract.options.address, null, null, null, sharingId); + // object checks + expect(Object.keys(sharings).sort()).to.deep.eq([nameResolver.soliditySha3(accounts[1]), nameResolver.soliditySha3(accounts[0])].sort()); + expect(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('sectionOne')]).to.deep.eq({ '0': randomSecret[0], }); + expect(sharings[nameResolver.soliditySha3(accounts[0])][nameResolver.soliditySha3('sectionOne')]).to.deep.eq({ '0': randomSecret[1], }); + // getKey checks + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 0, sharingId)).to.eq(randomSecret[0]); + expect(await sharing.getKey(contract.options.address, accounts[0], 'sectionOne', 0, sharingId)).to.eq(randomSecret[1]); + }); + + it('should be able to change keys for a section', async () => { + const randomSecret = [ + `super secret; ${Math.random()}`, + `super secret; ${Math.random()}`, + ]; + const contract = await executor.createContract(contractName, [], { from: accounts[0], gas: 500000, }); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], 'sectionOne', 100, randomSecret[0], null, false, sharingId); + let sharings = await sharing.getSharings(contract.options.address, null, null, null, sharingId); + // object checks + expect(Object.keys(sharings)).to.deep.eq([nameResolver.soliditySha3(accounts[1])]); + expect(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('sectionOne')]['100']).to.eq(randomSecret[0]); + expect(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('sectionOne')]['200']).to.eq(undefined); + // initial keys setup + // exactly in block 0 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 0, sharingId)).to.eq(undefined); + // between before block 100 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 12, sharingId)).to.eq(undefined); + // exactly in block 100 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 100, sharingId)).to.eq(randomSecret[0]); + // between block 100 and block 200 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 123, sharingId)).to.eq(randomSecret[0]); + // exactly in block 200 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 200, sharingId)).to.eq(randomSecret[0]); + // after block 200 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 234, sharingId)).to.eq(randomSecret[0]); + + // add new key, valid in and after block 200 + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], 'sectionOne', 200, randomSecret[1], null, false, sharingId); + // sharing.sharingCache = {}; + sharings = await sharing.getSharings(contract.options.address, null, null, null, sharingId); + expect(Object.keys(sharings)).to.deep.eq([nameResolver.soliditySha3(accounts[1])]); + expect(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('sectionOne')]['100']).to.eq(randomSecret[0]); + expect(sharings[nameResolver.soliditySha3(accounts[1])][nameResolver.soliditySha3('sectionOne')]['200']).to.eq(randomSecret[1]); + // exactly in block 0 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 0, sharingId)).to.eq(undefined); + // between before block 100 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 12, sharingId)).to.eq(undefined); + // exactly in block 100 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 100, sharingId)).to.eq(randomSecret[0]); + // between block 100 and block 200 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 123, sharingId)).to.eq(randomSecret[0]); + // exactly in block 200 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 200, sharingId)).to.eq(randomSecret[1]); + // after block 200 + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', 234, sharingId)).to.eq(randomSecret[1]); + }); + + it('should be able to get lates key if omitting block argument', async () => { + const randomSecret = [ + `super secret; ${Math.random()}`, + `super secret; ${Math.random()}`, + `super secret; ${Math.random()}`, + ]; + const contract = await executor.createContract(contractName, [], { from: accounts[0], gas: 500000, }); + // if no sharings added, key is undefined + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', Number.MAX_SAFE_INTEGER, sharingId)).to.eq(undefined); + // add a key, this will be the latest key + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], 'sectionOne', 100, randomSecret[0], null, false, sharingId); + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', Number.MAX_SAFE_INTEGER, sharingId)).to.eq(randomSecret[0]); + // add a key before the first one --> latest key should not change + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], 'sectionOne', 50, randomSecret[1], null, false, sharingId); + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', Number.MAX_SAFE_INTEGER, sharingId)).to.eq(randomSecret[0]); + // add a key after the first one --> latest key should change + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], 'sectionOne', 150, randomSecret[2], null, false, sharingId); + expect(await sharing.getKey(contract.options.address, accounts[1], 'sectionOne', Number.MAX_SAFE_INTEGER, sharingId)).to.eq(randomSecret[2]); + }); + + it('should be able to share hash keys', async () => { + const contract = await executor.createContract( + contractName, [], { from: accounts[0], gas: 500000, }); + const hashCryptor = cryptoProvider.getCryptorByCryptoAlgo('aesEcb'); + const hashKey = await hashCryptor.generateKey(); + await sharing.ensureHashKey(contract.options.address, accounts[0], accounts[0], hashKey, null, sharingId); + await sharing.ensureHashKey(contract.options.address, accounts[0], accounts[1], hashKey, null, sharingId); + const key = await sharing.getHashKey(contract.options.address, accounts[1], sharingId); + expect(key).to.eq(hashKey); + }); + + it('should be able to share hash implicitely when sharing other keys', async () => { + const contract = await executor.createContract( + contractName, [], { from: accounts[0], gas: 500000, }); + const hashCryptor = cryptoProvider.getCryptorByCryptoAlgo('aesEcb'); + const hashKey = await hashCryptor.generateKey(); + await sharing.ensureHashKey(contract.options.address, accounts[0], accounts[0], hashKey, null, sharingId); + // await sharing.ensureHashKey(contract.options.address, accounts[0], accounts[1], hashKey); + const randomSecret = `super secret; ${Math.random()}`; + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', 0, randomSecret, null, false, sharingId); + // check shared key + const sharedKey = await sharing.getKey(contract.options.address, accounts[1], '*', 0, sharingId); + expect(sharedKey).to.eq(randomSecret); + // check hash key + const hashKeyRetrieved = await sharing.getHashKey(contract.options.address, accounts[1], sharingId); + expect(hashKeyRetrieved).to.eq(hashKey); + }); + + describe('when adding preloaded sharing hashes', () => { + it('should be able to work with correct added to the hash cache', async () => { + const randomSecret = `super secret; ${Math.random()}`; + // create a contract with a sharing + const contract = await executor.createContract( + contractName, [], { from: accounts[0], gas: 500000, }); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', 0, randomSecret, null, false, sharingId); + + // create new sharing with empty cache + const newSharing = new Sharing({ + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider, + description, + executor, + dfs, + keyProvider: TestUtils.getKeyProvider(), + nameResolver: nameResolver, + defaultCryptoAlgo: 'aes', + }); + + // add sharing key + const sharingHash = await (sharingId ? + executor.executeContractCall(contract, 'multiSharings', sharingId) : + executor.executeContractCall(contract, 'sharing') + ); + newSharing.addHashToCache(contract.options.address, sharingHash, sharingId); + const key = await newSharing.getKey(contract.options.address, accounts[1], '*', 0, sharingId); + expect(key).to.eq(randomSecret); + }); + + it('should fail to load sharing, when added hashes contain errors', async () => { + // start the same way as in last test --> not added to cache by side effects + const randomSecret = `super secret; ${Math.random()}`; + // create a contract with a sharing + const contract = await executor.createContract( + contractName, [], { from: accounts[0], gas: 500000, }); + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', 0, randomSecret, null, false, sharingId); + + // create new sharing with empty cache + const newSharing = new Sharing({ + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider, + description, + executor, + dfs, + keyProvider: TestUtils.getKeyProvider(), + nameResolver: nameResolver, + defaultCryptoAlgo: 'aes', + }); + + // add random stuff, will trigger timeout and won't retrieve sharing + const sharingHash = + `0x${[...Array(64)].map(() => (Math.random() * 16).toString(16).split('.')[0]).join('')}`; + newSharing.addHashToCache(contract.options.address, sharingHash, sharingId); + const keyPromise = newSharing.getKey(contract.options.address, accounts[1], '*', 0, sharingId); + await expect(keyPromise).to.be.rejected; + }); + }); + } + + describe('for contracts that inherit from "Shared"', function() { + runContractTests(false); + }); + + describe('for contracts that inherit from "MultiShared"', function() { + runContractTests(true); + + it('can manage multiple sharings autonomously', async () => { + const count = 3; + const sharingIds = [...Array(count)].map(() => `0x${Math.floor(Math.random() * 255 * 255 * 255).toString(16).padStart(64, '0')}`); + const randomSecrets = [...Array(count)].map(() => `super secret; ${Math.random()}`); + const contract = await executor.createContract('MultiSharedTest', [], { from: accounts[0], gas: 500000, }); + for (let i = 0; i < count; i++) { + await sharing.addSharing(contract.options.address, accounts[0], accounts[1], '*', 0, randomSecrets[i], null, false, sharingIds[i]); + } + for (let i = 0; i < count; i++) { + const key = await sharing.getKey(contract.options.address, accounts[1], '*', 0, sharingIds[i]); + expect(key).to.eq(randomSecrets[i]); + } + }); + }); + + describe('for ENS descriptions', function() { + beforeEach(async () => { + // empty description + await description.setDescriptionToEns( + testAddress, { + public: { + name: 'sharing test', + description: 'sharing test', + author: 'sharing author', + version: '0.0.1', + }, + }, + accounts[1] + ); + }); + + it('should be able to add a sharing', async () => { + const randomSecret = `super secret; ${Math.random()}`; + await sharing.addSharing(testAddress, accounts[1], accounts[0], '*', 0, randomSecret); + }); + + it('should be able to get a sharing key', async () => { + const randomSecret = `super secret; ${Math.random()}`; + await sharing.addSharing(testAddress, accounts[1], accounts[0], '*', 0, randomSecret); + const key = await sharing.getKey(testAddress, accounts[0], '*', 0); + expect(key).to.eq(randomSecret); + }); + + it('should be able to list all sharings', async () => { + const randomSecret = `super secret; ${Math.random()}`; + await sharing.addSharing(testAddress, accounts[1], accounts[0], '*', 0, randomSecret); + const sharings = await sharing.getSharings(testAddress); + expect(sharings).not.to.be.undefined; + }); + + it('should be able to remove a sharing', async () => { + const randomSecret = `super secret; ${Math.random()}`; + await sharing.addSharing(testAddress, accounts[1], accounts[0], '*', 0, randomSecret); + let sharings = await sharing.getSharings(testAddress); + expect(Object.keys(sharings).length).to.eq(1); + expect(sharings[nameResolver.soliditySha3(accounts[0])][nameResolver.soliditySha3('*')][0]).to.eq(randomSecret); + await sharing.removeSharing(testAddress, accounts[1], accounts[0], '*'); + sharings = await sharing.getSharings(testAddress); + expect(Object.keys(sharings).length).to.eq(0); + const key1 = await sharing.getKey(testAddress, accounts[0], '*', 0); + expect(key1).to.be.undefined; + }); + + it('should be able to store sharings under a given context', async () => { + const randomSecret = `super secret; ${Math.random()}`; + await sharing.addSharing(testAddress, accounts[1], accounts[0], '*', 0, randomSecret, sampleContext); + const key = await sharing.getKey(testAddress, accounts[0], '*', 0); + expect(key).to.eq(randomSecret); + + const unknownContext = 'I have not been added to any config'; + let err; + try { + await sharing.addSharing(`foo${testAddress}`, accounts[1], accounts[0], '*', 0, randomSecret, unknownContext); + const notWorkingKey = await sharing.getKey(`foo${testAddress}`, accounts[0], '*', 0); + } catch (ex) { + err = ex; + } + expect(err).to.be.an('error'); + }); + }); +}); diff --git a/src/contracts/sharing.ts b/src/contracts/sharing.ts new file mode 100644 index 00000000..a907c5a6 --- /dev/null +++ b/src/contracts/sharing.ts @@ -0,0 +1,526 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import { + ContractLoader, + Cryptor, + Description, + Executor, + DfsInterface, + KeyProvider, + NameResolver, + Logger, + LoggerOptions, +} from '@evan.network/dbcp'; + +import { CryptoProvider } from '../encryption/crypto-provider'; + + +// constant hash: this.options.nameResolver.soliditySha3('*') +const catchAllSection = '0x04994f67dc55b09e814ab7ffc8df3686b4afb2bb53e60eae97ef043fe03fb829'; + +export interface SharingOptions extends LoggerOptions { + contractLoader: ContractLoader; + cryptoProvider: CryptoProvider; + description: Description; + executor: Executor; + dfs: DfsInterface; + keyProvider: KeyProvider; + nameResolver: NameResolver; + defaultCryptoAlgo?: string; +} + +/** + * Sharing helper. Can add Sharings to contract addresses and ENS endpoints + * + * @class Sharing (name) + */ +export class Sharing extends Logger { + options: SharingOptions; + + private readonly encodingUnencrypted = 'binary'; + private readonly encodingEncrypted = 'hex'; + private sharingCache = {}; + private hashCache = {}; + + constructor(options: SharingOptions) { + super(options); + this.options = Object.assign({ + defaultCryptoAlgo: 'aes', + }, options); + } + + /** + * add a sharing to a contract or an ENS address + * + * @param {string} address contract address or ENS address + * @param {string} originator Ethereum account id of the sharing user + * @param {string} partner Ethereum account id for which key shall be added + * @param {string} section data section the key is intended for or '*' + * @param {number|string} block starting with this block, the key is valid + * @param {string} sharingKey key to share + * @param {string} context context to share key in + * @param {boolean} isHashKey indicates if given key already is a hash key + * @param {string} sharingId id of a sharing (when multi-sharings is used) + * @return {Promise} resolved when done + */ + public async addSharing( + address: string, + originator: string, + partner: string, + section: string, + block: number|string, + sharingKey: string, + context?: string, + isHashKey = false, + sharingId: string = null, + ): Promise { + let sharings; + let contract; + let description; + + // load + if (address.startsWith('0x')) { + // encrypted sharings from contract + if (sharingId) { + contract = this.options.contractLoader.loadContract('MultiShared', address); + } else { + contract = this.options.contractLoader.loadContract('Shared', address); + } + sharings = await this.getSharingsFromContract(contract, sharingId); + } else { + description = await this.options.description.getDescriptionFromEns(address); + // ensure sharings + if (description && description.public && description.public.sharings) { + sharings = description.public.sharings; + } else { + sharings = {}; + } + // ensure description + if (!description) { + description = {}; + } + if (!description.public) { + description.public = {}; + } + } + + // extend sharings + sharings = await this.extendSharings(sharings, originator, partner, section, block, sharingKey, context); + + // if not already sharing a hash key + if (!isHashKey) { + // check, if partner already has a hash key + const setHashKey = await this.getHashKey(address, partner, sharingId); + if (!setHashKey) { + // retrieve hash key with originator account, extend hash key if required + const hashKey = await this.getHashKey(address, originator, sharingId); + if (hashKey) { + await this.extendSharings(sharings, originator, partner, '*', 'hashKey', hashKey, context); + } else { + this.log('originator does not have access to a hash key, skipping setting it', 'debug'); + } + } else { + this.log('partners hash key already set, skipping setting it', 'debug'); + } + } + + // save updated sharings + if (address.startsWith('0x')) { + // upload to ipfs and hash + const updatedHash = await this.options.dfs.add( + 'sharing', Buffer.from(JSON.stringify(sharings), this.encodingUnencrypted)); + // save to contract + if (sharingId) { + await this.options.executor.executeContractTransaction( + contract, 'setMultiSharing', { from: originator, autoGas: 1.1, }, sharingId, updatedHash); + } else { + await this.options.executor.executeContractTransaction( + contract, 'setSharing', { from: originator, autoGas: 1.1, }, updatedHash); + } + if (this.hashCache[contract.options.address] && this.hashCache[contract.options.address][sharingId]) { + delete this.hashCache[contract.options.address][sharingId]; + } + } else { + // save to ens + description.public.sharings = sharings; + await this.options.description.setDescriptionToEns(address, description, originator); + } + } + + /** + * add a hash to to cache, can be used to speed up sharing key retrieval, when sharings hash is + * already known + * + * @param {string} address contract address + * @param {string} sharingHash bytes32 hash of a sharing + * @param {string} sharingId id of a multisharing + */ + public addHashToCache(address: string, sharingHash: string, sharingId: string = null): void { + if (!this.hashCache[address]) { + this.hashCache[address] = {}; + } + this.hashCache[address][sharingId] = sharingHash; + } + + /** + * clear caches and fetch new hashes and sharing on next request + * + * @return {void} resolved when done + */ + public clearCache() { + this.sharingCache = {}; + this.hashCache = {}; + } + + /** + * give hash key "hashKey" to account "partner", if this account does not have a hash key already + * + * @param {string} address contract adress + * @param {string} originator executing users account id + * @param {string} partner account id of a contract participant + * @param {string} hashKey key for DFS hashes + * @param {string} context (optional) context for encryption + * @param {string} sharingId id of a sharing (when multi-sharings is used) + * @return {Promise} resolved when done + */ + public async ensureHashKey( + address: string, originator: string, partner: string, hashKey: string, context?: string, sharingId: string = null): Promise { + const setHashKey = await this.getHashKey(address, partner, sharingId); + if (!setHashKey) { + return this.addSharing(address, originator, partner, '*', 'hashKey', hashKey, context, true, sharingId); + } + } + + /** + * extend an existing sharing info with given key; this is done on a sharings object and does not + * perform a transaction on its own + * + * @param {any} sharings a sharings info + * @param {string} originator Ethereum account id of the sharing user + * @param {string} partner Ethereum account id for which key shall be added + * @param {string} section data section the key is intended for or '*' + * @param {number|string} block starting with this block, the key is valid + * @param {string} sharingKey key to share + * @param {string} context context to share key in + * @return {Promise} updated sharings info + */ + public async extendSharings( + sharings: any, + originator: string, + partner: string, + section: string, + block: number|string, + sharingKey: string, + context?: string): Promise { + // encrypt sharing key + const originatorHash = this.options.nameResolver.soliditySha3(originator); + const partnerHash = this.options.nameResolver.soliditySha3(partner); + const sectionHash = this.options.nameResolver.soliditySha3(section); + const edgeKey = context ? this.options.nameResolver.soliditySha3(context) : + this.options.nameResolver.soliditySha3.apply(this.options.nameResolver, [originatorHash, partnerHash].sort()); + const cryptor = this.options.cryptoProvider.getCryptorByCryptoAlgo(this.options.defaultCryptoAlgo); + const cryptoInfo = cryptor.getCryptoInfo(edgeKey); + const encryptionKey = await this.options.keyProvider.getKey(cryptoInfo); + if (!encryptionKey) { + throw new Error(`could not extend sharings, no key found for "${originatorHash}" to "${partnerHash}"` + + `${context ? (' in context "' + context + '"') : ''}`); + } + const encryptedBuffer = await cryptor.encrypt(sharingKey, { key: encryptionKey, }); + sharings[partnerHash] = sharings[partnerHash] ? sharings[partnerHash] : {}; + sharings[partnerHash][sectionHash] = sharings[partnerHash][sectionHash] ? sharings[partnerHash][sectionHash] : {}; + sharings[partnerHash][sectionHash][block] = { + private: encryptedBuffer.toString(this.encodingEncrypted), + cryptoInfo, + }; + return sharings; + } + + /** + * returns an accounts key for file hashes + * + * @param {string} address contract address or ENS address + * @param {string} partner Ethereum account id for which key shall be retrieved + * @param {string} sharingId id of a sharing (when multi-sharings is used) + * @return {Promise} matching key + */ + public async getHashKey(address: string, partner: string, sharingId: string = null): Promise { + return this.getKey(address, partner, '*', 'hashKey', sharingId); + } + + /** + * get sharing from a contract, if _partner, _section, _block matches + * + * @param {string} address address of a contract or an ENS address + * @param {string} _partner Ethereum account id for which key shall be retrieved + * @param {string} _section data section the key is intended for or '*' + * @param {number} _block starting with this block, the key is valid + * @return {Promise} sharings as an object. + */ + public async getSharings(address: string, _partner?: string, _section?: string, _block?: number, sharingId: string = null): Promise { + let sharings; + if (address.startsWith('0x')) { + // encrypted sharings from contract + let contract; + if (sharingId) { + contract = this.options.contractLoader.loadContract('MultiShared', address); + } else { + contract = this.options.contractLoader.loadContract('Shared', address); + } + sharings = await this.getSharingsFromContract(contract, sharingId); + } else { + // enrypted sharings from ens + const description = await this.options.description.getDescriptionFromEns(address); + if (description && description.public && description.public.sharings) { + sharings = description.public.sharings; + } else { + sharings = {}; + } + } + return this.decryptSharings(sharings, _partner, _section, _block); + } + + /** + * get a content key from the sharing of a contract + * + * @param {string} address contract address or ENS address + * @param {string} partner Ethereum account id for which key shall be retrieved + * @param {string} section data section the key is intended for or '*' + * @param {number|string} block starting with this block, the key is valid + * @param {string} sharingId id of a sharing (when multi-sharings is used) + * @return {Promise} matching key + */ + public async getKey( + address: string, + partner: string, + section: string, + block: number|string = Number.MAX_SAFE_INTEGER, + sharingId: string = null): Promise { + const partnerHash = this.options.nameResolver.soliditySha3(partner); + const sectionHash = this.options.nameResolver.soliditySha3(section || '*'); + if (!this.sharingCache[address] || + !this.sharingCache[address][sharingId] || + !this.sharingCache[address][sharingId][partnerHash] || + !this.sharingCache[address][sharingId][partnerHash][sectionHash] || + !this.sharingCache[address][sharingId][partnerHash][sectionHash][block]) { + if (!this.sharingCache[address]) { + this.sharingCache[address] = {}; + } + this.sharingCache[address][sharingId] = await this.getSharings(address, null, null, null, sharingId); + } + // check partner + const partnerSections = this.sharingCache[address][sharingId][partnerHash]; + if (!partnerSections) { + this.log(`could not find any keys for "${address}" and partner "${partner}"`, 'debug'); + } else { + // check section + let sectionBlocks; + if (partnerSections[sectionHash]) { + sectionBlocks = partnerSections[sectionHash]; + } else if (partnerSections[catchAllSection]) { + sectionBlocks = partnerSections[catchAllSection]; + } else { + this.log(`could not find section keys for contract "${address}" and partner "${partner}" for section "${section}"`, 'debug'); + return undefined; + } + if (typeof block === 'number') { + // look for matching block + // the block key, that was set last before encrypting (encryption happened after block ${block}) + const blocks = Object.keys(sectionBlocks).map(blockNr => parseInt(blockNr, 10)); + const lteBlocks = blocks.filter(blockNr => blockNr <= block); + if (!lteBlocks.length) { + this.log(`could not find key for contract "${address}" and partner "${partner}" for section "${section}", that is new enough`, 'debug'); + } else { + // oldest key block, that is younger than the context block + const matchingBlock = lteBlocks[lteBlocks.length - 1]; + return sectionBlocks[matchingBlock]; + } + } else { + // use drectly matching block (e.g. for 'hashKey') + return sectionBlocks[block]; + } + + } + } + + /** + * remove a sharing key from a contract with sharing info + * + * @param {string} address contract address or ENS address + * @param {string} originator Ethereum account id of the sharing user + * @param {string} partner Ethereum account id for which key shall be removed + * @param {string} section data section of the key + * @param {string} sharingId id of a sharing (when multi-sharings is used) + * @return {Promise} resolved when done + */ + public async removeSharing( + address: string, + originator: string, + partner: string, + section: string, + sharingId: string = null): Promise { + const partnerHash = this.options.nameResolver.soliditySha3(partner); + const sectionHash = this.options.nameResolver.soliditySha3(section); + let sharings; + let contract; + let description; + // load + if (address.startsWith('0x')) { + if (sharingId) { + contract = this.options.contractLoader.loadContract('MultiShared', address); + } else { + contract = this.options.contractLoader.loadContract('Shared', address); + } + sharings = await this.getSharingsFromContract(contract, sharingId); + } else { + description = await this.options.description.getDescriptionFromEns(address); + // ensure sharings + if (description && description.public && description.public.sharings) { + sharings = description.public.sharings; + } else { + sharings = {}; + } + // ensure description + if (!description) { + description = {}; + } + if (!description.public) { + description.public = {}; + } + } + if (sharings[partnerHash] && sharings[partnerHash][sectionHash]) { + // delete entry + delete sharings[partnerHash][sectionHash]; + // remove from cache if already cached + if (this.sharingCache[address] && + this.sharingCache[address][sharingId] && + this.sharingCache[address][sharingId][partnerHash] && + this.sharingCache[address][sharingId][partnerHash][sectionHash]) { + delete this.sharingCache[address][sharingId][partnerHash][sectionHash]; + } + // save + if (address.startsWith('0x')) { + const updatedHash = await this.options.dfs.add( + 'sharing', Buffer.from(JSON.stringify(sharings), this.encodingUnencrypted)); + if (sharingId) { + await this.options.executor.executeContractTransaction( + contract, 'setMultiSharing', { from: originator, autoGas: 1.1, }, sharingId, updatedHash); + } else { + await this.options.executor.executeContractTransaction( + contract, 'setSharing', { from: originator, autoGas: 1.1, }, updatedHash); + } + if (this.hashCache[contract.options.address] && this.hashCache[contract.options.address][sharingId]) { + delete this.hashCache[contract.options.address][sharingId]; + } + } else { + // save to ens + description.public.sharings = sharings; + await this.options.description.setDescriptionToEns(address, description, originator); + } + } + } + + /** + * get sharings from smart contract + * + * @param {any} contract contract with sharing info + * @param {string} sharingId id of a sharing in mutlisharings + * @return {Promise} sharings object + */ + private async getSharingsFromContract(contract: any, sharingId: string = null): Promise { + let result = {}; + let sharings; + let sharingHash; + // use preloaded hashes if available + if (!this.hashCache[contract.options.address] || + !this.hashCache[contract.options.address][sharingId] || + this.hashCache[contract.options.address][sharingId] === '0x0000000000000000000000000000000000000000000000000000000000000000') { + if (!this.hashCache[contract.options.address]) { + this.hashCache[contract.options.address] = {}; + } + if (sharingId) { + this.hashCache[contract.options.address][sharingId] = + await this.options.executor.executeContractCall(contract, 'multiSharings', sharingId); + } else { + this.hashCache[contract.options.address][sharingId] = + await this.options.executor.executeContractCall(contract, 'sharing'); + } + } + sharingHash = this.hashCache[contract.options.address][sharingId]; + + if (sharingHash !== '0x0000000000000000000000000000000000000000000000000000000000000000') { + const buffer = await this.options.dfs.get(sharingHash); + if (!buffer) { + throw new Error(`could not get sharings from hash ${sharingHash}`); + } + sharings = JSON.parse(buffer.toString()); + } else { + sharings = {}; + } + return sharings; + } + + private async decryptSharings(sharings: any, _partner?: string, _section?: string, _block?: number): Promise { + let result = {}; + const _partnerHash = _partner ? this.options.nameResolver.soliditySha3(_partner) : null; + for (let partnerHashKey of Object.keys(sharings)) { + if (_partnerHash && _partnerHash !== partnerHashKey) { + continue; + } + result[partnerHashKey] = {}; + const partner = sharings[partnerHashKey]; + const _sectionHash = _section ? this.options.nameResolver.soliditySha3(_section) : null; + for (let sectionHashKey of Object.keys(partner)) { + if (_sectionHash && _sectionHash !== sectionHashKey) { + continue; + } + result[partnerHashKey][sectionHashKey] = {}; + const section = partner[sectionHashKey]; + for (let blockKey of Object.keys(section)) { + if (_block && _block !== parseInt(blockKey, 10)) { + continue; + } + const block = section[blockKey]; + const cryptor = this.options.cryptoProvider.getCryptorByCryptoInfo(block.cryptoInfo); + const decryptKey = await this.options.keyProvider.getKey(block.cryptoInfo); + if (decryptKey) { + const decrypted = await cryptor.decrypt( + Buffer.from(block.private, this.encodingEncrypted), { key: decryptKey, }); + result[partnerHashKey][sectionHashKey][blockKey] = decrypted.toString(this.encodingUnencrypted); + } + } + if (!Object.keys(result[partnerHashKey][sectionHashKey]).length) { + delete result[partnerHashKey][sectionHashKey] + } + } + if (!Object.keys(result[partnerHashKey]).length) { + delete result[partnerHashKey]; + } + } + return result; + } +} diff --git a/src/dfs/ipld.spec.ts b/src/dfs/ipld.spec.ts new file mode 100644 index 00000000..e1252c5e --- /dev/null +++ b/src/dfs/ipld.spec.ts @@ -0,0 +1,591 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect } from 'chai'; +import bs58 = require('bs58'); +import Web3 = require('web3'); + +import { + Envelope, + KeyProvider, +} from '@evan.network/dbcp'; + +import { accounts } from '../test/accounts'; +import { Aes } from '../encryption/aes'; +import { config } from '../config'; +import { CryptoProvider } from '../encryption/crypto-provider'; +import { Ipld } from './ipld' +import { TestUtils } from '../test/test-utils' + +const sampleKey = '346c22768f84f3050f5c94cec98349b3c5cbfa0b7315304e13647a49181fd1ef'; +let keyProvider; + +describe('IPLD handler', function() { + this.timeout(300000); + let node; + let ipld: Ipld; + let ipfs; + let cryptoProvider: CryptoProvider; + let helperWeb3 = new Web3(null); + + before(async () => { + // create new ipld handler on ipfs node + ipfs = await TestUtils.getIpfs(); + cryptoProvider = TestUtils.getCryptoProvider(); + }); + + beforeEach(async () => { + // create new ipld handler on ipfs node + ipld = await TestUtils.getIpld(ipfs); + }); + + after(async () => { + // create new ipld handler on ipfs node + await ipfs.stop(); + }); + + describe('when creating a graph', () => { + it('should return an IPFS file hash with 32 bytes length when storing', async() => { + const sampleObject = { + personalInfo: { + firstName: 'eris', + }, + }; + + const stored = await ipld.store(Object.assign({}, sampleObject)); + expect(stored).to.match(/0x[0-9a-f]{64}/); + }); + + it('should be able to create a simple graph, retrieve and check the entire graph', async () => { + const sampleObject = { + personalInfo: { + firstName: 'eris', + }, + }; + + const stored = await ipld.store(Object.assign({}, sampleObject)); + const loaded = await ipld.getLinkedGraph(stored, ''); + expect(loaded).not.to.be.undefined; + Ipld.purgeCryptoInfo(loaded); + expect(JSON.stringify(loaded)).to.eq(JSON.stringify(sampleObject)); + }); + + it('should be able to handle special characters', async () => { + const sampleObject = { + personalInfo: { + firstName: 'Snörre', + }, + }; + + const stored = await ipld.store(Object.assign({}, sampleObject)); + const loaded = await ipld.getLinkedGraph(stored, ''); + expect(loaded).not.to.be.undefined; + Ipld.purgeCryptoInfo(loaded); + expect(JSON.stringify(loaded)).to.eq(JSON.stringify(sampleObject)); + }); + }); + + describe('when attaching nodes', () => { + it('should be able to attach a subtree to a simple tree', async () => { + const sampleObject = { + personalInfo: { + firstName: 'eris', + }, + }; + const sub = { + contracts: ['0x01', '0x02', '0x03'] + }; + + const extended = await ipld.set(sampleObject, 'dapps', sub); + const extendedstored = await ipld.store(Object.assign({}, extended)); + const loaded = await ipld.getLinkedGraph(extendedstored, ''); + + // unlinked (root data) + expect(loaded).not.to.be.undefined; + expect(loaded).to.haveOwnProperty('personalInfo'); + expect(loaded.personalInfo).to.haveOwnProperty('firstName'); + expect(loaded.personalInfo.firstName).to.eq('eris'); + // unlinked (root data) structure + expect(loaded.dapps).not.to.be.undefined; + expect(loaded.dapps).to.haveOwnProperty('/'); + expect(Buffer.isBuffer(loaded.dapps['/'])).to.be.true; + // linked (root data) access + const subLoaded = await ipld.getLinkedGraph(extendedstored, 'dapps'); + expect(subLoaded).not.to.be.undefined; + expect(Array.isArray(subLoaded.contracts)).to.be.true; + expect(subLoaded.contracts.length).to.eq(sub.contracts.length); + }); + + it('should be able to attach a subtree to a nested tree', async () => { + const sampleObject = { + personalInfo: { + firstName: 'eris', + }, + }; + const sub = { + contracts: ['0x01', '0x02', '0x03'] + }; + const subSub = { + contracts: '0x02', + }; + const stored = await ipld.store(Object.assign({}, sampleObject)); + const loadedStored = await ipld.getLinkedGraph(stored, ''); + + // add lv1 + const plusSub = await ipld.set(sampleObject, 'dapps', sub); + const plusSubstored = await ipld.store(Object.assign({}, plusSub)); + const loadedSub = await ipld.getLinkedGraph(plusSubstored, ''); + + // add lv2 + const plusSubSub = await ipld.set(loadedSub, 'dapps/favorites', subSub); + const plusSubSubstored = await ipld.store(Object.assign({}, plusSubSub)); + const loadedFull = await ipld.getLinkedGraph(plusSubSubstored, ''); + + // unlinked (root data) + expect(loadedFull).not.to.be.undefined; + expect(loadedFull).to.haveOwnProperty('personalInfo'); + expect(loadedFull.personalInfo).to.haveOwnProperty('firstName'); + expect(loadedFull.personalInfo.firstName).to.eq('eris'); + // unlinked (root data) acces + const subLoaded = await ipld.getLinkedGraph(plusSubSubstored, 'dapps'); + expect(subLoaded).not.to.be.undefined; + expect(Array.isArray(subLoaded.contracts)).to.be.true; + expect(subLoaded.contracts.length).to.eq(sub.contracts.length); + // unlinked (root data) access + const subSubLoaded = await ipld.getLinkedGraph(plusSubSubstored, 'dapps/favorites'); + expect(subSubLoaded).not.to.be.undefined; + expect(subSubLoaded).to.haveOwnProperty('contracts'); + expect(subSubLoaded.contracts).to.eq('0x02'); + }); + + it('should be able to attach a subtree with a different encryption than the main branch', async () => { + const sampleObject = { + personalInfo: { + firstName: 'eris', + }, + }; + const sub = { + contracts: ['0x01', '0x02', '0x03'] + }; + + const cryptor = cryptoProvider.getCryptorByCryptoAlgo('aes'); + const cryptoInfo = cryptor.getCryptoInfo(helperWeb3.utils.soliditySha3('context sample')); + const extended = await ipld.set(sampleObject, 'dapps', sub, false, cryptoInfo); + const extendedstored = await ipld.store(Object.assign({}, extended)); + const loaded = await ipld.getLinkedGraph(extendedstored, ''); + + // unlinked (root data) + expect(loaded).not.to.be.undefined; + expect(loaded).to.haveOwnProperty('personalInfo'); + expect(loaded.personalInfo).to.haveOwnProperty('firstName'); + expect(loaded.personalInfo.firstName).to.eq('eris'); + // unlinked (root data) structure + expect(loaded.dapps).not.to.be.undefined; + expect(loaded.dapps).to.haveOwnProperty('/'); + expect(Buffer.isBuffer(loaded.dapps['/'])).to.be.true; + // linked (root data) access + const subLoaded = await ipld.getLinkedGraph(extendedstored, 'dapps'); + expect(subLoaded).not.to.be.undefined; + expect(Array.isArray(subLoaded.contracts)).to.be.true; + expect(subLoaded.contracts.length).to.eq(sub.contracts.length); + }); + }); + + describe('when loading a graph', () => { + it('should be able to load a linked graph for a nested tree', async () => { + const sampleObject = { + personalInfo: { + firstName: 'eris', + }, + }; + const sub = { + contracts: ['0x01', '0x02', '0x03'] + }; + const subSub = { + contracts: '0x02', + }; + const stored = await ipld.store(Object.assign({}, sampleObject)); + const loadedStored = await ipld.getLinkedGraph(stored, ''); + + // add lv1 + const plusSub = await ipld.set(sampleObject, 'dapps', sub); + const plusSubstored = await ipld.store(Object.assign({}, plusSub)); + const loadedSub = await ipld.getLinkedGraph(plusSubstored, ''); + + // add lv2 + const plusSubSub = await ipld.set(loadedSub, 'dapps/favorites', subSub); + const plusSubSubstored = await ipld.store(Object.assign({}, plusSubSub)); + const loadedFull = await ipld.getLinkedGraph(plusSubSubstored, ''); + + // lv1 is linked + expect(loadedFull).to.haveOwnProperty('dapps'); + expect(loadedFull.dapps).to.haveOwnProperty('/'); + expect(Buffer.isBuffer(loadedFull.dapps['/'])).to.be.true; + + // lv2 is linked + const loadedLv1 = await ipld.getLinkedGraph(plusSubSubstored, 'dapps'); + expect(loadedLv1).to.haveOwnProperty('favorites'); + expect(loadedLv1.favorites).to.haveOwnProperty('/'); + expect(Buffer.isBuffer(loadedLv1.favorites['/'])).to.be.true; + // though lv2s own property is plain + expect(loadedLv1).to.haveOwnProperty('contracts'); + expect(Array.isArray(loadedLv1.contracts)).to.be.true; + expect(loadedLv1.contracts.length).to.eq(sub.contracts.length); + }); + + it('should be able to load a plain graph for a nested tree', async () => { + const sampleObject = { + personalInfo: { + firstName: 'eris', + }, + }; + const sub = { + contracts: ['0x01', '0x02', '0x03'] + }; + const subSub = { + contracts: '0x02', + }; + const stored = await ipld.store(Object.assign({}, sampleObject)); + const loadedStored = await ipld.getLinkedGraph(stored, ''); + + // add lv1 + const plusSub = await ipld.set(sampleObject, 'dapps', sub); + const plusSubstored = await ipld.store(Object.assign({}, plusSub)); + const loadedSub = await ipld.getLinkedGraph(plusSubstored, ''); + + // add lv2 + const plusSubSub = await ipld.set(loadedSub, 'dapps/favorites', subSub); + const plusSubSubstored = await ipld.store(Object.assign({}, plusSubSub)); + const loadedFull = await ipld.getResolvedGraph(plusSubSubstored, ''); + + const resolved = await ipld.getResolvedGraph(plusSubSubstored, ''); + + // lv1 is not linked + expect(loadedFull).to.haveOwnProperty('dapps'); + expect(loadedFull.dapps).to.haveOwnProperty('/'); + expect(Buffer.isBuffer(loadedFull.dapps['/'])).not.to.be.true; + expect(loadedFull.dapps['/']).to.haveOwnProperty('contracts'); + expect(loadedFull.dapps['/']).to.haveOwnProperty('favorites'); + + // lv2 is not linked + const loadedLv1 = await ipld.getResolvedGraph(plusSubSubstored, 'dapps'); + expect(loadedLv1).to.haveOwnProperty('favorites'); + expect(loadedLv1.favorites).to.haveOwnProperty('/'); + expect(Buffer.isBuffer(loadedLv1.favorites['/'])).not.to.be.true; + expect(loadedLv1.favorites['/']).to.haveOwnProperty('contracts'); + }); + + it('can work on graph instances the same way as on hashes/Buffers', async () => { + const sampleObject = { + personalInfo: { + firstName: 'eris', + titles: ['eris'] + }, + }; + const sub = { + contracts: ['0x01', '0x02', '0x03'] + }; + const stored = await ipld.store(Object.assign({}, sampleObject)); + const graph = await ipld.getLinkedGraph(stored); + await ipld.set(graph, 'dapps', sub); + + // queries on linked graph + expect(await ipld.getLinkedGraph(graph, 'personalInfo/firstName')).to.eq('eris'); + expect(await ipld.getLinkedGraph(graph, 'personalInfo/titles')).to.deep.eq(['eris']); + expect(await ipld.getLinkedGraph(graph, 'dapps/contracts')).to.deep.eq(['0x01', '0x02', '0x03']); + + // expand full graph + const expected = { + personalInfo: { + firstName: 'eris', + titles: ['eris'] + }, + dapps: { + '/': { + contracts: ['0x01', '0x02', '0x03'], + }, + }, + }; + const resolved = await ipld.getResolvedGraph(graph, ''); + Ipld.purgeCryptoInfo(resolved); + expect(resolved).to.deep.eq(expected); + }); + }); + + describe('when updating values', () => { + it('should be able to update values in a nested tree', async () => { + const sampleObject = { + personalInfo: { + firstName: 'eris', + titles: ['eris'] + }, + }; + const sub = { + contracts: ['0x01', '0x02', '0x03'] + }; + + const stored = await ipld.store(Object.assign({}, sampleObject)); + const extended = await ipld.set(sampleObject, 'dapps', sub); + const extendedstored = await ipld.store(Object.assign({}, extended)); + const loaded = await ipld.getLinkedGraph(extendedstored, ''); + + + const subModified = Object.assign({}, sub); + subModified.contracts.push('0x04'); + const updated = await ipld.set(loaded, 'dapps', subModified); + const updatedstored = await ipld.store(Object.assign({}, updated)); + const updatedloaded = await ipld.getLinkedGraph(updatedstored, ''); + + const updatedStored = await ipld.store(Object.assign({}, updated)); + const loadedUpdated = await ipld.getResolvedGraph(updatedStored, ''); + + // linked (root data) access + expect(loadedUpdated).not.to.be.undefined; + expect(Array.isArray(loadedUpdated.dapps['/'].contracts)).to.be.true; + expect(loadedUpdated.dapps['/'].contracts.length).to.eq(subModified.contracts.length); + }); + + it('should be able to update different ipld graphs with different keys at the same time', async () => { + let lastKey; + async function updateGraph() { + // shadow ipld with a new one with another key + const defaultCryptoAlgo = 'aes'; + const localIpld = await TestUtils.getIpld(ipfs); + const sampleObject = { + personalInfo: { + firstName: 'eris', + titles: ['eris'] + }, + }; + const sub = { + contracts: ['0x01', '0x02', '0x03'] + }; + + const stored = await localIpld.store(Object.assign({}, sampleObject)); + const extended = await localIpld.set(sampleObject, 'dapps', sub); + const extendedstored = await localIpld.store(Object.assign({}, extended)); + const loaded = await localIpld.getLinkedGraph(extendedstored, ''); + + + const subModified = Object.assign({}, sub); + subModified.contracts.push('0x04'); + const updated = await localIpld.set(loaded, 'dapps', subModified); + const updatedstored = await localIpld.store(Object.assign({}, updated)); + const updatedloaded = await localIpld.getLinkedGraph(updatedstored, ''); + + const updatedStored = await localIpld.store(Object.assign({}, updated)); + const loadedUpdated = await localIpld.getResolvedGraph(updatedStored, ''); + + // linked (root data) access + expect(loadedUpdated).not.to.be.undefined; + expect(Array.isArray(loadedUpdated.dapps['/'].contracts)).to.be.true; + expect(loadedUpdated.dapps['/'].contracts.length).to.eq(subModified.contracts.length); + } + + await Promise.all([...new Array(10)].map(() => updateGraph())); + }); + }); + + describe('when deleting nodes', () => { + const createSampleGraph = async () => { + const initialTree = { + gods: {}, + humans: { + helena: { + origin: 'troja', + }, + }, + }; + const eris = { + name: 'eris', + latinName: 'discordia', + }; + const erisDetails = { + occupation: 'goddess of chaos', + origin: 'greek', + }; + const expectedTree = { + gods: { + eris: { + '/': { + name: 'eris', + latinName: 'discordia', + details: { + '/': { + occupation: 'goddess of chaos', + origin: 'greek', + }, + }, + }, + }, + }, + humans: { + helena: { + origin: 'troja', + }, + }, + }; + let stored; + let loaded; + let updated; + + // store initial + stored = await ipld.store(Object.assign({}, initialTree)); + loaded = await ipld.getLinkedGraph(stored, ''); + + // add subtree + updated = await ipld.set(loaded, 'gods/eris', eris); + stored = await ipld.store(Object.assign({}, updated)); + loaded = await ipld.getLinkedGraph(stored, ''); + updated = await ipld.set(loaded, 'gods/eris/details', erisDetails); + stored = await ipld.store(Object.assign({}, updated)); + loaded = await ipld.getLinkedGraph(stored, ''); + + // check loaded + loaded = await ipld.getResolvedGraph(stored, ''); + Ipld.purgeCryptoInfo(loaded); + expect(loaded).to.deep.eq(expectedTree); + return loaded; + }; + + it('can create the sample graph', async () => { + await createSampleGraph(); + }); + + + it('can delete plain object properties in a simple tree', async () => { + const expectedGraph = { + gods: { + eris: { + '/': { + name: 'eris', + latinName: 'discordia', + details: { + '/': { + occupation: 'goddess of chaos', + origin: 'greek', + }, + }, + }, + }, + }, + humans: {}, + }; + + const sampleGraph = await createSampleGraph(); + const updated = await ipld.remove(sampleGraph, 'humans/helena'); + const loaded = await ipld.getResolvedGraph(updated, ''); + expect(loaded).not.to.be.undefined; + Ipld.purgeCryptoInfo(loaded); + expect(loaded).to.deep.eq(expectedGraph); + }); + + it('can delete linked subtrees', async () => { + const expectedGraph1 = { + gods: {}, + humans: { + helena: { + origin: 'troja', + }, + }, + }; + const expectedGraph2 = { + gods: { + eris: { + '/': { + name: 'eris', + latinName: 'discordia', + }, + }, + }, + humans: { + helena: { + origin: 'troja', + }, + }, + }; + let loaded; + let updated; + let sampleGraph; + let stored; + + // delete topmost link + sampleGraph = await createSampleGraph(); + updated = await ipld.remove(sampleGraph, 'gods/eris'); + stored = await ipld.store(Object.assign({}, updated)); + loaded = await ipld.getResolvedGraph(stored, ''); + expect(loaded).not.to.be.undefined; + Ipld.purgeCryptoInfo(loaded); + expect(loaded).to.deep.eq(expectedGraph1); + + // delete topmost link + sampleGraph = await createSampleGraph(); + updated = await ipld.remove(sampleGraph, 'gods/eris/details'); + stored = await ipld.store(Object.assign({}, updated)); + loaded = await ipld.getResolvedGraph(stored, ''); + expect(loaded).not.to.be.undefined; + Ipld.purgeCryptoInfo(loaded); + expect(loaded).to.deep.eq(expectedGraph2); + }); + + it('can delete properties inside of a linked subtree', async () => { + const deletion = 'gods/eris/details/origin'; + const expectedGraph = { + gods: { + eris: { + '/': { + name: 'eris', + latinName: 'discordia', + details: { + '/': { + occupation: 'goddess of chaos', + }, + }, + }, + }, + }, + humans: { + helena: { + origin: 'troja', + }, + }, + }; + let stored; + let loaded; + let updated; + + const sampleGraph = await createSampleGraph(); + updated = await ipld.remove(sampleGraph, deletion); + stored = await ipld.store(Object.assign({}, updated)); + loaded = await ipld.getResolvedGraph(stored, ''); + expect(loaded).not.to.be.undefined; + Ipld.purgeCryptoInfo(loaded); + expect(loaded).to.deep.eq(expectedGraph); + }) + }); +}); diff --git a/src/dfs/ipld.ts b/src/dfs/ipld.ts new file mode 100644 index 00000000..467c0ed1 --- /dev/null +++ b/src/dfs/ipld.ts @@ -0,0 +1,339 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import Graph = require('ipld-graph-builder'); +import bs58 = require('bs58'); +import * as https from 'https'; +import _ = require('lodash'); + +import { + CryptoInfo, + Cryptor, + Envelope, + Ipfs, + KeyProviderInterface, + Logger, + LoggerOptions, + NameResolver, +} from '@evan.network/dbcp'; + +import { CryptoProvider } from '../encryption/crypto-provider'; + +const IPLD_TIMEOUT = 120000; + +function rebuffer(toBuffer) { + Object.keys(toBuffer).forEach((key) => { + if (key === '/') { + toBuffer[key] = Buffer.from(toBuffer[key].data); + } else if (typeof toBuffer[key] === 'object' && toBuffer[key] !== null) { + rebuffer(toBuffer[key]); + } + }); +} + + +export interface IpldOptions extends LoggerOptions { + ipfs: Ipfs; + keyProvider: KeyProviderInterface; + cryptoProvider: CryptoProvider; + originator: string; + defaultCryptoAlgo: string; + nameResolver: NameResolver; +} + + +/** + * IPLD helper class, a single instance has to be created for each cryptor configuration. + * + * @class Ipld IPFS helper class + */ +export class Ipld extends Logger { + graph: Graph; + ipfs: Ipfs; + keyProvider: KeyProviderInterface; + cryptoProvider: CryptoProvider; + originator: string; + defaultCryptoAlgo: string; + nameResolver: NameResolver; + + private readonly dagOptions = { format: 'dag-pb', }; + private readonly encodingUnencrypted = 'utf-8'; + private readonly encodingEncrypted = 'hex'; + + /** + * remove all cryptoInfos from tree + * + * @param {any} toPurge tree to purge + */ + public static purgeCryptoInfo(toPurge: any): void { + Object.keys(toPurge).forEach((key) => { + if (key === 'cryptoInfo') { + delete toPurge.cryptoInfo; + } else if (typeof toPurge[key] === 'object' && toPurge[key] !== null) { + this.purgeCryptoInfo(toPurge[key]); + } + }); + } + + constructor(options: IpldOptions) { + super(options); + this.ipfs = options.ipfs; + this.graph = new Graph({ get: null, put: null, }); + this.keyProvider = options.keyProvider; + this.cryptoProvider = options.cryptoProvider; + this.originator = options.originator; + this.defaultCryptoAlgo = options.defaultCryptoAlgo; + this.nameResolver = options.nameResolver; + + // overwrite dag.put and dag.get if cryptor was provided + const originalDagPut = this.graph._dag.put; + this.graph._dag.put = async (...args) => { + const data = args[0]; + if (data.cryptoInfo || this.defaultCryptoAlgo) { + let cryptor; + let cryptoInfo; + if (data.cryptoInfo) { + cryptor = this.cryptoProvider.getCryptorByCryptoInfo(data.cryptoInfo); + cryptoInfo = data.cryptoInfo; + delete data.cryptoInfo; + } else { + cryptor = this.cryptoProvider.getCryptorByCryptoAlgo(this.defaultCryptoAlgo); + cryptoInfo = cryptor.getCryptoInfo(this.originator); + } + const key = await this.keyProvider.getKey(cryptoInfo); + const encrypted = await cryptor.encrypt(args[0], { key, }) + args[0] = encrypted.toString(this.encodingEncrypted); + const envelope: Envelope = { + private: encrypted, + cryptoInfo, + }; + args[0] = Buffer.from(JSON.stringify(envelope)); + } + // add file to ipfs instead of dag put because js-ipfs-api don't supports dag at the moment + return this.ipfs.add('dag', args[0]) + .then((hash) => { + const bufferHash = bs58.decode(Ipfs.bytes32ToIpfsHash(hash)); + const dagHash = bs58.encode(bufferHash); + return bufferHash; + }); + }; + + const originalDagGet = this.graph._dag.get; + this.graph._dag.get = (...args) => { + const timeout = new Promise((resolve, reject) => { + let wait = setTimeout(() => { + clearTimeout(wait); + reject(new Error('timeout reached')); + }, IPLD_TIMEOUT) + }) + this.log(`Getting IPLD Hash ${bs58.encode(args[0])}`, 'debug'); + // add file to ipfs instead of dag put because js-ipfs-api don't supports dag at the moment + const getHash = this.ipfs.get(bs58.encode(args[0])) + .then(async (dag) => { + if (this.defaultCryptoAlgo) { + const envelope: Envelope = JSON.parse(dag.toString('utf-8')); + const cryptor = this.cryptoProvider.getCryptorByCryptoInfo(envelope.cryptoInfo); + const key = await this.keyProvider.getKey(envelope.cryptoInfo); + if(!key) { + return {}; + } else { + + const decryptedObject = await cryptor.decrypt( + Buffer.from(envelope.private, this.encodingEncrypted), { key, }); + rebuffer(decryptedObject); + if(typeof decryptedObject === 'object') { + // keep crypto info for later re-encryption + decryptedObject.cryptoInfo = envelope.cryptoInfo; + } + return decryptedObject; + } + } + }) + ; + return Promise.race([ + getHash, + timeout + ]) + }; + } + + /** + * Get a path from a tree; resolve only if required (depends on requested path) + * + * @param {string | Buffer | any} graphReference hash/buffer to look up or a graph object + * @param {string} path path in the tree + * @return {Promise} linked graph. + */ + async getLinkedGraph(graphReference: string | Buffer | any, path = ''): Promise { + let graphObject; + if (typeof graphReference === 'string') { + // fetch ipfs file + const ipfsFile = (await this.ipfs.get(graphReference)).toString(this.encodingUnencrypted); + + // decrypt content + const envelope: Envelope = JSON.parse(ipfsFile); + if (envelope.cryptoInfo) { + const cryptor = this.cryptoProvider.getCryptorByCryptoInfo(envelope.cryptoInfo); + const key = await this.keyProvider.getKey(envelope.cryptoInfo); + const decryptedObject = await cryptor.decrypt( + Buffer.from(envelope.private, this.encodingEncrypted), { key, }); + rebuffer(decryptedObject); + graphObject = decryptedObject; + } else { + graphObject = { '/': Buffer.from(ipfsFile, this.encodingUnencrypted), }; + } + } else if (Buffer.isBuffer(graphReference)) { + graphObject = { '/': graphReference, }; + } else { + graphObject = graphReference; + } + if (!path) { + const tree = await this.graph.tree(graphObject, 0); + return tree['/'] || tree; + } else { + const element = await this.graph.get(graphObject, path) + if (element) { + this.log(`Got Linked Graph Path -> ${path} Element`, 'debug'); + } else { + this.log(`Could not get Linked Graph Path -> ${path} Element`, 'debug'); + } + + return element; + } + } + + /** + * Get a path from a tree; resolve links in paths up to depth (default is 10) + * + * @param {string | Buffer | any} graphReference hash/buffer to look up or a graph object + * @param {string} path path in the tree + * @param {number} depth resolve up do this many levels of depth + * (default: 10) + * @return {Promise} resolved graph + */ + async getResolvedGraph(graphReference: string | Buffer | any, path = '', depth = 10): Promise { + const treeNode = await this.getLinkedGraph(graphReference, path); + return await this.graph.tree(treeNode, depth, true); + } + + /** + * store tree, if tree contains merklefied links, stores tree with multiple linked subtrees + * + * @param {any} toSet tree to store + * @return {Promise} hash reference to a tree with with merklefied links + */ + async store(toSet: any): Promise { + const cryptoInfo = { + algorithm: 'unencrypted' + } + const treeToStore = _.cloneDeep(toSet); + const [rootObject, key] = await Promise.all([ + // get final tree + this.graph.flush(treeToStore, Object.assign({}, this.dagOptions, { cryptoInfo, })), + // encrypt dag and put in envelope + this.keyProvider.getKey(cryptoInfo), + ]); + const envelope: Envelope = { + private: Buffer.from(JSON.stringify(rootObject), this.encodingUnencrypted).toString(this.encodingEncrypted), + cryptoInfo, + }; + + // store to ipfs + return await this.ipfs.add('dag', Buffer.from(JSON.stringify(envelope))); + } + + /** + * set a value to a tree node; inserts new element as a linked subtree by default + * + * @param {any} tree tree to extend + * @param {string} path path of inserted element + * @param {any} subtree element that will be added + * @param {boolean} plainObject do not link value as new subtree + * @param {CryptoInfo} cryptoInfo crypto info for encrypting subtree + * @return {Promise} tree with merklefied links + */ + async set(tree: any, path: string, subtree: any, plainObject = false, cryptoInfo?: CryptoInfo): Promise { + if (cryptoInfo && typeof subtree === 'object') { + subtree.cryptoInfo = cryptoInfo; + } + const graphTree = await this.graph.set(tree, path, subtree, plainObject); + return graphTree; + } + + /** + * delete a value from a tree node + * + * @param {any} tree tree to extend + * @param {string} path path of inserted element + * @return {Promise} tree with merklefied links + */ + async remove(tree: any, path: string): Promise { + const splitPath = path.split('/'); + const node = splitPath[splitPath.length - 1]; + const toTraverse = splitPath.slice(0, -1); + let currentNode; + let currentTree + let linkedParent = tree; + + // find next linked node + while (currentNode = toTraverse.pop()) { + currentTree = await this.getLinkedGraph(tree, toTraverse.join('/')); + if (currentTree[currentNode]['/']) { + linkedParent = await this.getLinkedGraph(tree, `${toTraverse.join('/')}/${currentNode}`); + break; + } + } + + // find and delete in linked node + let pathInParent; + let splitPathInParent; + if (toTraverse.length) { + pathInParent = path.replace(`${toTraverse.join('/')}/`, ''); + splitPathInParent = pathInParent.split('/').slice(1, -1); // skip parent prop, skip last + } else { + pathInParent = path; + splitPathInParent = path.split('/').slice(0, -1); // skip last + } + let nodeInParentPath = linkedParent; + let nodeNameInParentPath; + while (nodeNameInParentPath = splitPathInParent.pop()) { + nodeInParentPath = nodeInParentPath[nodeNameInParentPath]; + } + delete nodeInParentPath[node]; + + if (toTraverse.length) { + // set updated linked node in entire graph + return await this.graph.set(tree, `${toTraverse.join('/')}/${currentNode}`, linkedParent); + } else { + // flush graph, return linked graph object + const cryptor = this.cryptoProvider.getCryptorByCryptoAlgo(this.defaultCryptoAlgo); + const cryptoInfo = cryptor.getCryptoInfo(this.originator); + const flushed = await this.graph.flush(tree, this.dagOptions, this.dagOptions, { cryptoInfo, }); + return this.getLinkedGraph(flushed); + } + } +} diff --git a/src/encryption/aes-blob.spec.ts b/src/encryption/aes-blob.spec.ts new file mode 100644 index 00000000..6e091a78 --- /dev/null +++ b/src/encryption/aes-blob.spec.ts @@ -0,0 +1,84 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect } from 'chai'; + +const fs = require('fs'); +const { promisify } = require('util'); + +import { AesBlob } from './aes-blob' +import { TestUtils } from '../test/test-utils' + +let sampleFile; +let fileDescription; +let ipfs; +const sampleEncryptedHex = '763e585420c5d1a4d46e2764ee4ff71e3019fb7dfabad91da1e5433171f8f87a37d1da127c50b91243901275f3df8815d1bd4a57401a62fa51fcb262962aefac3701594a6ed4fb0f006b618a02f6b9a4b8cfa83547a1884501170ae60ae7a1d4c7ae1899f64a70aeae65737e5b58a70193824d519f4ef963e922514eae4a9cb8c6ed3c48994fa006aa9323eecbdd1794'; +const sampleKey = '346c22768f84f3050f5c94cec98349b3c5cbfa0b7315304e13647a49181fd1ef'; + +describe('Blob Encryption', function() { + this.timeout(300000); + + before(async () => { + sampleFile = await promisify(fs.readFile)('./src/encryption/testfile.spec.jpg'); + fileDescription = { + name: 'testfile.spec.jpg', + fileType: 'image/jpeg', + file: sampleFile + }; + ipfs = await TestUtils.getIpfs(); + }); + + after(async () => { + await ipfs.stop(); + }); + + + it('should be able to be created', () => { + const aes = new AesBlob({dfs: ipfs}); + expect(aes).not.to.be.undefined; + }); + + it('should be able to generate keys', async () => { + const aes = new AesBlob({dfs: ipfs}); + const key = await aes.generateKey(); + expect(key).not.to.be.undefined; + }); + + it('should be able to encrypt a sample message', async () => { + const aes = new AesBlob({dfs: ipfs}); + const encrypted = await aes.encrypt(fileDescription, { key: sampleKey, }); + expect(encrypted.toString('hex')).to.deep.eq(sampleEncryptedHex); + }); + + it('should be able to decrypt a sample message', async () => { + const aes = new AesBlob({dfs: ipfs}); + const decrypted = await aes.decrypt(Buffer.from(sampleEncryptedHex, 'hex'), { key: sampleKey, }); + debugger; + expect(decrypted).to.deep.equal(fileDescription); + }); +}); \ No newline at end of file diff --git a/src/encryption/aes-blob.ts b/src/encryption/aes-blob.ts new file mode 100644 index 00000000..dcd821ef --- /dev/null +++ b/src/encryption/aes-blob.ts @@ -0,0 +1,311 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import crypto = require('crypto-browserify'); + +import { + Cryptor, + CryptoInfo, + Logger, + LoggerOptions, +} from '@evan.network/dbcp'; + + +/** + * generate new intiala vector, length is 16 bytes (aes) + * + * @return {Buffer} initial vector as Buffer + */ +function generateInitialVector(): Buffer { + return crypto.randomBytes(16); +} + +/** + * aes blob instance options + */ +export interface AesBlobOptions extends LoggerOptions { + dfs: any; +} + + +/** + * encrypts files, uploads them to DFS and keeps their references in an envelope + * + * @class AesBlob (name) + */ +export class AesBlob extends Logger implements Cryptor { + + private readonly encodingUnencrypted = 'utf-8'; + private readonly encodingEncrypted = 'hex'; + + options: any; + algorithm: string; + webCryptoAlgo: string; + + static defaultOptions = { + keyLength: 256, + algorithm: 'aes-blob', + }; + + constructor(options?: AesBlobOptions) { + super(options); + this.algorithm = 'aes-256-cbc'; + this.webCryptoAlgo = 'AES-CBC'; + this.options = Object.assign({}, AesBlob.defaultOptions, options || {}); + } + + + /** + * convert string to array buffer + * + * @param {string} str string to convert + * @return {Buffer} converted input + */ + stringToArrayBuffer(str){ + var len = str.length; + var bytes = new Uint8Array( len ); + for (var i = 0; i < len; i++) { + bytes[i] = str.charCodeAt(i); + } + return bytes.buffer; + } + + + getCryptoInfo(originator: string): CryptoInfo { + const ret = Object.assign({ originator, }, this.options); + delete ret.dfs; + return ret; + } + + chunkBuffer(buffer, chunkSize) { + const result = []; + const len = buffer.length; + let i = 0; + while (i < len) { + result.push(buffer.slice(i, i += chunkSize)); + } + return result; + } + + + /** + * generate key for cryptor/decryption + * + * @return {any} The iv from key. + + */ + async generateKey(): Promise { + return new Promise((resolve, reject) => { + crypto.randomBytes(this.options.keyLength / 8, (err, buf) => { + if (err) { + reject(err); + } else { + const hexString = buf.toString('hex') + resolve(hexString); + } + }); + }) + } + + async decryptBrowser(algorithm, buffer, decryptKey, iv) { + const key = await (global).crypto.subtle.importKey( + 'raw', + decryptKey, + { + name: algorithm, + length: 256, + }, + false, + ['decrypt'] + ); + const decrypted = await (global).crypto.subtle.decrypt( + { + name: algorithm, + iv: iv, + }, + key, + buffer + ); + return Buffer.from(decrypted); + } + + async encryptBrowser(algorithm, buffer, encryptionKey, iv) { + const key = await (global).crypto.subtle.importKey( + 'raw', + encryptionKey, + { + name: algorithm, + length: 256, + }, + false, + ['encrypt'] + ); + const encrypted = await (global).crypto.subtle.encrypt( + { + name: algorithm, + iv: iv, + }, + key, + buffer + ); + return Buffer.from(encrypted); + } + + /** + * encrypt a message + * + * @param {any} message The message + * @param {any} options cryptor options + * @return {Buffer} encrypted message + */ + async encrypt(message: any, options: any): Promise { + try { + if (!options.key) { + throw new Error('no key given'); + } + let encryptedWrapperMessage; + const initialVector = generateInitialVector(); + // its an array of blobs + if(Array.isArray(message)) { + const files = []; + for(let blob of message) { + let encrypted; + if((global).crypto.subtle) { + encrypted = await this.encryptBrowser(this.webCryptoAlgo, Buffer.from(blob.file), new Buffer(options.key, 'hex'), initialVector); + } else { + const cipher = crypto.createCipheriv(this.algorithm, new Buffer(options.key, 'hex'), initialVector); + encrypted = Buffer.concat([cipher.update(Buffer.from(blob.file)), cipher.final()]); + } + const encryptedWithIv = Buffer.concat([initialVector, encrypted]); + const stateMd5 = crypto.createHash('md5').update(encryptedWithIv).digest('hex'); + files.push({ + path: stateMd5, + content: encryptedWithIv + }); + } + const hashes = await this.options.dfs.addMultiple(files); + for(var i=0; i < message.length; i++) { + message[i].file = hashes[i]; + } + } else { + const cipher = crypto.createCipheriv(this.algorithm, new Buffer(options.key, 'hex'), initialVector); + let encrypted; + if((global).crypto.subtle) { + encrypted = await this.encryptBrowser(this.webCryptoAlgo,Buffer.from(message.file), new Buffer(options.key, 'hex'), initialVector); + } else { + const cipher = crypto.createCipheriv(this.algorithm, new Buffer(options.key, 'hex'), initialVector); + encrypted = Buffer.concat([cipher.update(Buffer.from(message.file)), cipher.final()]); + } + const encryptedWithIv = Buffer.concat([initialVector, encrypted]); + const stateMd5 = crypto.createHash('md5').update(encryptedWithIv).digest('hex'); + const hash = await this.options.dfs.add(stateMd5, encryptedWithIv); + message.file = hash; + } + const wrapperMessage = Buffer.from(JSON.stringify(message), this.encodingUnencrypted); + if((global).crypto.subtle) { + encryptedWrapperMessage = await this.encryptBrowser(this.webCryptoAlgo, Buffer.from(wrapperMessage), new Buffer(options.key, 'hex'), initialVector); + } else { + const wrapperDecipher = crypto.createCipheriv(this.algorithm, new Buffer(options.key, 'hex'), initialVector); + encryptedWrapperMessage = Buffer.concat([wrapperDecipher.update(wrapperMessage), wrapperDecipher.final()]); + } + return Promise.resolve(Buffer.concat([initialVector, encryptedWrapperMessage])); + } catch(ex) { + this.log(`could not encrypt; ${ex.message || ex}`, 'error'); + return Promise.reject(ex); + } + } + + /** + * decrypt a message + * + * @param {Buffer} message The message + * @param {any} options decryption options + * @return {any} decrypted message + */ + async decrypt(message: Buffer, options: any): Promise { + try { + if (!options.key) { + throw new Error('no key given'); + } + + const initialVector = message.slice(0, 16); + const encrypted = message.slice(16); + let decrypted; + if((global).crypto.subtle) { + decrypted = await this.decryptBrowser(this.webCryptoAlgo, encrypted, new Buffer(options.key, 'hex'), initialVector); + } else { + const decipher = crypto.createDecipheriv(this.algorithm, new Buffer(options.key, 'hex'), initialVector); + decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); + } + + const wrapper = JSON.parse(decrypted.toString(this.encodingUnencrypted)); + + + if(Array.isArray(wrapper)) { + const encryptedFiles = []; + for(let blob of wrapper) { + const ipfsFile = await this.options.dfs.get(blob.file, true); + let file = new Buffer(''); + const initialVectorFile = ipfsFile.slice(0, 16); + const encryptedFile = ipfsFile.slice(16); + if((global).crypto.subtle) { + file = await this.decryptBrowser(this.webCryptoAlgo, encryptedFile, new Buffer(options.key, 'hex'), initialVectorFile); + } else { + const fileDecipher = crypto.createDecipheriv(this.algorithm, new Buffer(options.key, 'hex'), initialVectorFile); + const chunks = this.chunkBuffer(encryptedFile, 1024); + for(let chunk of chunks) { + file = Buffer.concat([file, fileDecipher.update(chunk)]); + } + file = Buffer.concat([file, fileDecipher.final()]); + } + blob.file = file; + } + } else { + const ipfsFile = await this.options.dfs.get(wrapper.file, true); + const initialVectorFile = ipfsFile.slice(0, 16); + const encryptedFile = ipfsFile.slice(16); + let file = new Buffer(''); + if((global).crypto.subtle) { + file = await this.decryptBrowser(this.webCryptoAlgo, encryptedFile, new Buffer(options.key, 'hex'), initialVectorFile); + } else { + const fileDecipher = crypto.createDecipheriv(this.algorithm, new Buffer(options.key, 'hex'), initialVectorFile); + const chunks = this.chunkBuffer(encryptedFile, 1024); + for(let chunk of chunks) { + file = Buffer.concat([file, fileDecipher.update(chunk)]); + } + file = Buffer.concat([file, fileDecipher.final()]); + } + wrapper.file = file; + } + + return Promise.resolve(wrapper); + } catch (ex) { + this.log(`could not decrypt; ${ex.message || ex}`, 'error'); + return Promise.reject(ex); + } + } +} \ No newline at end of file diff --git a/src/encryption/aes-ecb.spec.ts b/src/encryption/aes-ecb.spec.ts new file mode 100644 index 00000000..36ae9b81 --- /dev/null +++ b/src/encryption/aes-ecb.spec.ts @@ -0,0 +1,68 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect } from 'chai'; + +import { AesEcb } from './aes-ecb' +import { TestUtils } from '../test/test-utils' + +const sampleUnencrypted = 'Id commodo nulla ut eiusmod.'; +const sampleKey = '346c22768f84f3050f5c94cec98349b3c5cbfa0b7315304e13647a49181fd1ef'; + +describe('aes (ecb) handler', function() { + this.timeout(300000); + + it('should be able to be created', () => { + const aes = new AesEcb(); + expect(aes).not.to.be.undefined; + }); + + it('should be able to generate keys', async () => { + const aes = new AesEcb(); + const key = await aes.generateKey(); + expect(key).not.to.be.undefined; + }); + + it('should be able to encrypt and decrypt a random message', async () => { + const aes = new AesEcb(); + const key = await aes.generateKey(); + const message = Buffer.from(Math.random().toString(), 'utf-8'); + const encrypted = await aes.encrypt(message, { key: key, }); + const decrypted = await aes.decrypt(encrypted, { key: key, }); + expect(decrypted.toString('utf-8')).to.eq(message.toString('utf-8')); + }); + + it('should be able to encrypt and decrypt a random number', async () => { + const aes = new AesEcb(); + const key = await aes.generateKey(); + const message = Buffer.from(Math.random().toString(), 'utf-8'); + const encrypted = await aes.encrypt(message, { key: key, }); + const decrypted = await aes.decrypt(encrypted, { key: key, }); + expect(decrypted).to.deep.eq(message); + }); +}); diff --git a/src/encryption/aes-ecb.ts b/src/encryption/aes-ecb.ts new file mode 100644 index 00000000..b0b8bae0 --- /dev/null +++ b/src/encryption/aes-ecb.ts @@ -0,0 +1,144 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import crypto = require('crypto-browserify'); + +import { + Cryptor, + CryptoInfo, + Logger, + LoggerOptions, +} from '@evan.network/dbcp'; + +/** + * aes ecb instance options + */ +export interface AesEcbOptions extends LoggerOptions { + +} + +export class AesEcb extends Logger implements Cryptor { + + private readonly encodingUnencrypted = 'utf-8'; + private readonly encodingEncrypted = 'hex'; + + options: any; + static defaultOptions = { + keyLength: 256, + algorithm: 'aes-256-ecb', + }; + + constructor(options?: AesEcbOptions) { + super(options); + this.options = Object.assign({}, AesEcb.defaultOptions, options || {}); + } + + + /** + * convert string to array buffer + * + * @param {string} str string to convert + * @return {Buffer} converted input + */ + stringToArrayBuffer(str) { + let len = str.length; + let bytes = new Uint8Array( len ); + for (let i = 0; i < len; i++) { + bytes[i] = str.charCodeAt(i); + } + return bytes.buffer; + } + + + getCryptoInfo(originator: string): CryptoInfo { + return Object.assign({ originator, }, this.options); + } + + /** + * generate key for cryptor/decryption + * + * @return {any} The iv from key. + + */ + async generateKey(): Promise { + return new Promise((resolve, reject) => { + crypto.randomBytes(this.options.keyLength / 8, (err, buf) => { + if (err) { + reject(err); + } else { + const hexString = buf.toString('hex') + resolve(hexString); + } + }); + }) + } + + /** + * encrypt a message + * + * @param {any} message The message + * @param {any} options cryptor options + * @return {Buffer} encrypted message + */ + async encrypt(message: Buffer, options: any): Promise { + try { + if (!options.key) { + throw new Error('no key given'); + } + const cipher = crypto.createCipher(this.options.algorithm, new Buffer(options.key, 'hex')); + cipher.setAutoPadding(false) + const encrypted = Buffer.concat([cipher.update(message), cipher.final()]); + + return Promise.resolve(encrypted); + } catch (ex) { + this.log(`could not encrypt; ${ex.message || ex}`, 'error'); + return Promise.reject(ex); + } + } + + /** + * decrypt a message + * + * @param {Buffer} message The message + * @param {any} options decryption options + * @return {any} decrypted message + */ + async decrypt(message: Buffer, options: any): Promise { + try { + if (!options.key) { + throw new Error('no key given'); + } + const decipher = crypto.createDecipher(this.options.algorithm, new Buffer(options.key, 'hex')); + decipher.setAutoPadding(false) + const decrypted = Buffer.concat([decipher.update(message), decipher.final()]); + return Promise.resolve(decrypted); + } catch (ex) { + this.log(`could not decrypt; ${ex.message || ex}`, 'error'); + return Promise.reject(ex); + } + } +} diff --git a/src/encryption/aes.spec.ts b/src/encryption/aes.spec.ts new file mode 100644 index 00000000..ed5a179a --- /dev/null +++ b/src/encryption/aes.spec.ts @@ -0,0 +1,66 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect } from 'chai'; + +import { Aes } from './aes' +import { TestUtils } from '../test/test-utils' + + +describe('aes handler', function() { + this.timeout(300000); + + it('should be able to be created', () => { + const aes = new Aes(); + expect(aes).not.to.be.undefined; + }); + + it('should be able to generate keys', async () => { + const aes = new Aes(); + const key = await aes.generateKey(); + expect(key).not.to.be.undefined; + }); + + it('should be able to encrypt and decrypt a random message', async () => { + const aes = new Aes(); + const key = await aes.generateKey(); + const message = Math.random().toString(); + const encrypted = await aes.encrypt(message, { key: key, }); + const decrypted = await aes.decrypt(encrypted, { key: key, }); + expect(decrypted.toString('utf-8')).to.eq(message); + }); + + it('should be able to encrypt and decrypt a random number', async () => { + const aes = new Aes(); + const key = await aes.generateKey(); + const message = Math.random(); + const encrypted = await aes.encrypt(message, { key: key, }); + const decrypted = await aes.decrypt(encrypted, { key: key, }); + expect(decrypted).to.eq(message); + }); +}); diff --git a/src/encryption/aes.ts b/src/encryption/aes.ts new file mode 100644 index 00000000..9db9f85e --- /dev/null +++ b/src/encryption/aes.ts @@ -0,0 +1,159 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import crypto = require('crypto-browserify'); + +import { + Cryptor, + CryptoInfo, + Logger, + LoggerOptions, +} from '@evan.network/dbcp'; + + +/** + * generate new intiala vector, length is 16 bytes (aes) + * + * @return {Buffer} initial vector as Buffer + */ +function generateInitialVector(): Buffer { + return crypto.randomBytes(16); +} + +/** + * aes instance options + */ +export interface AesOptions extends LoggerOptions { + +} + +export class Aes extends Logger implements Cryptor { + + private readonly encodingUnencrypted = 'utf-8'; + private readonly encodingEncrypted = 'hex'; + + options: any; + static defaultOptions = { + keyLength: 256, + algorithm: 'aes-256-cbc', + }; + + constructor(options?: AesOptions) { + super(options); + this.options = Object.assign({}, Aes.defaultOptions, options || {}); + } + + + /** + * convert string to array buffer + * + * @param {string} str string to convert + * @return {Buffer} converted input + */ + stringToArrayBuffer(str) { + let len = str.length; + let bytes = new Uint8Array( len ); + for (let i = 0; i < len; i++) { + bytes[i] = str.charCodeAt(i); + } + return bytes.buffer; + } + + + getCryptoInfo(originator: string): CryptoInfo { + return Object.assign({ originator, }, this.options); + } + + /** + * generate key for cryptor/decryption + * + * @return {any} The iv from key. + + */ + async generateKey(): Promise { + return new Promise((resolve, reject) => { + crypto.randomBytes(this.options.keyLength / 8, (err, buf) => { + if (err) { + reject(err); + } else { + const hexString = buf.toString('hex') + resolve(hexString); + } + }); + }) + } + + /** + * encrypt a message + * + * @param {any} message The message + * @param {any} options cryptor options + * @return {Buffer} encrypted message + */ + async encrypt(message: any, options: any): Promise { + try { + if (!options.key) { + throw new Error('no key given'); + } + const initialVector = generateInitialVector(); + const bufferedMessage = Buffer.from(JSON.stringify(message), this.encodingUnencrypted); + const cipher = crypto.createCipheriv(this.options.algorithm, new Buffer(options.key, 'hex'), initialVector); + const encrypted = Buffer.concat([cipher.update(bufferedMessage), cipher.final()]); + + return Promise.resolve(Buffer.concat([initialVector, encrypted])); + } catch (ex) { + this.log(`could not encrypt; ${ex.message || ex}`, 'error'); + return Promise.reject(ex); + } + } + + /** + * decrypt a message + * + * @param {Buffer} message The message + * @param {any} options decryption options + * @return {any} decrypted message + */ + async decrypt(message: Buffer, options: any): Promise { + try { + if (!options.key) { + throw new Error('no key given'); + } + const initialVector = message.slice(0, 16); + const encrypted = message.slice(16); + + const decipher = crypto.createDecipheriv(this.options.algorithm, new Buffer(options.key, 'hex'), initialVector); + const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); + + const result = JSON.parse(decrypted.toString(this.encodingUnencrypted)); + return Promise.resolve(result); + } catch (ex) { + this.log(`could not decrypt; ${ex.message || ex}`, 'error'); + return Promise.reject(ex); + } + } +} diff --git a/src/encryption/crypto-provider.ts b/src/encryption/crypto-provider.ts new file mode 100644 index 00000000..3001c78d --- /dev/null +++ b/src/encryption/crypto-provider.ts @@ -0,0 +1,60 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import { + CryptoInfo, + Cryptor, +} from '@evan.network/dbcp'; + +import * as Dbcp from '@evan.network/dbcp'; + + +/** + * wrapper for supported cryptors + * + * @class CryptoProvider (name) + */ +export class CryptoProvider extends Dbcp.CryptoProvider { + constructor(cryptors) { + super(cryptors); + } + + /** + * get a Cryptor matching the provided CryptoInfo + * + * @param {CryptoInfo} info details about en-/decryption + * @return {Cryptor} matching cryptor + */ + getCryptorByCryptoInfo(info: CryptoInfo): Cryptor { + switch (info.algorithm) { + case 'aes-256-cbc': return this.cryptors.aes; + case 'unencrypted': return this.cryptors.unencrypted; + case 'aes-blob': return this.cryptors.aesBlob; + default: throw new Error(`algorithm unsupported ${info.algorithm}`); + } + } +} diff --git a/src/encryption/testfile.spec.jpg b/src/encryption/testfile.spec.jpg new file mode 100644 index 00000000..b26b5fd4 Binary files /dev/null and b/src/encryption/testfile.spec.jpg differ diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..d117be60 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,52 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +export { + AccountStore, + ContractLoader, + EventHub, + Executor, + Ipfs, + KeyProvider, + NameResolver, + SignerInternal, + Unencrypted, +} from '@evan.network/dbcp' + +export { Aes } from './encryption/aes'; +export { AesEcb } from './encryption/aes-ecb'; +export { ContractState } from './contracts/base-contract/base-contract'; +export { createDefaultRuntime, Runtime } from './runtime'; +export { CryptoProvider } from './encryption/crypto-provider'; +export { DataContract } from './contracts/data-contract/data-contract'; +export { Description } from './shared-description'; +export { Ipld } from './dfs/ipld'; +export { KeyExchange } from './keyExchange'; +export { Mailbox } from './mailbox'; +export { Profile } from './profile/profile'; +export { Sharing } from './contracts/sharing'; +export { RightsAndRoles, ModificationType, PropertyType } from './contracts/rights-and-roles'; diff --git a/src/keyExchange.spec.ts b/src/keyExchange.spec.ts new file mode 100644 index 00000000..d735c665 --- /dev/null +++ b/src/keyExchange.spec.ts @@ -0,0 +1,239 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect, use } from 'chai'; +import chaiAsPromised = require('chai-as-promised'); + +use(chaiAsPromised); + +import BigNumber = require('bignumber.js'); + +import { + Ipfs, + NameResolver, +} from '@evan.network/dbcp'; + +import { accounts } from './test/accounts'; +import { Mail, Mailbox, MailboxOptions } from './mailbox'; +import { Profile } from './profile/profile'; +import { KeyExchange, KeyExchangeOptions } from './keyExchange'; +import { TestUtils } from './test/test-utils'; +import { Ipld } from './dfs/ipld'; + +describe('KeyExchange class', function() { + this.timeout(600000); + let ipfs: Ipfs; + let mailbox: Mailbox; + let mailbox2: Mailbox; + let keyExchange1: KeyExchange; + let keyExchange2: KeyExchange; + let keyExchangeKeys: any; + let commKey: Buffer; + let web3; + let ipld: Ipld; + let profile: Profile; + let profile2: Profile; + const random = Math.random(); + const getTestMail = (): Mail => ({ + content: { + from: accounts[0], + title: 'talking to myself', + body: `hi, me. I like random numbers, for example ${random}`, + attachments: [ + { + type: 'sharedExchangeKey', + key: '', + } + ], + }, + }); + const getTestAnswer = (parentId): Mail => ({ + parentId, + content: { + body: `but my favorite random number is ${random}`, + title: 'I like random numbers as well', + }, + }); + + before(async () => { + web3 = TestUtils.getWeb3(); + + ipfs = await TestUtils.getIpfs(); + ipld = await TestUtils.getIpld(ipfs); + + profile = new Profile({ + nameResolver: await TestUtils.getNameResolver(web3), + defaultCryptoAlgo: 'aes', + dataContract: await TestUtils.getDataContract(web3, ipfs), + contractLoader: await TestUtils.getContractLoader(web3), + ipld, + executor: await TestUtils.getExecutor(web3), + accountId: accounts[0], + }); + + profile2 = new Profile({ + nameResolver: await TestUtils.getNameResolver(web3), + defaultCryptoAlgo: 'aes', + dataContract: await TestUtils.getDataContract(web3, ipfs), + contractLoader: await TestUtils.getContractLoader(web3), + ipld, + executor: await TestUtils.getExecutor(web3), + accountId: accounts[1], + }); + + mailbox = new Mailbox({ + mailboxOwner: accounts[0], + nameResolver: await TestUtils.getNameResolver(web3), + ipfs, + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider: TestUtils.getCryptoProvider(), + keyProvider: TestUtils.getKeyProvider(), + defaultCryptoAlgo: 'aes', + }); + + // mailbox user 2 + mailbox2 = new Mailbox({ + mailboxOwner: accounts[1], + nameResolver: await TestUtils.getNameResolver(web3), + ipfs, + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider: TestUtils.getCryptoProvider(), + keyProvider: TestUtils.getKeyProvider(), + defaultCryptoAlgo: 'aes', + }); + + const keyExchangeOptions = { + mailbox, + cryptoProvider: TestUtils.getCryptoProvider(), + defaultCryptoAlgo: 'aes', + account: accounts[0], + keyProvider: TestUtils.getKeyProvider(), + } + const keyExchangeOptions2 = { + mailbox: mailbox2, + cryptoProvider: TestUtils.getCryptoProvider(), + defaultCryptoAlgo: 'aes', + account: accounts[1], + keyProvider: TestUtils.getKeyProvider(), + } + keyExchange1 = new KeyExchange(keyExchangeOptions); + keyExchange2 = new KeyExchange(keyExchangeOptions2); + + // create profile 1 + await profile.createProfile(keyExchange1.getDiffieHellmanKeys()); + // create profile 2 + await profile2.createProfile(keyExchange2.getDiffieHellmanKeys()); + }); + + after(async () => { + await ipfs.stop(); + web3.currentProvider.connection.close(); + }); + + it('should be able to send an invitation mail and store new commKey', async () => { + const foreignPubkey = await profile2.getPublicKey(); + const commKey = await keyExchange1.generateCommKey(); + await keyExchange1.sendInvite(accounts[1], foreignPubkey, commKey, { fromAlias: 'Bob', }); + await profile.addContactKey(accounts[1], 'commKey', commKey); + await profile.storeForAccount(profile.treeLabels.addressBook); + }); + + it('should compute 2 different keys for the both accounts', async () => { + expect(keyExchange1.getDiffieHellmanKeys().publicKey).to.not.eq(keyExchange2.getDiffieHellmanKeys().publicKey); + }); + + it('should be able to retrieve the invite mail from the second account', async () => { + const result = await mailbox2.getMails(1, 0); + const keys = Object.keys(result.mails); + expect(keys.length).to.eq(1); + }); + + it('should be able retrieve the encrypted communication key with the public key of account 2', async () => { + const result = await mailbox2.getMails(1, 0); + const keys = Object.keys(result.mails); + expect(result.mails[keys[0]].content.attachments[0].type).to.equal('commKey'); + let profile = new Profile({ + nameResolver: await TestUtils.getNameResolver(web3), + defaultCryptoAlgo: 'aes', + dataContract: await TestUtils.getDataContract(web3, ipfs), + contractLoader: await TestUtils.getContractLoader(web3), + ipld, + executor: await TestUtils.getExecutor(web3), + accountId: result.mails[keys[0]].content.from, + }); + + const publicKeyProfile = await profile.getPublicKey(); + const commSecret = keyExchange2.computeSecretKey(publicKeyProfile); + commKey = await keyExchange2.decryptCommKey(result.mails[keys[0]].content.attachments[0].key, commSecret.toString('hex')); + }); + + it('should not be able to decrypt the communication key when a third person gets the message', async () => { + const result = await mailbox2.getMails(1, 0); + const keys = Object.keys(result.mails); + expect(result.mails[keys[0]].content.attachments[0].type).to.equal('commKey'); + let profile = new Profile({ + nameResolver: await TestUtils.getNameResolver(web3), + defaultCryptoAlgo: 'aes', + dataContract: await TestUtils.getDataContract(web3, ipfs), + contractLoader: await TestUtils.getContractLoader(web3), + ipld, + executor: await TestUtils.getExecutor(web3), + accountId: result.mails[keys[0]].content.from, + }); + const keyExchangeOptions = { + mailbox, + cryptoProvider: TestUtils.getCryptoProvider(), + defaultCryptoAlgo: 'aes', + account: accounts[2], + keyProvider: TestUtils.getKeyProvider(), + }; + + const blackHat = new KeyExchange(keyExchangeOptions); + const publicKeyProfile = await profile.getPublicKey(); + const commSecret = blackHat.computeSecretKey(publicKeyProfile); + await expect(blackHat.decryptCommKey(result.mails[keys[0]].content.attachments[0].key, commSecret.toString('hex'))).to.be.rejected; + }); + + it('should be able to send an invitation to a remote account', async () => { + const remoteAddress = ''; + const profileLocal = new Profile({ + nameResolver: await TestUtils.getNameResolver(web3), + defaultCryptoAlgo: 'aes', + dataContract: await TestUtils.getDataContract(web3, ipfs), + contractLoader: await TestUtils.getContractLoader(web3), + ipld, + executor: await TestUtils.getExecutor(web3), + accountId: accounts[1], + }); + const foreignPubkey = await profileLocal.getPublicKey(); + const commKey = await keyExchange1.generateCommKey(); + await keyExchange1.sendInvite(accounts[1], foreignPubkey, commKey, 'hi'); + await profile.addContactKey(accounts[1], 'commKey', commKey); + await profile.storeForAccount(profile.treeLabels.addressBook); + }); +}); diff --git a/src/keyExchange.ts b/src/keyExchange.ts new file mode 100644 index 00000000..a17aa29c --- /dev/null +++ b/src/keyExchange.ts @@ -0,0 +1,215 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import crypto = require('crypto-browserify'); + +import { + ContractLoader, + Ipfs, + KeyProvider, + Logger, + LoggerOptions, + NameResolver, +} from '@evan.network/dbcp'; + +import { Aes } from './encryption/aes'; +import { CryptoProvider } from './encryption/crypto-provider'; +import { Ipld } from './dfs/ipld'; +import { Mail, Mailbox } from './mailbox'; + + +/** + * parameters for KeyExchange constructor + */ +export interface KeyExchangeOptions extends LoggerOptions { + account: string; + cryptoProvider: CryptoProvider; + defaultCryptoAlgo: string; + keyProvider: KeyProvider; + mailbox: Mailbox; + privateKey?: string; + publicKey?: string; +} + +/** + * The KeyExchange module is used to exchange communication keys between two parties, assuming that + * both have created a profile and public have a public facing partial Diffie Hellman key part (the + * combination of their own secret and the shared secret) + * + * @class KeyExchange (name) + */ +export class KeyExchange extends Logger { + + private SHARED_SECRET = Buffer.from('a832d7a4c60473d4fcddabf5c31f5b64dcb2382bbebbeb7c49b6cfc2f08fe9c3', 'hex'); + private diffieHellman: any; + private COMM_KEY_CONTEXT = 'mailboxKeyExchange'; + private mailbox: Mailbox; + private cryptoProvider: CryptoProvider; + private defaultCryptoAlgo: string; + private account: string; + private keyProvider: KeyProvider; + private aes: Aes; + + public publicKey: string; + + /** + * Creates an instance of KeyExchange. + * @param {KeyExchangeOptions} options + * @memberof KeyExchange + */ + constructor(options: KeyExchangeOptions) { + super(options); + this.aes = new Aes(); + this.mailbox = options.mailbox; + this.diffieHellman = crypto.createDiffieHellman(this.SHARED_SECRET); + + if (options.publicKey && options.privateKey) { + this.diffieHellman.setPublicKey(options.publicKey); + this.diffieHellman.setPrivateKey(options.privateKey); + } else { + this.diffieHellman.generateKeys('hex'); + } + this.cryptoProvider = options.cryptoProvider; + this.defaultCryptoAlgo = options.defaultCryptoAlgo; + this.account = options.account; + this.keyProvider = options.keyProvider; + } + + /** + * combines given partial key from another profile with own private key + * + * @param {string} partialKey publicKey(shared + private) from another profile + * @return {string} combined exchange key + */ + public computeSecretKey(partialKey: string) { + const secret = this.diffieHellman.computeSecret(Buffer.from(partialKey, 'hex'), 'hex'); + return secret; + } + + /** + * decrypts a given communication key with an exchange key + * + * @param {string} encryptedCommKey encrypted communications key received from another account + * @param {string} exchangeKey Diffie Hellman exchange key from computeSecretKey + * @return {Promise} commKey as a buffer + */ + public async decryptCommKey(encryptedCommKey: string, exchangeKey: string): Promise { + const cryptor = this.cryptoProvider.getCryptorByCryptoAlgo(this.defaultCryptoAlgo); + return await cryptor.decrypt(Buffer.from(encryptedCommKey, 'hex'), { key: exchangeKey, }); + } + + /** + * returns the public and private key from the diffieHellman + * + * @return {any} public and private key from the diffieHellman. + */ + public getDiffieHellmanKeys() { + return { + publicKey: this.diffieHellman.getPublicKey(), + privateKey: this.diffieHellman.getPrivateKey(), + } + } + + /** + * generates a new communication key end returns the hex string + * + * @return {string} comm key as string + */ + public async generateCommKey() { + return this.aes.generateKey(); + } + + /** + * creates a bmail for exchanging comm keys + * + * @param {string} from sender accountId + * @param {any} mailContent bmail metadata + * @param {string} encryptedCommKey comm key, that should be exchanged + * @return {Mail} bmail for key exchange + */ + public getExchangeMail = (from: string, mailContent: any, encryptedCommKey?: string): Mail => { + const ret: Mail = { + content: { + from, + fromAlias : mailContent.fromAlias, + fromMail : mailContent.fromMail, + title: mailContent.title, + body: mailContent.body, + attachments: mailContent.attachments || [] + } + }; + + ret.content.title = ret.content.title || 'Contact request'; + ret.content.body = ret.content.body || `Hi, + +I'd like to add you as a contact. Do you accept my invitation? + +With kind regards, + +${mailContent && mailContent.fromAlias || from}`; + ret.content.attachments.push({ + type: 'commKey', + key: encryptedCommKey + }); + return ret; + }; + + /** + * sends a mailbox mail to the target account with the partial key for the key exchange + * + * @param {string} targetAccount receiver of the invitation + * @param {string} targetPublicKey combination of shared secret plus targetAccounts + * private secret + * @param {string} commKey communication key between sender and + * targetAccount + * @param {any} mailContent mail to send + * @return {Promise} resolved when done + */ + public async sendInvite(targetAccount: string, targetPublicKey: string, commKey: string, mailContent: any): Promise { + const secret = this.computeSecretKey(targetPublicKey).toString('hex'); + const cryptor = this.cryptoProvider.getCryptorByCryptoAlgo(this.defaultCryptoAlgo); + const encryptedCommKey = await cryptor.encrypt(commKey, { key: secret, }); + await this.mailbox.sendMail( + this.getExchangeMail(this.account, mailContent, encryptedCommKey.toString('hex')), + this.account, + targetAccount, + '0', + 'mailboxKeyExchange' + ); + } + + /** + * set the private and public key on the current diffieHellman object + * + * @param {string} publicKey public Diffie Hellman key + * @param {string} privateKey private Diffie Hellman key + */ + public setPublicKey(publicKey: string, privateKey: string) { + this.diffieHellman.setPrivateKey(Buffer.from(privateKey, 'hex')); + this.diffieHellman.setPublicKey(Buffer.from(publicKey, 'hex')); + } +} diff --git a/src/mailbox.spec.ts b/src/mailbox.spec.ts new file mode 100644 index 00000000..dfc2eb3e --- /dev/null +++ b/src/mailbox.spec.ts @@ -0,0 +1,234 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect } from 'chai'; +import BigNumber = require('bignumber.js'); + +import { + Ipfs, + NameResolver, +} from '@evan.network/dbcp'; + +import { accounts } from './test/accounts'; +import { Ipld } from './dfs/ipld'; +import { Mail, Mailbox, MailboxOptions } from './mailbox'; +import { TestUtils } from './test/test-utils'; + +describe('Mailbox class', function() { + this.timeout(600000); + let ipfs: Ipfs; + let mailbox: Mailbox; + let web3; + const random = Math.random(); + const getTestMail = (to): Mail => ({ + content: { + from: accounts[0], + to, + title: 'talking to myself', + body: `hi, me. I like random numbers, for example ${random}`, + attachments: [ + { + type: 'sharedExchangeKey', + key: '' + } + ] + }, + }); + const getTestAnswer = (parentId): Mail => ({ + parentId, + content: { + body: `but my favorite random number is ${random}`, + from: null, + title: 'I like random numbers as well', + }, + }); + + before(async () => { + web3 = TestUtils.getWeb3(); + ipfs = await TestUtils.getIpfs(); + mailbox = new Mailbox({ + mailboxOwner: accounts[0], + nameResolver: await TestUtils.getNameResolver(web3), + ipfs, + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider: TestUtils.getCryptoProvider(), + keyProvider: TestUtils.getKeyProvider(), + defaultCryptoAlgo: 'aes', + }); + }); + + after(async () => { + web3.currentProvider.connection.close(); + await ipfs.stop(); + }); + + it('should be able to send a mail', async () => { + const startTime = Date.now(); + await mailbox.sendMail(getTestMail(accounts[0]), accounts[0], accounts[0]); + const result = await mailbox.getMails(1, 0); + const keys = Object.keys(result.mails); + expect(keys.length).to.eq(1); + expect(result.mails[keys[0]].content.sent).to.be.ok; + expect(result.mails[keys[0]].content.sent).to.be.gt(startTime); + expect(result.mails[keys[0]].content.sent).to.be.lt(Date.now()); + delete result.mails[keys[0]].content.sent; + expect(result.mails[keys[0]].content).to.deep.eq(getTestMail(accounts[0]).content); + }); + + it('should be able to load anything', async () => { + const mails = await mailbox.getMails(); + expect(mails).not.to.be.undefined; + expect(mails.totalResultCount).to.be.gte(0); + expect(Object.keys(mails.mails).length).to.be.gte(0); + }); + + it('should be able to get a set amount of mails', async () => { + await mailbox.sendMail(getTestMail(accounts[0]), accounts[0], accounts[0]); + let mails; + mails = await mailbox.getMails(1) + expect(mails).not.to.be.undefined; + expect(Object.keys(mails.mails).length).to.eq(1); + mails = await mailbox.getMails(2) + expect(mails).not.to.be.undefined; + expect(Object.keys(mails.mails).length).to.eq(2); + }); + + it('should be able to load all mails in the correct order', async () => { + // get last two mails + const mailSet1 = await mailbox.getMails(1, 0); + const mailSet2 = await mailbox.getMails(1, 1); + + // check that mails were returned in correct order + const indexMail1 = parseInt(Object.keys(mailSet1.mails)[0], 16); + const indexMail2 = parseInt(Object.keys(mailSet2.mails)[0], 16); + expect(indexMail1).to.be.gt(indexMail2); + }); + + it('should be able to send and retrieve answers', async () => { + await mailbox.sendMail(getTestMail(accounts[0]), accounts[0], accounts[0]); + let result = await mailbox.getMails(1, 0); + let keys = Object.keys(result.mails); + expect(keys.length).to.eq(1); + const initialMailId = keys[0]; + const answer = getTestAnswer(initialMailId); + answer.content.from = accounts[0]; + await mailbox.sendAnswer(Object.assign({}, answer), accounts[0], accounts[0]); + + result = await mailbox.getAnswersForMail(initialMailId); + expect(result).not.to.be.undefined; + expect(result.totalResultCount).to.eq(1); + keys = Object.keys(result.mails); + const answerId = keys[0]; + const mail = result.mails[answerId]; + Ipld.purgeCryptoInfo(mail); + expect(mail).to.deep.eq(answer); + }); + + it('should be able to read mails sent from another user', async () => { + const startTime = Date.now(); + // mailbox user 2 + const mailbox2 = new Mailbox({ + mailboxOwner: accounts[1], + nameResolver: await TestUtils.getNameResolver(web3), + ipfs, + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider: TestUtils.getCryptoProvider(), + keyProvider: TestUtils.getKeyProvider(), + defaultCryptoAlgo: 'aes', + }); + + await mailbox.sendMail(getTestMail(accounts[1]), accounts[0], accounts[1]); + + const result = await mailbox2.getMails(1, 0); + const keys = Object.keys(result.mails); + expect(keys.length).to.eq(1); + expect(result.mails[keys[0]].content.sent).to.be.ok; + expect(result.mails[keys[0]].content.sent).to.be.gt(startTime); + expect(result.mails[keys[0]].content.sent).to.be.lt(Date.now()); + delete result.mails[keys[0]].content.sent; + expect(result.mails[keys[0]].content).to.deep.eq(getTestMail(accounts[1]).content); + }); + + it('should be able to send UTC tokens with a mail', async () => { + const startTime = Date.now(); + await mailbox.init(); + const balanceToSend = new BigNumber(web3.utils.toWei('1', 'kWei')); + const balanceBefore = new BigNumber(await web3.eth.getBalance(accounts[0])); + const mailboxBalanceBefore = new BigNumber(await web3.eth.getBalance(mailbox.mailboxContract.options.address)); + await mailbox.sendMail(getTestMail(accounts[1]), accounts[0], accounts[1], `0x${balanceToSend.toString(16)}`); + const mailboxBalanceAfter = new BigNumber(await web3.eth.getBalance(mailbox.mailboxContract.options.address)); + const balanceAfter = new BigNumber(await web3.eth.getBalance(accounts[0])); + expect(balanceAfter.plus(balanceToSend).lte(balanceBefore)).to.be.true; // before - cost = after + value // (sender pays cost) + expect(mailboxBalanceBefore.plus(balanceToSend).eq(mailboxBalanceAfter)).to.be.true; // before + value = after + }); + + it('should allow checking balance for a mail', async () => { + const startTime = Date.now(); + const mailbox2 = new Mailbox({ + mailboxOwner: accounts[1], + nameResolver: await TestUtils.getNameResolver(web3), + ipfs, + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider: TestUtils.getCryptoProvider(), + keyProvider: TestUtils.getKeyProvider(), + defaultCryptoAlgo: 'aes', + }); + const balanceToSend = new BigNumber(web3.utils.toWei('0.1', 'Ether')); + await mailbox.sendMail(getTestMail(accounts[1]), accounts[0], accounts[1], web3.utils.toWei('0.1', 'Ether')); + const result = await mailbox2.getMails(1, 0); + const keys = Object.keys(result.mails); + const mailBalance = await mailbox2.getBalanceFromMail(keys[0]); + expect(balanceToSend.eq(mailBalance)).to.be.true; + }); + + it('should allow withdrawing UTC tokens for a mail', async () => { + const startTime = Date.now(); + const mailbox2 = new Mailbox({ + mailboxOwner: accounts[1], + nameResolver: await TestUtils.getNameResolver(web3), + ipfs, + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider: TestUtils.getCryptoProvider(), + keyProvider: TestUtils.getKeyProvider(), + defaultCryptoAlgo: 'aes', + }); + const balanceToSend = new BigNumber(web3.utils.toWei('0.1', 'Ether')); + await mailbox.sendMail(getTestMail(accounts[1]), accounts[0], accounts[1], web3.utils.toWei('0.1', 'Ether')); + const result = await mailbox2.getMails(1, 0); + const keys = Object.keys(result.mails); + const balanceBefore = new BigNumber(await web3.eth.getBalance(accounts[1])); + const mailboxBalanceBefore = new BigNumber(await web3.eth.getBalance(mailbox.mailboxContract.options.address)); + await mailbox2.withdrawFromMail(keys[0], accounts[1]); + const mailboxBalanceAfter = new BigNumber(await web3.eth.getBalance(mailbox.mailboxContract.options.address)); + const balanceAfter = new BigNumber(await web3.eth.getBalance(accounts[1])); + expect(balanceBefore.plus(balanceToSend).gte(balanceAfter)).to.be.true; // before + value - cost = after // (withdrawer pays cost) + expect(mailboxBalanceAfter.plus(balanceToSend).eq(mailboxBalanceBefore)).to.be.true; // before - value = after + }); + + it('should now allow withdrawing UTC tokens for a mail that has no tokens', async () => {}); +}); diff --git a/src/mailbox.ts b/src/mailbox.ts new file mode 100644 index 00000000..c0f3ff68 --- /dev/null +++ b/src/mailbox.ts @@ -0,0 +1,398 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import { + ContractLoader, + Ipfs, + Logger, + LoggerOptions, + NameResolver, + KeyProvider, +} from '@evan.network/dbcp'; + +import { CryptoProvider } from './encryption/crypto-provider'; +import { Ipld } from './dfs/ipld'; + + +/** + * mail object + */ +export interface Mail { + content: { + attachments?: any[], + body?: string, + from?: string, + fromAlias?: string, + fromMail?: string, + sent?: number, + title?: string, + to?: string, + }, + answers?: MailboxResult, + parentId?: string, +} + +/** + * collection of mails + */ +export interface MailboxResult { + mails: { [index: string]: Mail }, + totalResultCount: number; +} + +/** + * parameters for Mailbox constructor + */ +export interface MailboxOptions extends LoggerOptions { + mailboxOwner: string; + nameResolver: NameResolver; + ipfs: Ipfs; + contractLoader: ContractLoader; + cryptoProvider: CryptoProvider; + keyProvider: KeyProvider; + defaultCryptoAlgo: string; +} + +/** + * mailbox helper class for sending and retrieving mails and answers + * + * @class Mailbox (name) + */ +export class Mailbox extends Logger { + mailboxOwner: string; + nameResolver: NameResolver; + contractLoader: ContractLoader; + mailboxContract: any; + ipfs: Ipfs; + cryptoProvider: CryptoProvider; + keyProvider: KeyProvider; + defaultCryptoAlgo: string; + initialized: boolean; + + constructor(options: MailboxOptions) { + super(options); + this.mailboxOwner = options.mailboxOwner; + this.nameResolver = options.nameResolver; + this.contractLoader = options.contractLoader; + this.ipfs = options.ipfs; + this.keyProvider = options.keyProvider; + this.cryptoProvider = options.cryptoProvider; + this.defaultCryptoAlgo = options.defaultCryptoAlgo; + } + + /** + * initialize mailbox module + * + * @return {Promise} resolved when done + */ + async init(): Promise { + if (this.initialized) { + return; + } else { + const domain = this.nameResolver.getDomainName(this.nameResolver.config.domains.mailbox); + const address = await this.nameResolver.getAddress(domain); + this.mailboxContract = this.contractLoader.loadContract('MailBox', address); + this.initialized = true; + } + } + + async getSentMails(count = 10, offset = 0) { + return await this.getMails(count, offset, 'Sent'); + } + + /** + * gets received from mailboxOwner + * + * @param {number} count number of mails to retrieve (default 10) + * @param {number} offset mail offset (default 0) + * @return {Promise} The received mails. + */ + async getReceivedMails(count = 10, offset = 0) { + return await this.getMails(count, offset, 'Received'); + } + + /** + * Gets the last n mails, resolved contents + * + * @param {number} count retrieve up to this many answers (for paging) + * @param {number} offset skip this many answers (for paging) + * @param {string} type retrieve sent or received mails + * @return {Promise} resolved mails + */ + async getMails(count = 10, offset = 0, type = 'Received'): Promise { + await this.init(); + const results: MailboxResult = { + mails: {}, + totalResultCount: 0, + }; + + const executor = this.nameResolver.executor; + const listAddressHash = await executor.executeContractCall( + this.mailboxContract, `getMy${type}Mails`, { from: this.mailboxOwner, }); + if (listAddressHash !== '0x0000000000000000000000000000000000000000000000000000000000000000') { + const listAddress = this.nameResolver.bytes32ToAddress(listAddressHash); + const listContract = this.contractLoader.loadContract('DataStoreList', listAddress); + const listLength = await executor.executeContractCall(listContract, 'length'); + results.totalResultCount = parseInt(listLength, 10); + if (results.totalResultCount) { + let ipld: Ipld; + const mailIds = await this.nameResolver.getArrayFromListContract(listContract, count, offset, true); + const originator = this.nameResolver.soliditySha3.apply(this.nameResolver, [ + this.nameResolver.soliditySha3(this.mailboxOwner), this.nameResolver.soliditySha3(this.mailboxOwner), ].sort()); + ipld = new Ipld({ + ipfs: this.ipfs, + keyProvider: this.keyProvider, + cryptoProvider: this.cryptoProvider, + defaultCryptoAlgo: this.defaultCryptoAlgo, + originator, + nameResolver: this.nameResolver, + }); + for (let mailId of mailIds) { + try { + const mailResult = await executor.executeContractCall(this.mailboxContract, 'getMail', mailId); + const mail = await ipld.getLinkedGraph(mailResult.data); + const hashedSender = this.nameResolver.soliditySha3(mail.content.from); + if (hashedSender !== mailResult.sender) { + throw new Error(`mail claims to be sent from ${hashedSender}, but was sent from ${mailResult.sender}`); + } else { + results.mails[mailId] = mail; + } + } catch (ex) { + this.log(`could not unpack mail: "${mailId}"; ${ex.message || ex}`, 'warning'); + results.mails[mailId] = null; + } + } + } + } + return results; + } + + /** + * Gets one single mail directly + * + * @param {string} mail mail to resolve (mailId or hash) + * @return {Promise} The mail. + */ + async getMail(mail: string): Promise { + await this.init(); + const executor = this.nameResolver.executor; + let ipld: Ipld; + const originator = this.nameResolver.soliditySha3.apply(this.nameResolver, [ + this.nameResolver.soliditySha3(this.mailboxOwner), this.nameResolver.soliditySha3(this.mailboxOwner), ].sort()); + ipld = new Ipld({ + ipfs: this.ipfs, + keyProvider: this.keyProvider, + cryptoProvider: this.cryptoProvider, + defaultCryptoAlgo: this.defaultCryptoAlgo, + originator, + nameResolver: this.nameResolver, + }); + + try { + if (mail.startsWith('Qm')) { + return await ipld.getLinkedGraph(mail); + } else { + const mailResult = await executor.executeContractCall(this.mailboxContract, 'getMail', mail); + const mailItem = await ipld.getLinkedGraph(mailResult.data); + const hashedSender = this.nameResolver.soliditySha3(mailItem.content.from); + if (hashedSender !== mailResult.sender) { + throw new Error(`mail claims to be sent from ${hashedSender}, but was sent from ${mailResult.sender}`); + } else { + return mailItem; + } + } + } catch (ex) { + this.log(`could not decrypt mail: "${mail}"; ${ex.message || ex}`, 'warning'); + return null; + } + } + + /** + * Gets answer tree for mail, traverses subanswers as well + * + * @param {string} mailId mail to resolve + * @param {number} count retrieve up to this many answers (for paging) + * @param {number} offset skip this many answers (for paging) + * @return {Promise} answer tree for mail + */ + async getAnswersForMail(mailId: string, count = 5, offset = 0): Promise { + await this.init(); + const results: MailboxResult = { + mails: {}, + totalResultCount: 0, + }; + + const executor = this.nameResolver.executor; + const listAddressHash = await executor.executeContractCall( + this.mailboxContract, 'getAnswersForMail', mailId, { from: this.mailboxOwner, }); + if (listAddressHash !== '0x0000000000000000000000000000000000000000000000000000000000000000') { + const listAddress = this.nameResolver.bytes32ToAddress(listAddressHash); + const listContract = this.contractLoader.loadContract('DataStoreList', listAddress); + const listLength = await executor.executeContractCall(listContract, 'length'); + results.totalResultCount = parseInt(listLength, 10); + if (results.totalResultCount) { + let ipld: Ipld; + const mailIds = await this.nameResolver.getArrayFromListContract(listContract, count, offset, true); + const originator = this.nameResolver.soliditySha3.apply(this.nameResolver, [ + this.nameResolver.soliditySha3(this.mailboxOwner), this.nameResolver.soliditySha3(this.mailboxOwner), ].sort()); + ipld = new Ipld({ + ipfs: this.ipfs, + keyProvider: this.keyProvider, + cryptoProvider: this.cryptoProvider, + defaultCryptoAlgo: this.defaultCryptoAlgo, + originator, + nameResolver: this.nameResolver, + }); + for (let answerId of mailIds) { + try { + const mailResult = await executor.executeContractCall(this.mailboxContract, 'getMail', answerId); + const mail = await ipld.getLinkedGraph(mailResult.data); + const hashedSender = this.nameResolver.soliditySha3(mail.content.from); + if (hashedSender !== mailResult.sender) { + throw new Error(`mail claims to be sent from ${hashedSender}, but was sent from ${mailResult.sender}`); + } else { + results.mails[answerId] = mail; + } + } catch (ex) { + this.log(`could not unpack answer: "${answerId}"; ${ex.message || ex}`, 'warning'); + results.mails[answerId] = null; + } + } + } + } + + return results; + } + + /** + * sends a mail to given target + * + * @param {Mail} mail a mail to send + * @param {string} from account id to send mail from + * @param {string} to account id to send mail to + * @param {string} value (optional) UTC amount to send with mail in Wei can be + * created with web3[.utils].toWei(...) + * @param {string} context encrypt mail with different context + * @return {Promise} resolved when done + */ + async sendMail(mail: Mail, from: string, to: string, value = '0', context?: string): Promise { + await this.init(); + mail.content.from = from; + mail.content.sent = new Date().getTime(); + mail.content.to = to; + const combinedHash = this.nameResolver.soliditySha3.apply(this.nameResolver, + [this.nameResolver.soliditySha3(to), this.nameResolver.soliditySha3(this.mailboxOwner)].sort()); + const ipld: Ipld = new Ipld({ + ipfs: this.ipfs, + keyProvider: this.keyProvider, + cryptoProvider: this.cryptoProvider, + originator: context ? this.nameResolver.soliditySha3(context) : combinedHash, + defaultCryptoAlgo: this.defaultCryptoAlgo, + nameResolver: this.nameResolver, + }); + const hash = await ipld.store(mail); + await this.nameResolver.executor.executeContractTransaction( + this.mailboxContract, + 'sendMail', + { from, autoGas: 1.1, value, }, + [ to ], + hash, + ); + } + + /** + * send answer to a mail + * + * @param {Mail} mail mail to send as a reply + * @param {string} from sender address + * @param {string} to receiver address + * @param {string} value (optional) UTC amount to send with mail in Wei can be + * created with web3[.utils].toWei(...) + * @return {Promise} resolved when done + */ + async sendAnswer(mail: Mail, from: string, to: string, value = '0'): Promise { + await this.init(); + mail.content.sent = new Date().getTime(); + const combinedHash = this.nameResolver.soliditySha3.apply(this.nameResolver, + [this.nameResolver.soliditySha3(to), this.nameResolver.soliditySha3(this.mailboxOwner)].sort()); + const ipld: Ipld = new Ipld({ + ipfs: this.ipfs, + keyProvider: this.keyProvider, + cryptoProvider: this.cryptoProvider, + originator: combinedHash, + defaultCryptoAlgo: this.defaultCryptoAlgo, + nameResolver: this.nameResolver, + }); + const parentId = mail.parentId; + const hash = await ipld.store(mail); + await this.nameResolver.executor.executeContractTransaction( + this.mailboxContract, + 'sendAnswer', + { from, autoGas: 1.1, value, }, + [ to ], + hash, + parentId, + ); + } + + /** + * returns amount of UTC deposited for a mail + * + * @param {string} mailId mail to resolve + * @return {Promise} balance of the mail in Wei can be converted with + * web3[.utils].fromWei(...) + */ + async getBalanceFromMail(mailId: string): Promise { + await this.init(); + // mailboxOwner + return this.nameResolver.executor.executeContractCall( + this.mailboxContract, + 'getBalanceFromMail', + mailId, + { from: this.mailboxOwner, }, + ); + } + + /** + * transfers mails deposited UTC tokens to target account + * + * @param {string} mailId mail to resolve + * @param {string} recipient account, that receives the EVEs + * @return {Promise} resolved when done + */ + async withdrawFromMail(mailId: string, recipient: string): Promise { + await this.init(); + // mailboxOwner + await this.nameResolver.executor.executeContractTransaction( + this.mailboxContract, + 'withdrawFromMail', + { from: this.mailboxOwner, autoGas: 1.1, }, + mailId, + recipient, + ); + } +} diff --git a/src/onboarding.spec.ts b/src/onboarding.spec.ts new file mode 100644 index 00000000..3a89394b --- /dev/null +++ b/src/onboarding.spec.ts @@ -0,0 +1,92 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect } from 'chai'; + +import { KeyProvider } from '@evan.network/dbcp'; + +import { accounts } from './test/accounts'; +import { config } from './config'; +import { Ipld } from './dfs/ipld'; +import { InvitationMail, Onboarding } from './onboarding'; +import { Mailbox } from './mailbox'; +import { Profile } from './profile/profile'; +import { TestUtils } from './test/test-utils'; + +describe('Onboarding helper', function() { + this.timeout(600000); + let ipfs; + let eventHub; + let mailbox: Mailbox; + let nameResolver; + let onboarding: Onboarding; + let web3; + + before(async () => { + web3 = TestUtils.getWeb3(); + ipfs = await TestUtils.getIpfs(); + const keyProvider = await TestUtils.getKeyProvider(); + const ipld = await TestUtils.getIpld(ipfs, keyProvider); + const profile = await TestUtils.getProfile(web3, ipfs, ipld); + await profile.loadForAccount(accounts[0]); + keyProvider.init(profile); + keyProvider.currentAccount = accounts[0]; + + nameResolver = await TestUtils.getNameResolver(web3); + mailbox = new Mailbox({ + mailboxOwner: accounts[0], + nameResolver: await TestUtils.getNameResolver(web3), + ipfs, + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider: TestUtils.getCryptoProvider(), + keyProvider, + defaultCryptoAlgo: 'aes', + }); + const executor = await TestUtils.getExecutor(web3); + onboarding = new Onboarding({ + mailbox, + smartAgentId: config.smartAgents.onboarding.accountId, + executor, + }); + }); + + after(async () => { + web3.currentProvider.connection.close(); + await ipfs.stop(); + }); + + it('should be able to send an invitation via smart agent', async () => { + await onboarding.sendInvitation({ + fromAlias: 'example inviter', + to: 'example invitee ', + lang: 'en', + subject: 'evan.network Onboarding Invitation', + body: 'I\'d like to welcome you on board.', + }, web3.utils.toWei('1')); + }); +}); diff --git a/src/onboarding.ts b/src/onboarding.ts new file mode 100644 index 00000000..39c426c5 --- /dev/null +++ b/src/onboarding.ts @@ -0,0 +1,99 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import { + ContractLoader, + Ipfs, + Logger, + LoggerOptions, + NameResolver, + KeyProvider, +} from '@evan.network/dbcp'; + +import { CryptoProvider } from './encryption/crypto-provider'; +import { Ipld } from './dfs/ipld'; +import { Mail, Mailbox } from './mailbox'; + + +/** + * mail that will be sent to invitee + */ +export interface InvitationMail { + body: string, + subject: string, + to: string, + fromAlias?: string, + lang?: string, +} + +/** + * parameters for Onboarding constructor + */ +export interface OnboardingOptions extends LoggerOptions { + mailbox: Mailbox, + smartAgentId: string, + executor: any, +} + +/** + * helper class for sending onboarding mails + * + * @class Mailbox (name) + */ +export class Onboarding extends Logger { + options: OnboardingOptions; + + constructor(optionsInput: OnboardingOptions) { + super(optionsInput); + this.options = optionsInput; + } + + /** + * send invitation to another user via smart agent that sends a mail + * + * @param {InvitationMail} invitation mail that will be sent to invited person + * @param {string} weiToSend amount of ETC to transfert to new member, can be + * created with web3.utils.toWei(10, 'ether') + * [web3 >=1.0] / web.toWei(10, 'ether') [web3 < 1.0] + * @return {Promise} resolved when done + */ + async sendInvitation(invitation: InvitationMail, weiToSend: string): Promise { + // build bmail container + const mail: Mail = { + content: { + attachments: [{ + type: 'onboardingEmail', + data: JSON.stringify(invitation), + }] + } + }; + + // send mail to smart agent + await this.options.mailbox.sendMail( + mail, this.options.mailbox.mailboxOwner, this.options.smartAgentId, weiToSend); + } +} diff --git a/src/profile/business-center-profile.spec.ts b/src/profile/business-center-profile.spec.ts new file mode 100644 index 00000000..c3d374c5 --- /dev/null +++ b/src/profile/business-center-profile.spec.ts @@ -0,0 +1,108 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect } from 'chai'; +import IpfsServer = require('ipfs'); + +import { + CryptoProvider, + NameResolver, + SignerInternal, +} from '@evan.network/dbcp'; + +import { accounts } from '../test/accounts'; +import { accountMap } from '../test/accounts'; +import { Aes } from '../encryption/aes'; +import { BusinessCenterProfile } from './business-center-profile'; +import { config } from '../config'; +import { CryptoProvider } from '../encryption/crypto-provider'; +import { Ipld } from '../dfs/ipld'; +import { TestUtils } from '../test/test-utils'; + + +describe('BusinessCenterProfile helper', function() { + this.timeout(600000); + let ipld: Ipld; + let nameResolver: NameResolver; + let ensName; + let businessCenterDomain; + let web3; + let cryptoProvider = TestUtils.getCryptoProvider(); + let bcAddress; + const sampleProfile = { + alias: 'fnord', + contact: 'fnord@contoso.com', + }; + + before(async () => { + web3 = TestUtils.getWeb3(); + ipld = await TestUtils.getIpld(); + + nameResolver = await TestUtils.getNameResolver(web3); + ensName = nameResolver.getDomainName(config.nameResolver.domains.profile); + businessCenterDomain = nameResolver.getDomainName(config.nameResolver.domains.businessCenter); + }); + + after(async () => { + web3.currentProvider.connection.close(); + await ipld.ipfs.stop(); + }); + + it('should be able to set and load a profile for a given user in a business center', async () => { + // use own key for test + ipld.keyProvider.keys[nameResolver.soliditySha3(businessCenterDomain)] = ipld.keyProvider.keys[nameResolver.soliditySha3(accounts[0])]; + // create profile + const profile = new BusinessCenterProfile({ + ipld, + nameResolver, + defaultCryptoAlgo: 'aes', + bcAddress: businessCenterDomain, + cryptoProvider:TestUtils.getCryptoProvider() + }); + await profile.setContactCard(JSON.parse(JSON.stringify(sampleProfile))); + + // store + const from = Object.keys(accountMap)[0]; + await profile.storeForBusinessCenter(businessCenterDomain, from); + + // load + const newProfile = new BusinessCenterProfile({ + ipld, + nameResolver, + defaultCryptoAlgo: 'aes', + bcAddress: businessCenterDomain, + cryptoProvider:TestUtils.getCryptoProvider() + }); + await newProfile.loadForBusinessCenter(businessCenterDomain, from); + + // test contacts + const loadedProfile = await newProfile.getContactCard(); + Ipld.purgeCryptoInfo(loadedProfile); + expect(loadedProfile).to.deep.eq(sampleProfile); + }); +}); diff --git a/src/profile/business-center-profile.ts b/src/profile/business-center-profile.ts new file mode 100644 index 00000000..75f34089 --- /dev/null +++ b/src/profile/business-center-profile.ts @@ -0,0 +1,205 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import { + Logger, + LoggerOptions, + NameResolver, +} from '@evan.network/dbcp'; + +import { CryptoProvider } from '../encryption/crypto-provider'; +import { Ipld } from '../dfs/ipld'; + +function saveGet(root, labels) { + const split = labels.split('/'); + let pointer = root; + for (let i = 0; i < split.length; i++) { + let sublabel = split[i]; + if (pointer.hasOwnProperty(sublabel)) { + pointer = pointer[sublabel]; + } else { + pointer = undefined; + break; + } + } + return pointer; +} + + +function saveSet(root, labels, child) { + const split = labels.split('/'); + let pointer = root; + for (let i = 0; i < split.length; i++) { + let sublabel = split[i]; + if (i === split.length - 1) { + pointer[sublabel] = child; + } else if (!pointer[sublabel]) { + pointer[sublabel] = {}; + } + pointer = pointer[sublabel]; + } +} + + +/** + * parameters for Profile constructor + */ +export interface ProfileOptions extends LoggerOptions { + ipld: Ipld; + nameResolver: NameResolver; + defaultCryptoAlgo: string; + cryptoProvider: CryptoProvider + bcAddress: string; + ipldData?: any; +} + + +export interface DappBookmark { + title: string; + description: string; + img: string; + primaryColor: string; + secondaryColor?: string; +} + + +/** + * profile helper class, builds profile graphs + * + * @class Profile (name) + */ +export class BusinessCenterProfile extends Logger { + ipldData: any; + ipld: Ipld; + nameResolver: NameResolver; + defaultCryptoAlgo: string; + cryptoProvider: CryptoProvider; + bcAddress: string; + + constructor(options: ProfileOptions) { + super(options); + this.ipld = options.ipld; + this.ipldData = options.ipldData || {}; + this.nameResolver = options.nameResolver; + this.bcAddress = options.bcAddress; + this.cryptoProvider = options.cryptoProvider; + this.defaultCryptoAlgo = options.defaultCryptoAlgo; + } + + /** + * store profile in ipfs as an ipfs file that points to a ipld dag + * + * @return {string} hash of the ipfs file + */ + async storeToIpld(): Promise { + const stored = await this.ipld.store(this.ipldData); + return stored; + } + + /** + * load profile from ipfs via ipld dag via ipfs file hash + * + * @param {string} ipldIpfsHash ipfs file hash that points to a file with ipld a hash + * @return {Profile} this profile + */ + async loadFromIpld(ipldIpfsHash: string): Promise { + let loaded; + try { + loaded = await this.ipld.getLinkedGraph(ipldIpfsHash); + } catch (e) { + this.log(`Error getting BC Profile ${e}`, 'debug'); + loaded = { + alias: {} + }; + } + this.ipldData = loaded; + return this; + } + + /** + * get contact card from + * + * @return {any} contact card + */ + async getContactCard(): Promise { + return this.ipld.getLinkedGraph(this.ipldData, 'contactCard'); + } + + /** + * set contact card on current profile + * + * @param {any} contactCard contact card to store + * @return {any} updated tree + */ + async setContactCard(contactCard: any): Promise { + const cryptor = this.cryptoProvider.getCryptorByCryptoAlgo(this.defaultCryptoAlgo); + const cryptoInfo = cryptor.getCryptoInfo(this.nameResolver.soliditySha3(this.bcAddress)); + return this.ipld.set(this.ipldData, 'contactCard', contactCard, false, cryptoInfo); + } + + /** + * stores profile to business centers profile store + * + * @param {string} businessCenterDomain ENS domain name of a business center + * @param {string} account Ethereum account id + * @return {Promise} resolved when done + */ + async storeForBusinessCenter(businessCenterDomain: string, account: string): Promise { + const [stored, address] = await Promise.all([ + this.storeToIpld(), + this.nameResolver.getAddress(businessCenterDomain), + ]); + const contract = this.nameResolver.contractLoader.loadContract('BusinessCenterInterface', address); + await this.nameResolver.executor.executeContractTransaction( + contract, + 'setMyProfile', + { from: account, autoGas: 1.1, }, + stored, + ); + } + + /** + * load profile for given account from global profile contract + * + * @param {string} businessCenterDomain ENS domain name of a business center + * @param {string} account Ethereum account id + * @return {Promise} resolved when done + */ + async loadForBusinessCenter(businessCenterDomain: string, account: string): Promise { + const address = await this.nameResolver.getAddress(businessCenterDomain); + const contract = this.nameResolver.contractLoader.loadContract('BusinessCenterInterface', address); + const hash = await this.nameResolver.executor.executeContractCall(contract, 'getProfile', account); + if (hash === '0x0000000000000000000000000000000000000000000000000000000000000000') { + this.ipldData = { + alias: '', + }; + return Promise.resolve(); + } else { + await this.loadFromIpld(hash); + } + } +} diff --git a/src/profile/profile.spec.ts b/src/profile/profile.spec.ts new file mode 100644 index 00000000..4ff0e041 --- /dev/null +++ b/src/profile/profile.spec.ts @@ -0,0 +1,582 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect } from 'chai'; +// import IpfsServer = require('ipfs'); + +import { + ContractLoader, + KeyProvider, + NameResolver, + SignerInternal, +} from '@evan.network/dbcp'; + +import { accountMap } from '../test/accounts'; +import { accounts } from '../test/accounts'; +import { Aes } from '../encryption/aes'; +import { config } from '../config'; +import { CryptoProvider } from '../encryption/crypto-provider'; +import { DataContract } from '../contracts/data-contract/data-contract'; +import { Ipld } from '../dfs/ipld'; +import { KeyExchange } from '../keyExchange'; +import { Mailbox } from '../mailbox'; +import { Profile } from './profile'; +import { RightsAndRoles } from '../contracts/rights-and-roles'; +import { TestUtils } from '../test/test-utils'; + + +describe('Profile helper', function() { + this.timeout(600000); + let ipfs; + let ipld; + let nameResolver: NameResolver; + let ensName; + let web3; + let dataContract: DataContract; + let contractLoader: ContractLoader; + let keyExchange; + let mailbox; + let executor; + let rightsAndRoles: RightsAndRoles; + let cryptoProvider; + const sampleDesc = { + title: 'sampleTest', + description: 'desc', + img: 'img', + primaryColor: '#FFFFFF', + }; + const sampleUpdateDesc = { + title: 'sampleUpdateTest', + description: 'desc', + img: 'img', + primaryColor: '#FFFFFF', + }; + const emptyProfile = { + bookmarkedDapps: {}, + addressBook: {}, + contracts: {} + } + + before(async () => { + web3 = TestUtils.getWeb3(); + ipfs = await TestUtils.getIpfs(); + ipld = await TestUtils.getIpld(ipfs); + contractLoader = await TestUtils.getContractLoader(web3); + dataContract = await TestUtils.getDataContract(web3, ipld.ipfs) + nameResolver = await TestUtils.getNameResolver(web3); + ensName = nameResolver.getDomainName(config.nameResolver.domains.profile); + cryptoProvider = await TestUtils.getCryptoProvider(); + mailbox = new Mailbox({ + mailboxOwner: accounts[0], + nameResolver: await TestUtils.getNameResolver(web3), + ipfs: ipld.ipfs, + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider, + keyProvider: TestUtils.getKeyProvider(), + defaultCryptoAlgo: 'aes', + }); + const keyExchangeOptions = { + mailbox: mailbox, + cryptoProvider, + defaultCryptoAlgo: 'aes', + account: accounts[0], + keyProvider: TestUtils.getKeyProvider(), + }; + keyExchange = new KeyExchange(keyExchangeOptions); + const eventHub = await TestUtils.getEventHub(web3); + executor = await TestUtils.getExecutor(web3); + executor.eventHub = eventHub; + rightsAndRoles = await TestUtils.getRightsAndRoles(web3); + }); + + after(async () => { + web3.currentProvider.connection.close(); + await ipld.ipfs.stop(); + }); + + it('should be able to be add contact keys', async () => { + let profile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + await profile.addContactKey(accounts[0], 'context a', 'key 0x01_a'); + await profile.addContactKey(accounts[1], 'context a', 'key 0x02_a'); + await profile.addContactKey(accounts[1], 'context b', 'key 0x02_b'); + + expect(await profile.getContactKey(accounts[0], 'context a')).to.eq('key 0x01_a'); + expect(await profile.getContactKey(accounts[1], 'context a')).to.eq('key 0x02_a'); + expect(await profile.getContactKey(accounts[1], 'context b')).to.eq('key 0x02_b'); + expect(await profile.getContactKey(accounts[2], 'context a')).to.be.undefined; + }); + + it('should be able to be add dapp bookmarks', async () => { + let profile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + await profile.addDappBookmark('sample1.test', sampleDesc); + + expect(await profile.getDappBookmark('sample1.test')).to.be.ok; + expect((await profile.getDappBookmark('sample1.test')).title) + .to.eq('sampleTest'); + + // adding on existing + await profile.addDappBookmark('sample1.test', sampleUpdateDesc); + expect((await profile.getDappBookmark('sample1.test')).title) + .to.eq('sampleUpdateTest'); + }); + + it('should be able to save an encrypted profile to IPLD', async () => { + let profile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + await profile.addContactKey(accounts[0], 'context a', 'key 0x01_a'); + await profile.addContactKey(accounts[1], 'context a', 'key 0x02_a'); + await profile.addContactKey(accounts[1], 'context b', 'key 0x02_b'); + await profile.addDappBookmark('sample1.test', sampleDesc); + + // store as ipldIpfsHash + const ipldIpfsHash = await profile.storeToIpld(profile.treeLabels.addressBook); + expect(ipldIpfsHash).not.to.be.undefined; + + // load it to new profile instance + const loadedProfile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + await loadedProfile.loadFromIpld(profile.treeLabels.addressBook, ipldIpfsHash); + + // test contacts + expect(await loadedProfile.getContactKey(accounts[0], 'context a')).to.eq('key 0x01_a'); + expect(await loadedProfile.getContactKey(accounts[1], 'context a')).to.eq('key 0x02_a'); + expect(await loadedProfile.getContactKey(accounts[1], 'context b')).to.eq('key 0x02_b'); + expect(await loadedProfile.getContactKey(accounts[2], 'context a')).to.be.undefined; + + // test bookmarks + expect(await profile.getDappBookmark('sample1.test')).to.be.ok; + expect((await profile.getDappBookmark('sample1.test')).title).to.eq('sampleTest'); + + // adding on existing + await profile.addDappBookmark('sample1.test', sampleUpdateDesc); + expect((await profile.getDappBookmark('sample1.test')).title).to.eq('sampleUpdateTest'); + }); + + it('should be able to set and load a value for a given users profile contract from the blockchain', async () => { + const nameResolver = await TestUtils.getNameResolver(web3); + const address = await nameResolver.getAddress(ensName); + const contract = nameResolver.contractLoader.loadContract('ProfileIndex', address); + const label = await nameResolver.sha3('profiles'); + const valueToSet = '0x0000000000000000000000000000000000000004'; + let hash; + const from = Object.keys(accountMap)[0]; + hash = await nameResolver.executor.executeContractCall(contract, 'getProfile', from, { from, }); + const internalSigner = nameResolver.executor.signer as SignerInternal; + await nameResolver.executor.executeContractTransaction( + contract, + 'setMyProfile', + { from, autoGas: 1.1, }, + valueToSet, + ); + hash = await nameResolver.executor.executeContractCall(contract, 'getProfile', from, { from, }); + expect(hash).to.eq(valueToSet); + }); + + it('should be able to set and load a profile for a given user from the blockchain shorthand', async () => { + // create profile + const profile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + await profile.createProfile(keyExchange.getDiffieHellmanKeys()); + await profile.addContactKey(accounts[0], 'context a', 'key 0x01_a'); + await profile.addContactKey(accounts[1], 'context a', 'key 0x02_a'); + await profile.addContactKey(accounts[1], 'context b', 'key 0x02_b'); + await profile.addDappBookmark('sample1.test', sampleDesc); + + // store + const from = Object.keys(accountMap)[0]; + await profile.storeForAccount(profile.treeLabels.addressBook); + + // load + const newProfile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + + // test contacts + expect(await newProfile.getContactKey(accounts[0], 'context a')).to.eq('key 0x01_a'); + expect(await newProfile.getContactKey(accounts[1], 'context a')).to.eq('key 0x02_a'); + expect(await newProfile.getContactKey(accounts[1], 'context b')).to.eq('key 0x02_b'); + expect(await newProfile.getContactKey(accounts[2], 'context a')).to.be.undefined; + // test bookmarks + expect(await profile.getDappBookmark('sample1.test')).to.be.ok; + expect((await profile.getDappBookmark('sample1.test')).title).to.eq('sampleTest'); + }); + + it('allow to check if a profile exists', async () => { + let profile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: '0xbbF5029Fd710d227630c8b7d338051B8E76d50B3', + }); + expect(await profile.exists()).to.be.false; + + profile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + expect(await profile.exists()).to.be.true; + }); + + it('should remove a bookmark from a given profile', async () => { + let profile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + await profile.addDappBookmark('sample1.test', sampleDesc); + await profile.addDappBookmark('sample2.test', sampleDesc); + + expect(await profile.getDappBookmark('sample1.test')).to.be.ok; + expect(await profile.getDappBookmark('sample2.test')).to.be.ok; + + await profile.removeDappBookmark('sample1.test'); + expect(await profile.getDappBookmark('sample1.test')).not.to.be.ok; + expect(await profile.getDappBookmark('sample2.test')).to.be.ok; + }); + + it('should create a new Profile', async () => { + // create new profile helper instance + const from = Object.keys(accountMap)[0]; + ipld.originator = nameResolver.soliditySha3(from); + let profile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + + // create new profile, set private key and keyexchange partial key + await profile.createProfile(keyExchange.getDiffieHellmanKeys()); + + // add a bookmark + await profile.addDappBookmark('sample1.test', sampleDesc); + + // store tree to contract + await profile.storeForAccount(profile.treeLabels.bookmarkedDapps); + + // load + const newProfile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + // test contacts + expect(await profile.getDappBookmark('sample1.test')).to.deep.eq(sampleDesc); + }); + + it('should read a public part of a profile (e.g. public key)', async () => { + let profile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + + const mailbox = new Mailbox({ + mailboxOwner: accounts[0], + nameResolver: await TestUtils.getNameResolver(web3), + ipfs: ipld.ipfs, + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider: TestUtils.getCryptoProvider(), + keyProvider: TestUtils.getKeyProvider(), + defaultCryptoAlgo: 'aes', + }); + const keyExchangeOptions = { + mailbox: mailbox, + cryptoProvider: TestUtils.getCryptoProvider(), + defaultCryptoAlgo: 'aes', + account: accounts[0], + keyProvider: TestUtils.getKeyProvider(), + }; + const keyExchange = new KeyExchange(keyExchangeOptions); + + // store + const from = Object.keys(accountMap)[0]; + + await profile.createProfile(keyExchange.getDiffieHellmanKeys()); + + // simulate a different account with a different keyStore + const originalKeyStore = ipld.keyProvider; + const modifiedKeyStore = TestUtils.getKeyProvider(['mailboxKeyExchange']); + ipld.keyProvider = modifiedKeyStore; + // load + const newProfile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + + const pubKey = await newProfile.getPublicKey(); + expect(pubKey).to.be.ok; + + const bookmarks = await newProfile.getBookmarkDefinitions(); + expect(bookmarks).to.deep.eq({}); + + // set original key provider back + ipld.keyProvider = originalKeyStore; + }); + + it('should be able to set a contact as known', async () => { + let profile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + + await profile.createProfile(keyExchange.getDiffieHellmanKeys()); + + await profile.loadForAccount(); + expect(await profile.getContactKnownState(accounts[1])).to.be.false; + await profile.setContactKnownState(accounts[1], true); + expect(await profile.getContactKnownState(accounts[1])).to.be.true; + }); + + it('should be able to set a contact as unknown', async () => { + let profile = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld, + executor, + accountId: accounts[0], + }); + + await profile.createProfile(keyExchange.getDiffieHellmanKeys()); + + await profile.loadForAccount(); + expect(await profile.getContactKnownState(accounts[1])).to.be.false; + await profile.setContactKnownState(accounts[1], true); + expect(await profile.getContactKnownState(accounts[1])).to.be.true; + await profile.setContactKnownState(accounts[1], false); + expect(await profile.getContactKnownState(accounts[1])).to.be.false; + }); + + it('should allow to create profile profile and propfile data with separate accounts', async () => { + const [ profileReceiver, profileCreator, profileTestUser, ] = accounts; + const getKeyIpld = async (accountId) => { + const defaultKeys = TestUtils.getKeys(); + const keys = {}; + const dataKeyKeys = [ + nameResolver.soliditySha3('mailboxKeyExchange'), + nameResolver.soliditySha3(accountId), + nameResolver.soliditySha3( + nameResolver.soliditySha3(accountId), nameResolver.soliditySha3(accountId)), + ]; + dataKeyKeys.forEach((key) => { keys[key] = defaultKeys[key]; }); + const keyProvider = new KeyProvider({ keys, }); + const accountIpld = await TestUtils.getIpld(ipfs, keyProvider); + accountIpld.originator = nameResolver.soliditySha3(accountId); + return accountIpld; + }; + // separate profiles (with separate key providers for ipld) + const profile1 = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld: await getKeyIpld(profileReceiver), + executor, + accountId: profileReceiver, + }); + + // create profile data with profileReceiver (profileReceiver is the account, that will own the profile) + const dhKeys = keyExchange.getDiffieHellmanKeys(); + await profile1.addContactKey(profileReceiver, 'dataKey', dhKeys.privateKey.toString('hex')); + await profile1.addProfileKey(profileReceiver, 'alias', 'sample user 1'); + await profile1.addPublicKey(dhKeys.publicKey.toString('hex')); + const sharing = await dataContract.createSharing(profileReceiver); + const fileHashes = {}; + fileHashes[profile1.treeLabels.addressBook] = await profile1.storeToIpld(profile1.treeLabels.addressBook); + fileHashes[profile1.treeLabels.publicKey] = await profile1.storeToIpld(profile1.treeLabels.publicKey); + fileHashes.sharingsHash = sharing.sharingsHash; + const cryptor = cryptoProvider.getCryptorByCryptoAlgo('aesEcb'); + fileHashes[profile1.treeLabels.addressBook] = await cryptor.encrypt( + Buffer.from(fileHashes[profile1.treeLabels.addressBook].substr(2), 'hex'), + { key: sharing.hashKey, } + ); + fileHashes[profile1.treeLabels.addressBook] = `0x${fileHashes[profile1.treeLabels.addressBook].toString('hex')}`; + + // store it with profileCreator + const factoryDomain = nameResolver.getDomainName(nameResolver.config.domains.profileFactory); + const profileContract = await dataContract.create( + factoryDomain, + profileCreator, + null, + '0x0000000000000000000000000000000000000000000000000000000000000000', + true, + '0x0000000000000000000000000000000000000000000000000000000000000000', + ); + await dataContract.setEntry( + profileContract, + profile1.treeLabels.publicKey, + fileHashes[profile1.treeLabels.publicKey], + profileCreator, + false, + false, + ); + await dataContract.setEntry( + profileContract, + profile1.treeLabels.addressBook, + fileHashes[profile1.treeLabels.addressBook], + profileCreator, + false, + false, + ); + await executor.executeContractTransaction( + profileContract, + 'setSharing', + { from: profileCreator, autoGas: 1.1, }, + fileHashes.sharingsHash, + ); + + // profileCreator hands contract over to profileReceiver + await dataContract.inviteToContract(null, profileContract.options.address, profileCreator, profileReceiver); + // grand admin role + await rightsAndRoles.addAccountToRole(profileContract, profileCreator, profileReceiver, 0); + // cancel own member role + await rightsAndRoles.removeAccountFromRole(profileContract, profileCreator, profileCreator, 1); + // cancel own admin role + await rightsAndRoles.removeAccountFromRole(profileContract, profileCreator, profileCreator, 0); + // transfer ownership + await rightsAndRoles.transferOwnership(profileContract, profileCreator, profileReceiver); + + + // user profile with account 1 + // stores profile (atm still done as receiver, as this is msg.sender relative) + const profileIndexDomain = nameResolver.getDomainName(nameResolver.config.domains.profile); + const address = await nameResolver.getAddress(profileIndexDomain); + const contract = nameResolver.contractLoader.loadContract('ProfileIndex', address); + await executor.executeContractTransaction( + contract, 'setMyProfile', { from: profileReceiver, autoGas: 1.1, }, profileContract.options.address); + // can read own keys + const profile2 = new Profile({ + nameResolver, + defaultCryptoAlgo: 'aes', + dataContract, + contractLoader, + ipld: await getKeyIpld(profileReceiver), + executor, + accountId: profileReceiver, + }); + + // can read public key + await profile2.loadForAccount(profile2.treeLabels.publicKey); + expect(await profile2.getPublicKey()).to.eq(dhKeys.publicKey.toString('hex')); + + // can read private key and alias (==> has access to data) + await profile2.loadForAccount(profile2.treeLabels.addressBook); + expect(await profile2.getContactKey(profileReceiver, 'dataKey')).to.eq(dhKeys.privateKey.toString('hex')); + expect(await profile2.getProfileKey(profileReceiver, 'alias')).to.eq('sample user 1'); + + // could transfer it to another user (==> has smart contract permissions) + // profileCreator hands contract over to profileReceiver + await dataContract.inviteToContract(null, profileContract.options.address, profileReceiver, profileTestUser); + // grand admin role + await rightsAndRoles.addAccountToRole(profileContract, profileReceiver, profileTestUser, 0); + // // cancel own member role + await rightsAndRoles.removeAccountFromRole(profileContract, profileReceiver, profileReceiver, 1); + // cancel own admin role + await rightsAndRoles.removeAccountFromRole(profileContract, profileReceiver, profileReceiver, 0); + // transfer ownership + await rightsAndRoles.transferOwnership(profileContract, profileReceiver, profileTestUser); + }); +}); diff --git a/src/profile/profile.ts b/src/profile/profile.ts new file mode 100644 index 00000000..4d1b3aa8 --- /dev/null +++ b/src/profile/profile.ts @@ -0,0 +1,618 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import { + ContractLoader, + Executor, + Logger, + LoggerOptions, + NameResolver, + obfuscate, +} from '@evan.network/dbcp'; + +import { CryptoProvider } from '../encryption/crypto-provider'; +import { DataContract } from '../contracts/data-contract/data-contract'; +import { Ipld } from '../dfs/ipld'; +import { KeyExchange } from '../keyExchange'; + + +/** + * parameters for Profile constructor + */ +export interface ProfileOptions extends LoggerOptions { + accountId: string, + contractLoader: ContractLoader, + dataContract: DataContract, + defaultCryptoAlgo: string, + executor: Executor, + ipld: Ipld, + nameResolver: NameResolver, +} + + +/** + * bookmark to a dapp + */ +export interface DappBookmark { + description: string, + img: string, + primaryColor: string, + secondaryColor?: string, + title: string, +} + + +/** + * profile helper class, builds profile graphs + * + * @class Profile (name) + */ +export class Profile extends Logger { + activeAccount: string; + contractLoader: ContractLoader; + dataContract: DataContract; + defaultCryptoAlgo: string; + executor: Executor; + ipld: Ipld; + nameResolver: NameResolver; + profileContract: any; + trees: any; + treeLabels = { + addressBook: 'addressBook', + bookmarkedDapps: 'bookmarkedDapps', + contracts: 'contracts', + publicKey: 'publicKey', + }; + + constructor(options: ProfileOptions) { + super(options); + this.activeAccount = options.accountId; + this.contractLoader = options.contractLoader; + this.dataContract = options.dataContract; + this.defaultCryptoAlgo = options.defaultCryptoAlgo; + this.executor = options.executor; + this.ipld = options.ipld; + this.nameResolver = options.nameResolver; + this.trees = {}; + } + + /** + * add a contract to a business center scope the current profile + * + * @param {string} bc business center ens address or contract address + * @param {string} address contact address + * @param {any} data bookmark metadata + * @return {Promise} resolved when done + */ + async addBcContract(bc: string, address: string, data: any): Promise { + this.ensureTree('contracts'); + const bcSet = await this.ipld.getLinkedGraph(this.trees['contracts'], bc); + if (!bcSet) { + await this.ipld.set(this.trees['contracts'], bc, {}, false); + } + await this.ipld.set(this.trees['contracts'], `${bc}/${address}`, data, false); + } + + /** + * add a key for a contact to bookmarks + * + * @param {string} address account key of the contact + * @param {string} context store key for this context, can be a contract, bc, etc. + * @param {string} key communication key to store + * @return {Promise} resolved when done + */ + async addContactKey(address: string, context: string, key: string): Promise { + this.log(`add contact key: account "${address}", context "${context}", key "${obfuscate(key)}"`, 'debug'); + this.ensureTree('addressBook'); + + let addressHash; + // check if address is already hashed + if (address.length === 42) { + addressHash = this.nameResolver.soliditySha3.apply(this.nameResolver, [ + this.nameResolver.soliditySha3(address), + this.nameResolver.soliditySha3(this.activeAccount), + ].sort()); + } else { + addressHash = address; + } + const keysSet = await this.ipld.getLinkedGraph(this.trees['addressBook'], `keys`); + if (!keysSet) { + await this.ipld.set(this.trees['addressBook'], 'keys', {}, true); + } + const contactSet = await this.ipld.getLinkedGraph(this.trees['addressBook'], `keys/${addressHash}`); + if (!contactSet) { + await this.ipld.set(this.trees['addressBook'], `keys/${addressHash}`, {}, true); + } + await this.ipld.set(this.trees['addressBook'], `keys/${addressHash}/${context}`, key, true); + } + + /** + * add a contract to the current profile + * + * @param {string} address contact address + * @return {any} bookmark info + */ + async addContract(address: string, data: any): Promise { + this.ensureTree('contracts'); + await this.ipld.set(this.trees['contracts'], address, data, false); + } + + /** + * add a bookmark for a dapp + * + * @param {string} address ENS name or contract address (if no ENS name is set) + * @param {DappBookmark} description description for bookmark + * @return {Promise} resolved when done + */ + async addDappBookmark(address: string, description: DappBookmark): Promise { + this.ensureTree('bookmarkedDapps'); + if (!address || !description) { + throw new Error('no valid description or address given!'); + } + await this.ipld.set(this.trees['bookmarkedDapps'], address, {}, true); + const descriptionKeys = Object.keys(description); + for (let key of descriptionKeys) { + await this.ipld.set(this.trees['bookmarkedDapps'], `${address}/${key}`, description[key], true); + } + } + + /** + * add a profile value to an account + * + * @param {string} address account key of the contact + * @param {string} key store key for the account like alias, etc. + * @param {string} value value of the profile key + * @return {Promise} resolved when done + */ + async addProfileKey(address: string, key: string, value: string): Promise { + this.ensureTree('addressBook'); + const profileSet = await this.ipld.getLinkedGraph(this.trees['addressBook'], `profile`); + if (!profileSet) { + await this.ipld.set(this.trees['addressBook'], `profile`, {}, true); + } + const addressSet = await this.ipld.getLinkedGraph(this.trees['addressBook'], `profile/${address}`); + if (!addressSet) { + await this.ipld.set(this.trees['addressBook'], `profile/${address}`, {}, true); + } + await this.ipld.set(this.trees['addressBook'], `profile/${address}/${key}`, value, true); + } + + /** + * add a key for a contact to bookmarks + * + * @param {string} key public Diffie Hellman key part to store + * @return {Promise} resolved when done + */ + async addPublicKey(key: string): Promise { + this.ensureTree('publicKey'); + await this.ipld.set(this.trees['publicKey'], 'publicKey', key, true); + } + + /** + * create new profile, store it to profile index initialize addressBook and publicKey + * + * @param {string} keys communication key to store + * @return {Promise} resolved when done + */ + async createProfile(keys: any): Promise { + // create new profile contract and store in profile index + const factoryDomain = this.nameResolver.getDomainName(this.nameResolver.config.domains.profileFactory); + this.profileContract = await this.dataContract.create(factoryDomain, this.activeAccount); + await Promise.all([ + (async () => { + const ensName = this.nameResolver.getDomainName(this.nameResolver.config.domains.profile); + const address = await this.nameResolver.getAddress(ensName); + const contract = this.nameResolver.contractLoader.loadContract('ProfileIndex', address); + await this.executor.executeContractTransaction( + contract, 'setMyProfile', { from: this.activeAccount, autoGas: 1.1, }, this.profileContract.options.address); + })(), + (async () => { + await this.addContactKey(this.activeAccount, 'dataKey', keys.privateKey.toString('hex')); + await this.addPublicKey(keys.publicKey.toString('hex')); + await Promise.all([ + this.storeForAccount('addressBook'), + this.storeForAccount('publicKey') + ]); + })(), + ]); + } + + /** + * check if a profile has been stored for current account + * + * @return {Promise} true if a contract was registered, false if not + */ + async exists(): Promise { + const ensName = this.nameResolver.getDomainName(this.nameResolver.config.domains.profile); + const address = await this.nameResolver.getAddress(ensName); + const indexContract = this.nameResolver.contractLoader.loadContract('ProfileIndex', address); + const profileContractAddress = await this.executor.executeContractCall( + indexContract, 'getProfile', this.activeAccount, { from: this.activeAccount, }); + return profileContractAddress !== '0x0000000000000000000000000000000000000000'; + } + + /** + * get the whole addressBook + * + * @return {any} bookmark info + */ + async getAddressBook(): Promise { + if (!this.trees[this.treeLabels.addressBook]) { + await this.loadForAccount(this.treeLabels.addressBook); + } + return this.trees[this.treeLabels.addressBook]; + } + + /** + * get a specific addressBook entry for a given address + * + * @param {string} address contact address + * @return {Promise} bookmark info + */ + async getAddressBookAddress(address: string): Promise { + if (!this.trees[this.treeLabels.addressBook]) { + await this.loadForAccount(this.treeLabels.addressBook); + } + return this.ipld.getLinkedGraph(this.trees[this.treeLabels.addressBook], `profile/${address}`); + } + + /** + * get a specific contract entry for a given address + * + * @param {string} bc business center ens address or contract address + * @param {string} address contact address + * @return {Promise} bookmark info + */ + async getBcContract(bc: string, address: string): Promise { + if (!this.trees[this.treeLabels.contracts]) { + await this.loadForAccount(this.treeLabels.contracts); + } + return this.ipld.getLinkedGraph(this.trees[this.treeLabels.contracts], `${bc}/${address}`); + } + + /** + * get all contracts grouped under a business center + * + * @param {string} bc business center + * @return {Promise} bc contracts. + */ + async getBcContracts(bc: string): Promise { + if (!this.trees[this.treeLabels.contracts]) { + await this.loadForAccount(this.treeLabels.contracts); + } + return this.ipld.getLinkedGraph(this.trees[this.treeLabels.contracts], bc); + } + + /** + * get all bookmarks for profile + * + * @return {any} all bookmarks for profile + */ + async getBookmarkDefinitions(): Promise { + if (!this.trees[this.treeLabels.bookmarkedDapps]) { + await this.loadForAccount(this.treeLabels.bookmarkedDapps); + } + return this.ipld.getLinkedGraph(this.trees[this.treeLabels.bookmarkedDapps], `bookmarkedDapps`); + } + + /** + * get a communication key for a contact from bookmarks + * + * @param {string} address account key of the contact + * @param {string} context store key for this context, can be a contract, bc, etc. + * @return {Promise} matching key + */ + async getContactKey(address: string, context: string): Promise { + let addressHash; + // check if address is already hashed + if (address.length === 42) { + addressHash = this.nameResolver.soliditySha3.apply(this.nameResolver, [ + this.nameResolver.soliditySha3(address), + this.nameResolver.soliditySha3(this.activeAccount), + ].sort()); + } else { + addressHash = address; + } + if (!this.trees[this.treeLabels.addressBook]) { + await this.loadForAccount(this.treeLabels.addressBook); + } + return this.ipld.getLinkedGraph(this.trees[this.treeLabels.addressBook], `keys/${addressHash}/${context}`); + } + + /** + * get a specific contract entry for a given address + * + * @param {string} address contact address + * @return {Promise} bookmark info + */ + async getContract(address: string): Promise { + if (!this.trees[this.treeLabels.contracts]) { + await this.loadForAccount(this.treeLabels.contracts); + } + return this.ipld.getLinkedGraph(this.trees[this.treeLabels.contracts], address); + } + + /** + * get all contracts for the current profile + * + * @return {Promise} contracts info + */ + async getContracts(): Promise { + if (!this.trees[this.treeLabels.contracts]) { + await this.loadForAccount(this.treeLabels.contracts); + } + return this.ipld.getLinkedGraph(this.trees[this.treeLabels.contracts], this.treeLabels.contracts); + } + + /** + * get a bookmark for a given address if any + * + * @param {string} address ENS name or contract address (if no ENS name is set) + * @return {Promise} bookmark info + */ + async getDappBookmark(address: string): Promise { + if (!this.trees[this.treeLabels.bookmarkedDapps]) { + await this.loadForAccount(this.treeLabels.bookmarkedDapps); + } + return this.ipld.getLinkedGraph(this.trees[this.treeLabels.bookmarkedDapps], address); + } + + /** + * check, known state for given account + * + * @param {string} accountId account id of a contact + * @return {Promise} true if known account + */ + async getContactKnownState(accountId: string): Promise { + const value = await this.dataContract.getMappingValue( + this.profileContract, + 'contacts', + accountId, + this.activeAccount, + false, + false, + ); + return value.substr(-1) === '0' ? false : true; + } + + /** + * get a key from an address in the address book + * + * @param {string} address address to look up + * @param {string} key type of key to get + * @return {Promise} key + */ + async getProfileKey(address: string, key: string): Promise { + if (!this.trees[this.treeLabels.addressBook]) { + await this.loadForAccount(this.treeLabels.addressBook); + } + return this.ipld.getLinkedGraph(this.trees[this.treeLabels.addressBook], `profile/${address}/${key}`); + } + + /** + * get public key of profiles + * + * @return {any} public key + */ + async getPublicKey(): Promise { + if (!this.trees[this.treeLabels.publicKey]) { + await this.loadForAccount(this.treeLabels.publicKey); + } + return this.ipld.getLinkedGraph(this.trees[this.treeLabels.publicKey], 'publicKey'); + } + + /** + * load profile for given account from global profile contract, if a tree is given, load that tree + * from ipld as well + * + * @param {string} tree tree to load ('bookmarkedDapps', 'contracts', ...) + * @return {Promise} resolved when done + */ + async loadForAccount(tree?: string): Promise { + // ensure profile contract + if (!this.profileContract) { + const ensName = this.nameResolver.getDomainName(this.nameResolver.config.domains.profile); + const address = await this.nameResolver.getAddress(ensName); + const indexContract = this.nameResolver.contractLoader.loadContract('ProfileIndex', address); + const profileContractAddress = await this.executor.executeContractCall( + indexContract, 'getProfile', this.activeAccount, { from: this.activeAccount, }); + if (profileContractAddress === '0x0000000000000000000000000000000000000000') { + throw new Error(`no profile found for account "${this.activeAccount}"`); + } else { + const contractAddress = profileContractAddress.length === 66 ? + this.executor.web3.utils.toChecksumAddress(profileContractAddress.substr(0, 42)) : profileContractAddress; + this.profileContract = this.contractLoader.loadContract('DataContractInterface', contractAddress); + } + } + if (tree) { + let hash; + if (tree === this.treeLabels.publicKey) { + hash = await this.dataContract.getEntry(this.profileContract, tree, this.activeAccount, false, false); + } else { + hash = await this.dataContract.getEntry(this.profileContract, tree, this.activeAccount, false, true); + } + if (hash === '0x0000000000000000000000000000000000000000000000000000000000000000') { + this.trees[tree] = { + bookmarkedDapps: {}, + addressBook: {}, + contracts: {}, + }; + return Promise.resolve(); + } else { + await this.loadFromIpld(tree, hash); + } + } + } + + /** + * load profile from ipfs via ipld dag via ipfs file hash + * + * @param {string} tree tree to load ('bookmarkedDapps', 'contracts', + * ...) + * @param {string} ipldIpfsHash ipfs file hash that points to a file with ipld a + * hash + * @return {Promise} this profile + */ + async loadFromIpld(tree: string, ipldIpfsHash: string): Promise { + let loaded; + try { + loaded = await this.ipld.getLinkedGraph(ipldIpfsHash); + } catch (e) { + this.log(`could not load profile from ipld ${ e.message || e }`, 'error'); + loaded = { + bookmarkedDapps: {}, + addressBook: {}, + contracts: {}, + }; + } + this.trees[tree] = loaded; + return this; + } + + /** + * remove a contact from bookmarkedDapps + * + * @param {string} address account key of the contact + * @return {Promise} resolved when done + */ + async removeContact(address: string): Promise { + const addressHash = this.nameResolver.soliditySha3.apply(this.nameResolver, [ + this.nameResolver.soliditySha3(address), + this.nameResolver.soliditySha3(this.activeAccount), + ].sort()); + const addressBook = await this.getAddressBook(); + delete addressBook.keys[addressHash]; + delete addressBook.profile[address]; + } + + /** + * remove a dapp bookmark from the bookmarkedDapps + * + * @param {string} address address of the bookmark to remove + * @return {Promise} resolved when done + */ + async removeDappBookmark(address: string): Promise { + if (!address ) { + throw new Error('no valid address given!'); + } + this.ensureTree('bookmarkedDapps'); + await this.ipld.remove(this.trees['bookmarkedDapps'], address); + } + + /** + * set bookmarks with given value + * + * @param {any} bookmarks bookmarks to set + * @return {Promise} resolved when done + */ + async setDappBookmarks(bookmarks: any): Promise { + if (!bookmarks) { + throw new Error('no valid bookmarks are given'); + } + this.ensureTree(this.treeLabels.bookmarkedDapps); + await this.ipld.set( + this.trees[this.treeLabels.bookmarkedDapps], + this.treeLabels.bookmarkedDapps, + bookmarks, + true + ); + } + + /** + * store given state for this account + * + * @param {string} accountId account id of a contact + * @param {boolean} contactKnown true if known, false if not + * @return {Promise} resolved when done + */ + async setContactKnownState(accountId: string, contactKnown: boolean): Promise { + await this.dataContract.setMappingValue( + this.profileContract, + 'contacts', + accountId, + `0x${(contactKnown ? '1' : '0').padStart(64, '0')}`, // cast bool to bytes32 + this.activeAccount, + false, + false, + ); + } + + /** + * stores profile tree or given hash to global profile contract + * + * @param {string} tree tree to store ('bookmarkedDapps', 'contracts', ...) + * @param {string} ipldHash store this hash instead of the current tree for account + * @return {Promise} resolved when done + */ + async storeForAccount(tree: string, ipldHash?: string): Promise { + if (ipldHash) { + this.log(`store tree "${tree}" with given hash to profile contract for account "${this.activeAccount}"`); + if (tree === this.treeLabels.publicKey) { + await this.dataContract.setEntry(this.profileContract, tree, ipldHash, this.activeAccount, false, false); + } else { + await this.dataContract.setEntry(this.profileContract, tree, ipldHash, this.activeAccount, false, false); + } + } else { + this.log(`store tree "${tree}" to ipld and then to profile contract for account "${this.activeAccount}"`); + const stored = await this.storeToIpld(tree); + let hash; + if (tree === this.treeLabels.publicKey) { + await this.dataContract.setEntry(this.profileContract, tree, stored, this.activeAccount, false, false); + } else { + await this.dataContract.setEntry(this.profileContract, tree, stored, this.activeAccount, false, true); + } + await this.loadForAccount(tree); + } + } + + /** + * store profile in ipfs as an ipfs file that points to a ipld dag + * + * @param {string} tree tree to store ('bookmarkedDapps', 'contracts', ...) + * @return {Promise} hash of the ipfs file + */ + async storeToIpld(tree: string): Promise { + return await this.ipld.store(this.trees[tree]); + } + + /** + * make sure that specified tree is available locally for inserting values + * + * @param {string} tree name of the tree + */ + private ensureTree(tree: string): void { + if (!this.trees[tree] && tree === 'publicKey') { + this.trees[tree] = { + cryptoInfo: { + algorithm: 'unencrypted' + }, + }; + } else if (!this.trees[tree]) { + this.trees[tree] = {}; + } + } +} diff --git a/src/runtime.ts b/src/runtime.ts new file mode 100644 index 00000000..580f5020 --- /dev/null +++ b/src/runtime.ts @@ -0,0 +1,310 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import smartContract = require('@evan.network/smart-contracts-core'); +import smartContractsAdmin = require('@evan.network/smart-contracts-admin'); + +import { + AccountStore, + ContractLoader, + DfsInterface, + EventHub, + Executor, + Ipfs, + KeyProvider, + Logger, + NameResolver, + SignerInterface, + SignerInternal, + Unencrypted, +} from '@evan.network/dbcp'; + +import { Aes } from './encryption/aes'; +import { AesEcb } from './encryption/aes-ecb'; +import { BaseContract } from './contracts/base-contract/base-contract'; +import { config } from './config'; +import { CryptoProvider } from './encryption/crypto-provider'; +import { DataContract } from './contracts/data-contract/data-contract'; +import { Description } from './shared-description'; +import { Ipld } from './dfs/ipld'; +import { KeyExchange } from './keyExchange'; +import { Mailbox } from './mailbox'; +import { Onboarding } from './onboarding'; +import { Profile } from './profile/profile'; +import { RightsAndRoles } from './contracts/rights-and-roles'; +import { ServiceContract } from './contracts/service-contract/service-contract'; +import { Sharing } from './contracts/sharing'; + + +/** + * runtime for interacting with dbcp, including helpers for transactions & co + */ +export interface Runtime { + accountStore: AccountStore, + activeAccount: string, + baseContract: BaseContract, + contractLoader: ContractLoader, + cryptoProvider: CryptoProvider, + description: Description, + dataContract: DataContract, + dfs: DfsInterface, + executor: Executor, + ipld: Ipld, + keyExchange: KeyExchange, + keyProvider: KeyProvider, + mailbox: Mailbox, + nameResolver: NameResolver, + onboarding: Onboarding, + profile: Profile, + rightsAndRoles: RightsAndRoles, + serviceContract: ServiceContract, + sharing: Sharing, + signer: SignerInterface, + web3: any, +}; + +/** + * create new runtime instance + * + * @param {any} web3 connected web3 instance + * @param {DfsInterface} dfs interface for retrieving file from dfs + * @param {any} runtimeConfig configuration values + * @return {Promise} runtime instance + */ +export async function createDefaultRuntime(web3: any, dfs: DfsInterface, runtimeConfig: any): Promise { + // get default logger + const log = (new Logger()).logFunction; + // get/compile smart contracts + const solc = new smartContract.Solc({ config: { compileContracts: false, }, log, }); + await solc.ensureCompiled([smartContractsAdmin.getContractsPath()]); + const contracts = solc.getContracts(); + + // web3 contract interfaces + const contractLoader = new ContractLoader({ contracts, web3, }); + + // executor + const accountStore = new AccountStore({ accounts: runtimeConfig.accountMap, }); + const signer = new SignerInternal({ accountStore, contractLoader, config: {}, web3, }); + const executor = new Executor({ config, signer, web3, }); + await executor.init({}); + const nameResolver = new NameResolver({ + config: runtimeConfig.nameResolver || config.nameResolver, + executor, + contractLoader, + web3, + }); + const eventHub = new EventHub({ + config: runtimeConfig.nameResolver || config.nameResolver, + contractLoader, + nameResolver, + }); + executor.eventHub = eventHub; + + // encryption + const cryptoConfig = {}; + cryptoConfig['aes'] = new Aes(); + cryptoConfig['unencrypted'] = new Unencrypted(); + cryptoConfig['aesEcb'] = new AesEcb(); + const cryptoProvider = new CryptoProvider(cryptoConfig); + const keyProvider = new KeyProvider({ keys: runtimeConfig.keyConfig, }); + + // description + const description = new Description({ + contractLoader, + cryptoProvider, + dfs, + executor, + keyProvider, + nameResolver, + sharing: null, + web3, + }); + const sharing = new Sharing({ + contractLoader, + cryptoProvider, + description, + executor, + dfs, + keyProvider, + nameResolver, + defaultCryptoAlgo: 'aes', + }); + description.sharing = sharing; + + const baseContract = new BaseContract({ + executor, + loader: contractLoader, + nameResolver, + }); + + const dataContract = new DataContract({ + cryptoProvider, + dfs, + executor, + loader: contractLoader, + nameResolver, + sharing, + web3, + description, + }); + + const activeAccount = Object.keys(runtimeConfig.accountMap)[0]; + const ipld = new Ipld({ + ipfs: dfs as Ipfs, + keyProvider, + cryptoProvider, + defaultCryptoAlgo: 'aes', + originator: nameResolver.soliditySha3(activeAccount), + nameResolver, + }); + + // 'own' key provider, that won't be linked to profile and used in 'own' ipld + // this prevents key lookup infinite loops + const keyProviderOwn = new KeyProvider({ keys: runtimeConfig.keyConfig, }); + const ipldOwn = new Ipld({ + ipfs: dfs as Ipfs, + keyProvider: keyProviderOwn, + cryptoProvider, + defaultCryptoAlgo: 'aes', + originator: nameResolver.soliditySha3(activeAccount), + nameResolver, + }); + const sharingOwn = new Sharing({ + contractLoader, + cryptoProvider, + description, + executor, + dfs, + keyProvider: keyProviderOwn, + nameResolver, + defaultCryptoAlgo: 'aes', + }); + const dataContractOwn = new DataContract({ + cryptoProvider, + dfs, + executor, + loader: contractLoader, + nameResolver, + sharing: sharingOwn, + web3, + description, + }); + let profile = new Profile({ + accountId: activeAccount, + contractLoader, + dataContract: dataContractOwn, + defaultCryptoAlgo: 'aes', + executor, + ipld: ipldOwn, + nameResolver, + }); + // this key provider is linked to profile for key retrieval + // keyProviderOwn is not liked to profile to prevent profile key lookups + keyProvider.init(profile); + + const rightsAndRoles = new RightsAndRoles({ + contractLoader, + executor, + nameResolver, + web3, + }); + + const serviceContract = new ServiceContract({ + cryptoProvider, + dfs, + executor, + keyProvider, + loader: contractLoader, + nameResolver, + sharing, + web3, + }); + + const mailbox = new Mailbox({ + mailboxOwner: activeAccount, + nameResolver, + ipfs: dfs as Ipfs, + contractLoader, + cryptoProvider, + keyProvider, + defaultCryptoAlgo: 'aes', + }); + + const keyExchange = new KeyExchange({ + mailbox, + cryptoProvider, + defaultCryptoAlgo: 'aes', + account: activeAccount, + keyProvider, + }); + if (await profile.exists()) { + log(`profile for ${activeAccount} exists, fetching keys`, 'debug'); + try { + keyExchange.setPublicKey( + await profile.getPublicKey(), + await profile.getContactKey(activeAccount, 'dataKey'), + ); + } catch (ex) { + log(`fetching keys for ${activeAccount} failed with "${ex.msg || ex}", removing profile from runtime`, 'warning'); + profile = null; + keyProvider.profile = null; + } + } else { + log(`profile for ${activeAccount} doesn't exist`) + } + + const onboarding = new Onboarding({ + mailbox, + smartAgentId: '0x063fB42cCe4CA5448D69b4418cb89E663E71A139', + executor, + }); + + // return runtime object + return { + accountStore, + activeAccount, + baseContract, + contractLoader, + cryptoProvider, + dataContract, + description, + dfs, + executor, + ipld, + keyExchange, + keyProvider, + mailbox, + nameResolver, + onboarding, + profile, + rightsAndRoles, + serviceContract, + sharing, + signer, + web3, + }; +}; diff --git a/src/shared-description.spec.ts b/src/shared-description.spec.ts new file mode 100644 index 00000000..7450f4de --- /dev/null +++ b/src/shared-description.spec.ts @@ -0,0 +1,268 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import 'mocha'; +import { expect, use, } from 'chai'; +import chaiAsPromised = require('chai-as-promised'); + +import { + ContractLoader, + Envelope, + Executor, + Ipfs, + KeyProvider, + NameResolver, + Unencrypted, +} from '@evan.network/dbcp'; + +import { accounts } from './test/accounts'; +import { Aes } from './encryption/aes'; +import { BaseContract } from './contracts/base-contract/base-contract'; +import { config } from './config'; +import { CryptoProvider } from './encryption/crypto-provider'; +import { DataContract } from './contracts/data-contract/data-contract'; +import { Description } from './shared-description'; +import { Sharing } from './contracts/sharing'; +import { TestUtils } from './test/test-utils'; + + +use(chaiAsPromised); + +const testAddressPrefix = 'testDapp'; +/* tslint:disable:quotemark */ +const sampleDescription = { + "name": "test description", + "description": "description used in tests.", + "author": "description test user", + "version": "0.0.1", + "dbcpVersion": 1, + "dapp": { + "dependencies": { + "angular-bc": "^1.0.0", + "angular-core": "^1.0.0", + "angular-libs": "^1.0.0" + }, + "entrypoint": "task.js", + "files": [ + "task.js", + "task.css" + ], + "origin": "Qm...", + "primaryColor": "#e87e23", + "secondaryColor": "#fffaf5", + "standalone": true, + "type": "dapp" + }, +}; +/* tslint:enable:quotemark */ +const sampleKey = '346c22768f84f3050f5c94cec98349b3c5cbfa0b7315304e13647a49181fd1ef'; +let description: Description; +let testAddressFoo; +let executor: Executor; +let loader: ContractLoader; +let businessCenterDomain; +let web3; +let cryptoProvider: CryptoProvider; +let sharing: Sharing; +let dfs; +let dc: DataContract; +let nameResolver: NameResolver; + +describe('Description handler', function() { + this.timeout(300000); + + before(async () => { + web3 = TestUtils.getWeb3(); + description = await TestUtils.getDescription(web3); + nameResolver = await TestUtils.getNameResolver(web3); + executor = await TestUtils.getExecutor(web3); + executor.eventHub = await TestUtils.getEventHub(web3); + loader = await TestUtils.getContractLoader(web3); + cryptoProvider = await TestUtils.getCryptoProvider(); + sharing = new Sharing({ + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider, + description: description, + executor: await TestUtils.getExecutor(web3), + dfs: description.dfs, + keyProvider: TestUtils.getKeyProvider(), + nameResolver: await TestUtils.getNameResolver(web3), + defaultCryptoAlgo: 'aes', + }); + description.sharing = sharing; + dc = new DataContract({ + cryptoProvider, + dfs, + executor, + loader, + log: TestUtils.getLogger(), + nameResolver, + sharing, + web3: TestUtils.getWeb3(), + description, + }); + businessCenterDomain = nameResolver.getDomainName(config.nameResolver.domains.businessCenter); + const businessCenterAddress = await nameResolver.getAddress(businessCenterDomain); + const businessCenter = await loader.loadContract('BusinessCenter', businessCenterAddress); + if (!await executor.executeContractCall(businessCenter, 'isMember', accounts[0], { from: accounts[0], })) { + await executor.executeContractTransaction(businessCenter, 'join', { from: accounts[0], autoGas: 1.1, }); + } + + testAddressFoo = `${testAddressPrefix}.${nameResolver.getDomainName(config.nameResolver.domains.root)}`; + }); + + after(async () => { + await description.dfs.stop(); + web3.currentProvider.connection.close(); + }); + + describe('when validing used description', () => { + it('should allow valid description', async () => { + const contract = await executor.createContract('Described', [], {from: accounts[0], gas: 1000000, }); + const descriptionEnvelope = { public: Object.assign({}, sampleDescription), }; + await description.setDescriptionToContract(contract.options.address, descriptionEnvelope, accounts[0]); + }); + + it('should reject invalid description', async () => { + const contract = await executor.createContract('Described', [], {from: accounts[0], gas: 1000000, }); + let descriptionEnvelope; + let promise; + + // missing property + descriptionEnvelope = { public: Object.assign({}, sampleDescription), }; + delete descriptionEnvelope.public.version; + promise = description.setDescriptionToContract(contract.options.address, descriptionEnvelope, accounts[0]); + await expect(promise).to.be.rejected; + + // additional property + descriptionEnvelope = { public: Object.assign({}, sampleDescription), }; + descriptionEnvelope.public.newPropery = 123; + promise = description.setDescriptionToContract(contract.options.address, descriptionEnvelope, accounts[0]); + await expect(promise).to.be.rejected; + + // wrong type + descriptionEnvelope = { public: Object.assign({}, sampleDescription), }; + descriptionEnvelope.public.version = 123; + promise = description.setDescriptionToContract(contract.options.address, descriptionEnvelope, accounts[0]); + await expect(promise).to.be.rejected; + + // additional sub property + descriptionEnvelope = { public: Object.assign({}, sampleDescription), }; + descriptionEnvelope.public.dapp = Object.assign({}, descriptionEnvelope.public.dapp, { newProperty: 123, }); + promise = description.setDescriptionToContract(contract.options.address, descriptionEnvelope, accounts[0]); + await expect(promise).to.be.rejected; + }); + }); + + describe('when working with ENS descriptions', () => { + it('should be able to set and get unencrypted content for ENS addresses', async () => { + await description.setDescriptionToEns(testAddressFoo, { public: sampleDescription, }, accounts[1]); + const content = await description.getDescriptionFromEns(testAddressFoo); + expect(content).to.deep.eq({ public: sampleDescription, }); + }); + + it('should be able to set and get unencrypted content for ENS addresses including special characters', async () => { + const sampleDescriptionSpecialCharacters = { + public: Object.assign({}, sampleDescription, { name: 'Special Characters !"§$%&/()=?ÜÄÖ', }), + }; + await description.setDescriptionToEns(testAddressFoo, sampleDescriptionSpecialCharacters, accounts[1]); + const content = await description.getDescriptionFromEns(testAddressFoo); + expect(content).to.deep.eq(sampleDescriptionSpecialCharacters); + }); + + it('should be able to set and get encrypted content for ENS addresses', async () => { + const keyConfig = {}; + keyConfig[nameResolver.soliditySha3(accounts[1])] = sampleKey; + const keyProvider = new KeyProvider(keyConfig); + description.keyProvider = keyProvider; + const cryptor = new Aes(); + const cryptoConfig = {}; + const cryptoInfo = cryptor.getCryptoInfo(nameResolver.soliditySha3(accounts[1])); + cryptoConfig['aes'] = cryptor; + description.cryptoProvider = new CryptoProvider(cryptoConfig); + const secureDescription = { + public: sampleDescription, + private: { + name: 'real name', + }, + cryptoInfo, + }; + await description.setDescriptionToEns(testAddressFoo, secureDescription, accounts[1]); + const content = await description.getDescriptionFromEns(testAddressFoo); + expect(content).to.deep.eq(secureDescription); + }); + }); + + describe('when working with ENS descriptions', () => { + it('should be able to set a description on a created contract', async () => { + const contract = await dc.create('testdatacontract', accounts[0], businessCenterDomain); + const keyConfig = {}; + keyConfig[nameResolver.soliditySha3(contract.options.address)] = sampleKey; + const keyProvider = new KeyProvider(keyConfig); + description.keyProvider = keyProvider; + const cryptor = new Aes(); + const cryptoConfig = {}; + const cryptoInfo = cryptor.getCryptoInfo(nameResolver.soliditySha3(contract.options.address)); + cryptoConfig['aes'] = cryptor; + description.cryptoProvider = new CryptoProvider(cryptoConfig); + const envelope = { + cryptoInfo: cryptoInfo, + public: sampleDescription, + private: { + name: 'real name', + } + }; + await description + .setDescriptionToContract(contract.options.address, envelope, accounts[0]); + }); + + it('should be able to get a description from a created contract', async () => { + const contract = await dc.create('testdatacontract', accounts[0], businessCenterDomain); + const keyConfig = {}; + keyConfig[nameResolver.soliditySha3(contract.options.address)] = sampleKey; + const keyProvider = new KeyProvider(keyConfig); + description.keyProvider = keyProvider; + const cryptor = new Aes(); + const cryptoConfig = {}; + const cryptoInfo = cryptor.getCryptoInfo(nameResolver.soliditySha3(contract.options.address)); + cryptoConfig['aes'] = cryptor; + description.cryptoProvider = new CryptoProvider(cryptoConfig); + const envelope = { + cryptoInfo: cryptoInfo, + public: sampleDescription, + private: { + name: 'real name', + } + }; + await description + .setDescriptionToContract(contract.options.address, envelope, accounts[0]); + const contractDescription = await description + .getDescriptionFromContract(contract.options.address, accounts[0]); + expect(contractDescription).to.deep.eq(envelope); + }); + }); +}); diff --git a/src/shared-description.ts b/src/shared-description.ts new file mode 100644 index 00000000..c3dfd1bb --- /dev/null +++ b/src/shared-description.ts @@ -0,0 +1,117 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import { Envelope } from '@evan.network/dbcp'; + +import * as Dbcp from '@evan.network/dbcp'; + +import { Sharing } from './contracts/sharing'; + +export interface DescriptionOptions extends Dbcp.DescriptionOptions { + sharing: Sharing, +} + +export class Description extends Dbcp.Description { + sharing: Sharing; + + constructor(options: DescriptionOptions) { + super(options); + this.sharing = options.sharing; + } + + /** + * loads description envelope from contract + * + * @param {string} ensAddress The ens address where the description is stored + * @return {Envelope} description as an Envelope + */ + async getDescriptionFromContract(contractAddress: string, accountId: string): Promise { + let result = null; + const contract = this.contractLoader.loadContract('Described', contractAddress); + const hash = await this.executor.executeContractCall(contract, 'contractDescription'); + if (hash && hash !== '0x0000000000000000000000000000000000000000000000000000000000000000') { + const content = (await this.dfs.get(hash)).toString(this.encodingEnvelope); + result = JSON.parse(content); + if (result.private && result.cryptoInfo) { + if (this.sharing) { + try { + const cryptor = this.cryptoProvider.getCryptorByCryptoInfo(result.cryptoInfo); + const sharingKey = await this.sharing.getKey(contractAddress, accountId, '*', result.cryptoInfo.block); + const key = sharingKey; + const privateData = await cryptor.decrypt( + Buffer.from(result.private, this.encodingEncrypted), { key, }); + result.private = privateData; + } catch (e) { + result.private = new Error('wrong_key'); + } + } else { + result.private = new Error('profile_sharing_missing'); + } + } + } + return result; + }; + + + /** + * store description at contract + * + * @param {string} contractAddress The contract address where description will be + * stored + * @param {Envelope|string} envelope description as an envelope or a presaved description hash + * @param {string} accountId ETH account id + * @return {Promise} resolved when done + */ + async setDescriptionToContract(contractAddress: string, envelope: Envelope|string, accountId: string): + Promise { + let hash; + if (typeof envelope === 'string') { + hash = envelope; + } else { + const content: Envelope = Object.assign({}, envelope); + // add dbcp version + content.public.dbcpVersion = content.public.dbcpVersion || this.dbcpVersion; + const validation = this.validateDescription(content); + if (validation !== true) { + throw new Error(`description invalid: ${JSON.stringify(validation)}`); + } + if (content.private && content.cryptoInfo) { + const cryptor = this.cryptoProvider.getCryptorByCryptoInfo(content.cryptoInfo); + const blockNr = await this.web3.eth.getBlockNumber(); + const sharingKey = await this.sharing.getKey(contractAddress, accountId, '*', blockNr); + const key = sharingKey; + const encrypted = await cryptor.encrypt(content.private, { key, }); + content.private = encrypted.toString(this.encodingEncrypted); + content.cryptoInfo.block = blockNr; + } + hash = await this.dfs.add( + 'description', Buffer.from(JSON.stringify(content), this.encodingEnvelope)); + } + const contract = this.contractLoader.loadContract('Described', contractAddress); + await this.executor.executeContractTransaction(contract, 'setContractDescription', {from: accountId, gas: 200000}, hash); + }; +} diff --git a/src/test/accounts.ts b/src/test/accounts.ts new file mode 100644 index 00000000..5f59739a --- /dev/null +++ b/src/test/accounts.ts @@ -0,0 +1,38 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +const accountMap = { + '0x001De828935e8c7e4cb56Fe610495cAe63fb2612': + '01734663843202e2245e5796cb120510506343c67915eb4f9348ac0d8c2cf22a', + '0x0030C5e7394585400B1FB193DdbCb45a37Ab916E': + '7d09c0873e3f8dc0c7282bb7c2ba76bfd432bff53c38ace06193d1e4faa977e7', + '0x00D1267B27C3A80080f9E1B6Ba01DE313b53Ab58': + 'a76a2b068fb715830d042ca40b1a4dab8d088b217d11af91d15b972a7afaf202', +}; +const accounts = Object.keys(accountMap); + +export { accounts, accountMap } diff --git a/src/test/test-utils.ts b/src/test/test-utils.ts new file mode 100644 index 00000000..2352f324 --- /dev/null +++ b/src/test/test-utils.ts @@ -0,0 +1,350 @@ +/* + Copyright (C) 2018-present evan GmbH. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. + If not, see http://www.gnu.org/licenses/ or write to the + + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, 02110-1301 USA, + + or download the license from the following URL: https://evan.network/license/ + + You can be released from the requirements of the GNU Affero General Public License + by purchasing a commercial license. + Buying such a license is mandatory as soon as you use this software or parts of it + on other blockchains than evan.network. + + For more information, please contact evan GmbH at this address: https://evan.network/license/ +*/ + +import IpfsApi = require('ipfs-api'); +import smartContract = require('@evan.network/smart-contracts-core'); +import smartContractsAdmin = require('@evan.network/smart-contracts-admin'); +import Web3 = require('web3'); + +import { + AccountStore, + ContractLoader, + DfsInterface, + EventHub, + Executor, + Ipfs, + KeyProvider, + Logger, + NameResolver, + SignerInternal, + Unencrypted, +} from '@evan.network/dbcp'; + +import { accountMap } from './accounts'; +import { accounts } from './accounts'; +import { Aes } from '../encryption/aes'; +import { AesEcb } from '../encryption/aes-ecb'; +import { BaseContract } from '../contracts/base-contract/base-contract'; +import { config } from './../config'; +import { CryptoProvider } from '../encryption/crypto-provider'; +import { DataContract } from '../contracts/data-contract/data-contract'; +import { Ipld } from '../dfs/ipld'; +import { Profile } from '../profile/profile'; +import { RightsAndRoles } from '../contracts/rights-and-roles'; +import { ServiceContract } from '../contracts/service-contract/service-contract'; +import { setTimeout } from 'timers'; +import { Description } from '../shared-description'; +import { Sharing } from '../contracts/sharing'; + + +export const publicMailBoxExchange = 'mailboxKeyExchange'; +export const sampleContext = 'context sample'; + +const web3Provider = 'ws://localhost:8546'; +const helperWeb3 = new Web3(null); +const sampleKeys = {}; +// dataKeys +sampleKeys[helperWeb3.utils.soliditySha3(accounts[0])] = + '001de828935e8c7e4cb56fe610495cae63fb2612000000000000000000000000'; // plain acc0 key +sampleKeys[helperWeb3.utils.soliditySha3(accounts[1])] = + '0030c5e7394585400b1fb193ddbcb45a37ab916e000000000000000000000011'; // plain acc1 key +sampleKeys[helperWeb3.utils.soliditySha3(sampleContext)] = + '00000000000000000000000000000000000000000000000000000000005a3973'; +sampleKeys[helperWeb3.utils.soliditySha3(publicMailBoxExchange)] = + '346c22768f84f3050f5c94cec98349b3c5cbfa0b7315304e13647a4918ffff22'; // accX <--> mailbox edge key +sampleKeys[helperWeb3.utils.soliditySha3('wulfwulf.test')] = + '00000000000000000000000000000000000000000000000000000000005a3973'; +sampleKeys[helperWeb3.utils.soliditySha3(accounts[2])] = + '00d1267b27c3a80080f9e1b6ba01de313b53ab58000000000000000000000022'; + +// commKeys +sampleKeys[helperWeb3.utils.soliditySha3.apply(helperWeb3.utils.soliditySha3, + [helperWeb3.utils.soliditySha3(accounts[0]), helperWeb3.utils.soliditySha3(accounts[0])].sort())] = + '001de828935e8c7e4cb56fe610495cae63fb2612000000000000000000000000'; // acc0 <--> acc0 edge key +sampleKeys[helperWeb3.utils.soliditySha3.apply(helperWeb3.utils.soliditySha3, + [helperWeb3.utils.soliditySha3(accounts[0]), helperWeb3.utils.soliditySha3(accounts[1])].sort())] = + '001de828935e8c7e4cb50030c5e7394585400b1f000000000000000000000001'; // acc0 <--> acc1 edge key +sampleKeys[helperWeb3.utils.soliditySha3.apply(helperWeb3.utils.soliditySha3, + [helperWeb3.utils.soliditySha3(accounts[0]), helperWeb3.utils.soliditySha3(accounts[2])].sort())] = + '001de828935e8c7e4cb500d1267b27c3a80080f9000000000000000000000002'; // acc0 <--> acc1 edge key +sampleKeys[helperWeb3.utils.soliditySha3.apply(helperWeb3.utils.soliditySha3, + [helperWeb3.utils.soliditySha3(accounts[1]), helperWeb3.utils.soliditySha3(accounts[1])].sort())] = + '0030c5e7394585400b1fb193ddbcb45a37ab916e000000000000000000000011'; +sampleKeys[helperWeb3.utils.soliditySha3.apply(helperWeb3.utils.soliditySha3, + [helperWeb3.utils.soliditySha3(accounts[1]), helperWeb3.utils.soliditySha3(accounts[2])].sort())] = + '0030c5e7394585400b1f00d1267b27c3a80080f9000000000000000000000012'; // acc1 <--> acc2 edge key +sampleKeys[helperWeb3.utils.soliditySha3.apply(helperWeb3.utils.soliditySha3, + [helperWeb3.utils.soliditySha3(accounts[2]), helperWeb3.utils.soliditySha3(accounts[2])].sort())] = + '00d1267b27c3a80080f9e1b6ba01de313b53ab58000000000000000000000022'; + + +export class TestUtils { + static getAccountStore(options): AccountStore { + return new AccountStore({ accounts: accountMap, }); + } + + static async getBaseContract(web3): Promise { + return new BaseContract({ + executor: await TestUtils.getExecutor(web3), + loader: await TestUtils.getContractLoader(web3), + log: Logger.getDefaultLog(), + nameResolver: await TestUtils.getNameResolver(web3), + }); + }; + + static getConfig(): any { + return config; + } + + static async getContractLoader(web3): Promise { + const contracts = await this.getContracts(); + return new ContractLoader({ + contracts, + web3 + }); + } + + static async getContracts() { + const solc = new smartContract.Solc({ + log: Logger.getDefaultLog(), + config: { compileContracts: false, }, + }); + await solc.ensureCompiled([smartContractsAdmin.getContractsPath()]); + return solc.getContracts(); + } + + static getCryptoProvider() { + const cryptor = new Aes(); + const unencryptedCryptor = new Unencrypted(); + const cryptoConfig = {}; + const cryptoInfo = cryptor.getCryptoInfo(helperWeb3.utils.soliditySha3(accounts[0])); + cryptoConfig['aes'] = cryptor; + cryptoConfig['aesEcb'] = new AesEcb(); + cryptoConfig['unencrypted'] = unencryptedCryptor; + return new CryptoProvider(cryptoConfig); + } + + static async getDataContract(web3, dfs) { + const sharing = await this.getSharing(web3, dfs); + const description = await this.getDescription(web3, dfs); + description.sharing = sharing; + const eventHub = await this.getEventHub(web3); + const executor = await this.getExecutor(web3); + executor.eventHub = eventHub; + return new DataContract({ + cryptoProvider: this.getCryptoProvider(), + dfs, + executor, + loader: await this.getContractLoader(web3), + log: TestUtils.getLogger(), + nameResolver: await this.getNameResolver(web3), + sharing, + web3: TestUtils.getWeb3(), + description: await TestUtils.getDescription(web3, dfs), + }); + } + + static async getDescription(web3, dfsParam?: DfsInterface): Promise { + const executor = await this.getExecutor(web3); + const contracts = await this.getContracts(); + const contractLoader = await this.getContractLoader(web3); + const dfs = dfsParam || await this.getIpfs(); + const nameResolver = await this.getNameResolver(web3); + const cryptoProvider = this.getCryptoProvider(); + return new Description({ + contractLoader, + cryptoProvider, + dfs, + executor, + keyProvider: this.getKeyProvider(), + nameResolver, + sharing: null, + web3, + }); + } + + static async getEventHub(web3): Promise { + return new EventHub({ + config: config.nameResolver, + contractLoader: await this.getContractLoader(web3), + log: this.getLogger(), + nameResolver: await this.getNameResolver(web3), + }); + } + + static async getExecutor(web3, isReadonly?): Promise { + if (isReadonly) { + return new Executor({}); + } else { + const contracts = await this.getContracts(); + const contractLoader = new ContractLoader({ + contracts, + web3, + }); + const accountStore = this.getAccountStore({}); + const signer = new SignerInternal({ + accountStore, + contractLoader, + config: {}, + web3, + }); + const executor = new Executor({ config, signer, web3, }); + await executor.init({}); + + return executor; + } + } + + static async getIpld(_ipfs?: Ipfs, _keyProvider?: KeyProvider): Promise { + const cryptor = new Aes(); + const key = await cryptor.generateKey(); + const ipfs = _ipfs ? _ipfs : await this.getIpfs(); + const nameResolver = await this.getNameResolver(await this.getWeb3()); + return new Promise((resolve) => { + // crypto provider + const cryptoConfig = {}; + const cryptoInfo = cryptor.getCryptoInfo(helperWeb3.utils.soliditySha3(accounts[0])); + const cryptoProvider = this.getCryptoProvider(); + // key provider + const keyProvider = _keyProvider || (new KeyProvider({ keys: sampleKeys, })); + + resolve(new Ipld({ + ipfs, + keyProvider, + cryptoProvider, + defaultCryptoAlgo: 'aes', + originator: nameResolver.soliditySha3(accounts[0]), + nameResolver, + })) + }); + } + + static async getIpfs(): Promise { + return new Promise((resolve) => { + const remoteNode = IpfsApi({host: 'ipfs.evan.network', port: '443', protocol: 'https'}) + resolve(new Ipfs({ remoteNode})); + }); + } + + static getKeyProvider(requestedKeys?: string[]) { + let keys; + if (!requestedKeys) { + keys = sampleKeys; + } else { + keys = {}; + requestedKeys.forEach((key) => { + keys[key] = sampleKeys[key]; + }); + } + return new KeyProvider({ keys, }); + } + + static getKeys(): any { + return sampleKeys; + } + + static getLogger(): Function { + return Logger.getDefaultLog(); + } + + static async getNameResolver(web3): Promise { + const contracts = await this.getContracts(); + const contractLoader = new ContractLoader({ + contracts, + web3, + }); + const executor = await this.getExecutor(web3); + const nameResolver = new NameResolver({ + config: config.nameResolver, + executor, + contractLoader, + web3, + }); + + return nameResolver; + } + + static async getProfile(web3, ipfs?, ipld?, accountId?): Promise { + const executor = await TestUtils.getExecutor(web3); + executor.eventHub = await TestUtils.getEventHub(web3); + const profile = new Profile({ + accountId: accountId || accounts[0], + contractLoader: await TestUtils.getContractLoader(web3), + dataContract: await TestUtils.getDataContract(web3, ipfs), + defaultCryptoAlgo: 'aes', + executor, + ipld: ipld || await TestUtils.getIpld(ipfs), + nameResolver: await TestUtils.getNameResolver(web3), + }); + return profile; + } + + static async getRightsAndRoles(web3) { + return new RightsAndRoles({ + contractLoader: await TestUtils.getContractLoader(web3), + executor: await TestUtils.getExecutor(web3) , + nameResolver: await TestUtils.getNameResolver(web3), + web3, + }); + } + + static async getServiceContract(web3, ipfs?: Ipfs, keyProvider?: KeyProvider) { + const executor = await TestUtils.getExecutor(web3); + executor.eventHub = await TestUtils.getEventHub(web3); + const dfs = ipfs || await TestUtils.getIpfs(); + return new ServiceContract({ + cryptoProvider: TestUtils.getCryptoProvider(), + dfs, + executor, + keyProvider: keyProvider || TestUtils.getKeyProvider(), + loader: await TestUtils.getContractLoader(web3), + log: TestUtils.getLogger(), + nameResolver: await TestUtils.getNameResolver(web3), + sharing: await TestUtils.getSharing(web3, ipfs), + web3, + }); + } + + static async getSharing(web3, dfsParam?: DfsInterface): Promise { + const dfs = dfsParam ? dfsParam : await TestUtils.getIpfs(); + return new Sharing({ + contractLoader: await TestUtils.getContractLoader(web3), + cryptoProvider: TestUtils.getCryptoProvider(), + description: await TestUtils.getDescription(web3, dfs), + executor: await TestUtils.getExecutor(web3), + dfs, + keyProvider: TestUtils.getKeyProvider(), + nameResolver: await TestUtils.getNameResolver(web3), + defaultCryptoAlgo: 'aes', + }); + } + + static getWeb3(provider = web3Provider) { + // connect to web3 + return new Web3(new Web3.providers.WebsocketProvider(provider)); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..bd032e4e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "declaration": true, + "inlineSourceMap": true, + "inlineSources": true, + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "target": "es2017", + "lib": [ + "es2017" + ], + "paths": { + "bcc": ["./src/bundles/bcc/bcc"] + } + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 00000000..44b1f0a6 --- /dev/null +++ b/tslint.json @@ -0,0 +1,92 @@ +{ + "rulesDirectory": [ + ], + "rules": { + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": true, + "forin": true, + "indent": [ + true, + "spaces" + ], + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + "static-before-instance", + "variables-before-functions" + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-empty": false, + "no-eval": true, + "no-inferrable-types": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-unused-variable": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +}