├── .eslintignore ├── .eslintrc.js ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── SECURITY.md ├── SUPPORT.md ├── codeql │ └── codeql-config.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── api.md ├── classes │ └── TrytesHelper.md └── interfaces │ ├── IMamChannelState.md │ ├── IMamFetchedMessage.md │ └── IMamMessage.md ├── esm-modules.js ├── examples ├── README.md ├── api │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── now.json │ ├── package-lock.json │ ├── package.json │ ├── spec │ │ ├── _changelog.md │ │ ├── _example.md │ │ ├── api.json │ │ └── logo.png │ ├── src │ │ ├── api │ │ │ └── handler.ts │ │ ├── auth.ts │ │ ├── data │ │ │ └── config.template.json │ │ ├── docs │ │ │ └── index.html │ │ ├── errors │ │ │ ├── httpError.ts │ │ │ └── httpStatusCodes.ts │ │ ├── factories │ │ │ └── serviceFactory.ts │ │ ├── index.ts │ │ ├── initServices.ts │ │ ├── models │ │ │ ├── api │ │ │ │ ├── IDataResponse.ts │ │ │ │ ├── IResponse.ts │ │ │ │ └── v0 │ │ │ │ │ ├── IFetchRequest.ts │ │ │ │ │ ├── IFetchResponse.ts │ │ │ │ │ ├── IPublishRequest.ts │ │ │ │ │ └── IPublishResponse.ts │ │ │ ├── app │ │ │ │ ├── IHttpRequest.ts │ │ │ │ ├── IHttpResponse.ts │ │ │ │ └── IRoute.ts │ │ │ └── configuration │ │ │ │ └── IConfiguration.ts │ │ ├── routes.ts │ │ ├── routes │ │ │ └── v0 │ │ │ │ ├── fetch.ts │ │ │ │ └── publish.ts │ │ └── utils │ │ │ ├── apiHelper.ts │ │ │ └── validationHelper.ts │ ├── test │ │ ├── docs.http │ │ ├── example.js │ │ ├── fetch.http │ │ ├── publish.http │ │ └── version.http │ └── tsconfig.json ├── browser │ └── index.html ├── listen │ ├── index.js │ ├── package-lock.json │ └── package.json ├── simple-json │ ├── index.js │ ├── package-lock.json │ └── package.json └── simple │ ├── index.js │ ├── package-lock.json │ └── package.json ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── index-browser.ts ├── index-node.ts ├── index.ts ├── mam │ ├── channel.ts │ ├── client.ts │ └── parser.ts ├── merkle │ ├── merkleHashGenerator.ts │ ├── merkleNode.ts │ └── merkleTree.ts ├── models │ ├── IMamChannelState.ts │ ├── IMamFetchedMessage.ts │ ├── IMamMessage.ts │ └── mamMode.ts ├── pearlDiver │ ├── hammingDiver.ts │ └── pearlDiverSearchStates.ts ├── signing │ └── iss-p27.ts └── utils │ ├── arrayHelper.ts │ ├── guards.ts │ ├── mask.ts │ ├── pascal.ts │ ├── textHelper.ts │ └── trytesHelper.ts ├── test ├── mam │ ├── channel.spec.ts │ ├── createAndParse.spec.ts │ └── parser.spec.ts ├── pearlDiver │ └── hammingDiver.spec.ts ├── signing │ └── iss-p27.spec.ts └── utils │ ├── mask.spec.ts │ ├── pascal.spec.ts │ ├── textHelper.spec.ts │ └── trytesHelper.spec.ts ├── tsconfig.eslint.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | rollup.config.js 3 | examples 4 | dist 5 | 6 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Code of Conduct 2 | 3 | In the IOTA community, participants from all over the world come together to create. This is made possible by the support, hard work and enthusiasm of thousands of people, including those who create and use the IOTA technology. 4 | 5 | This document offers some guidance to ensure IOTA participants can cooperate effectively in a positive and inspiring atmosphere, and to explain how together we can strengthen and support each other. 6 | 7 | This Code of Conduct is shared by all contributors and users who engage with the IOTA Foundation team and its community services. 8 | 9 | ## Overview 10 | 11 | This Code of Conduct presents a summary of the shared values and “common sense” thinking in our community. The basic social ingredients that hold our project together include: 12 | 13 | - Being considerate 14 | - Being respectful 15 | - Being collaborative 16 | - Being pragmatic 17 | - Supporting others in the community 18 | - Getting support from others in the community 19 | 20 | This Code of Conduct reflects the agreed standards of behavior for members of the IOTA community, in any social media platform, forum, mailing list, wiki, web site, discord channel, public meeting or private correspondence within the context of the IOTA Foundation team and the IOTA Tangle technology. The community acts according to the standards written down in this Code of Conduct and will defend these standards for the benefit of the community. Leaders of any group, such as moderators of social media groups, mailing lists, discord channels, forums, etc., will exercise the right to suspend access to any person who persistently breaks our shared Code of Conduct. 21 | 22 | ## Be considerate 23 | 24 | Your actions and work will affect and be used by other people and you, in turn, will depend on the work and actions of others. Any decision you take will affect other community members, and we expect you to take those consequences into account when making decisions. 25 | 26 | As a user, remember that community members work hard on their part of IOTA and take great pride in it. 27 | 28 | ## Be respectful 29 | 30 | In order for the IOTA community to stay healthy, its members must feel comfortable and accepted. Treating one another with respect is absolutely necessary for this. In a disagreement, in the first instance, assume that people mean well. 31 | 32 | We do not tolerate personal attacks, racism, sexism or any other form of discrimination. Disagreement is inevitable, from time to time, but respect for the views of others will go a long way to winning respect for your own view. Respecting other people, their work, their contributions and assuming well-meaning motivation will make community members feel comfortable and safe and will result in motivation and productivity. 33 | 34 | We expect members of our community to be respectful when dealing with other contributors, users, and communities. Remember that IOTA is an international project and that you may be unaware of important aspects of other cultures. 35 | 36 | ## Be collaborative 37 | 38 | Your feedback is important, as is its form. Poorly thought out comments can cause pain and the demotivation of other community members, but considerate discussion of problems can bring positive results. An encouraging word works wonders. 39 | 40 | ## Be pragmatic 41 | 42 | The IOTA community is pragmatic and fair. We value tangible results over having the last word in a discussion. We defend our core values like freedom and respectful collaboration, but we don’t let arguments about minor issues get in the way of achieving more important results. We are open to suggestions and welcome solutions regardless of their origin. When in doubt support a solution which helps to get things done over one which has theoretical merits, but isn’t being worked on. Use the tools and methods which help to get the job done. Let decisions be taken by those who do the work. 43 | 44 | ## Support others in the community 45 | 46 | The IOTA community is made strong by mutual respect, collaboration and pragmatic, responsible behavior. Sometimes there are situations where this has to be defended and other community members need help. 47 | 48 | If you witness others being attacked, think first about how you can offer them personal support. If you feel that the situation is beyond your ability to help individually, go privately to the victim and ask if some form of official intervention is needed. 49 | 50 | When problems do arise, consider respectfully reminding those involved of our shared Code of Conduct as a first action. Leaders are defined by their actions and can help set a good example by working to resolve issues in the spirit of this Code of Conduct before they escalate. 51 | 52 | ## Get support from others in the community 53 | 54 | Disagreements, both political and technical, happen all the time. Our community is no exception to the rule. The goal is not to avoid disagreements or differing views but to resolve them constructively. You should turn to the community to seek advice and to resolve disagreements and where possible consult the team most directly involved. 55 | 56 | Think deeply before turning a disagreement into a public dispute. If necessary, request mediation, and try to resolve differences in a less emotional medium. If you do feel that you or your work is being attacked, take your time to think things through before writing heated replies. Consider a 24-hour moratorium if emotional language is being used – a cooling-off period is sometimes all that is needed. If you really want to go a different way, then we encourage you to publish your ideas and your work, so that it can be tried and tested. 57 | 58 | This work, "IOTA Community Guidelines", is a derivative of the [Community code of conduct by ownCloud](https://owncloud.org/community/code-of-conduct/), used under [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/). "IOTA Community Guidelines" is licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) by IOTA Foundation. -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to the JavaScript MAM library 2 | 3 | This document describes how to contribute to the JavaScript MAM library. 4 | 5 | We encourage everyone with knowledge of IOTA technology to contribute. 6 | 7 | Thanks! :heart: 8 | 9 |
10 | Do you have a question :question: 11 |
12 | 13 | If you have a general or technical question, you can use one of the following resources instead of submitting an issue: 14 | 15 | - [**Developer documentation:**](https://docs.iota.org/) For official information about developing with IOTA technology 16 | - [**Discord:**](https://discord.iota.org/) For real-time chats with the developers and community members 17 | - [**IOTA cafe:**](https://iota.cafe/) For technical discussions with the Research and Development Department at the IOTA Foundation 18 | - [**StackExchange:**](https://iota.stackexchange.com/) For technical and troubleshooting questions 19 |
20 | 21 |
22 | 23 |
24 | Ways to contribute :mag: 25 |
26 | 27 | To contribute to the JavaScript MAM library on GitHub, you can: 28 | 29 | - Report a bug 30 | - Suggest a new feature 31 | - Build a new feature 32 | - Contribute to the documentation 33 |
34 | 35 |
36 | 37 |
38 | Report a bug :bug: 39 |
40 | 41 | This section guides you through reporting a bug. Following these guidelines helps maintainers and the community understand the bug, reproduce the behavior, and find related bugs. 42 | 43 | ### Before reporting a bug 44 | 45 | Please check the following list: 46 | 47 | - **Do not open a GitHub issue for [security vulnerabilities](SECURITY.MD)**, instead, please contact us at [security@iota.org](mailto:security@iota.org). 48 | 49 | - **Ensure the bug was not already reported** by searching on GitHub under [**Issues**](https://github.com/iotaledger/mam.js/issues). If the bug has already been reported **and the issue is still open**, add a comment to the existing issue instead of opening a new one. You can also find related issues by their [label](https://github.com/iotaledger/mam.js/labels?page=1&sort=name-asc). 50 | 51 | **Note:** If you find a **Closed** issue that seems similar to what you're experiencing, open a new issue and include a link to the original issue in the body of your new one. 52 | 53 | ### Submitting A Bug Report 54 | 55 | To report a bug, [open a new issue](https://github.com/iotaledger/mam.js/issues/new), and be sure to include as many details as possible, using the template. 56 | 57 | **Note:** Minor changes such as fixing a typo can but do not need an open issue. 58 | 59 | If you also want to fix the bug, submit a [pull request](#pull-requests) and reference the issue. 60 |
61 | 62 |
63 | 64 |
65 | Suggest a new feature :bulb: 66 |
67 | 68 | This section guides you through suggesting a new feature. Following these guidelines helps maintainers and the community collaborate to find the best possible way forward with your suggestion. 69 | 70 | ### Before suggesting a new feature 71 | 72 | **Ensure the feature has not already been suggested** by searching on GitHub under [**Issues**](https://github.com/iotaledger/mam.js/issues). 73 | 74 | ### Suggesting a new feature 75 | 76 | To suggest a new feature, talk to the IOTA community and IOTA Foundation members on [Discord](https://discord.iota.org/). 77 | 78 | If the team members approves your feature, they will create an issue for it. 79 |
80 | 81 |
82 | 83 |
84 | Build a new feature :hammer: 85 |
86 | 87 | This section guides you through building a new feature. Following these guidelines helps give your feature the best chance of being approved and merged. 88 | 89 | ### Before building a new feature 90 | 91 | Make sure to discuss the feature with the developers on [Discord](https://discord.iota.org/). 92 | 93 | Otherwise, your feature may not be approved at all. 94 | 95 | ### Building a new feature 96 | 97 | To build a new feature, check out a new branch based on the `next` branch, and be sure to consider the following: 98 | 99 | - If the feature has a public facing API, make sure to document it, using [JSDoc](https://jsdoc.app/) code comments 100 | 101 |
102 | 103 |
104 | 105 |
106 | Contribute to the documentation :black_nib: 107 |
108 | 109 | The JavaScript MAM library documentation is hosted on https://docs.iota.org, which is built from content in the [documentation](https://github.com/iotaledger/documentation) repository. 110 | 111 | Please see the [guidelines](https://github.com/iotaledger/documentation/CONTRIBUTING.md) on the documentation repository for information on how to contribute to the documentation. 112 |
113 | 114 |
115 | 116 |
117 | Pull requests :mega: 118 |
119 | 120 | This section guides you through submitting a pull request (PR). Following these guidelines helps give your PR the best chance of being approved and merged. 121 | 122 | ### Before submitting a pull request 123 | 124 | When creating a pull request, please follow these steps to have your contribution considered by the maintainers: 125 | 126 | - A pull request should have only one concern (for example one feature or one bug). If a PR addresses more than one concern, it should be split into two or more PRs. 127 | 128 | - A pull request can be merged only if it references an open issue 129 | 130 | **Note:** Minor changes such as fixing a typo can but do not need an open issue. 131 | 132 | - All code should be well tested 133 | 134 | ### Submitting a pull request 135 | 136 | The following is a typical workflow for submitting a new pull request: 137 | 138 | 1. Fork this repository 139 | 2. Create a new branch based on your fork 140 | 3. Commit changes and push them to your fork 141 | 4. Create a pull request against the `next` branch 142 | 143 | If all [status checks](https://help.github.com/articles/about-status-checks/) pass, and the maintainer approves the PR, it will be merged. 144 | 145 | **Note:** Reviewers may ask you to complete additional work, tests, or other changes before your pull request can be approved and merged. 146 |
147 | 148 |
149 | 150 |
151 | Code of Conduct :clipboard: 152 |
153 | 154 | This project and everyone participating in it is governed by the [IOTA Code of Conduct](CODE_OF_CONDUCT.md). 155 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a bug in the JavaScript MAM library 3 | about: Report a bug 4 | labels: T - Bug 5 | --- 6 | 7 | ## Bug description 8 | 9 | Briefly describe the bug. 10 | 11 | ## Version 12 | 13 | Which version of the library are you running? 14 | 15 | - Version: 16 | 17 | ## IOTA network 18 | 19 | Which node are you connected to and which IOTA network is it in? 20 | 21 | - Node URL: 22 | - Network: 23 | 24 | ## Hardware specification 25 | 26 | What hardware are you using? 27 | 28 | - Operating system: 29 | - RAM: 30 | - Number of cores: 31 | - Device type: 32 | 33 | ## Steps To reproduce the bug 34 | 35 | Explain how the maintainer can reproduce the bug. 36 | 37 | 1. 38 | 2. 39 | 3. 40 | 41 | ## Expected behaviour 42 | 43 | Describe what you expect to happen. 44 | 45 | ## Actual behaviour 46 | 47 | Describe what actually happens. 48 | 49 | ## Errors 50 | 51 | Paste any errors that you see. 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord 4 | url: https://discord.iota.org/ 5 | about: Please ask and answer questions here. 6 | - name: Security vulnerabilities 7 | url: security@iota.org 8 | about: Please report security vulnerabilities here. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Request a feature for the JavaScript MAM library 3 | about: Request a feature 4 | --- 5 | 6 | ## Description 7 | 8 | Briefly describe the feature that you are requesting. 9 | 10 | ## Motivation 11 | 12 | Explain why this feature is needed. 13 | 14 | ## Requirements 15 | 16 | Write a list of what you want this feature to do. 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 22 | ## Open questions (optional) 23 | 24 | Use this section to ask any questions that are related to the feature. 25 | 26 | ## Are you planning to do it yourself in a pull request? 27 | 28 | Yes/No. 29 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 |

Responsible disclosure policy

2 | 3 | At the IOTA Foundation, we consider the security of our systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present. If you've discovered a vulnerability, please follow the guidelines below to report it to our security team: 4 | 7 | Please follow these rules when testing/reporting vulnerabilities: 8 | 15 | What we promise: 16 | 22 | We sincerely appreciate the efforts of security researchers in keeping our community safe. 23 | 24 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Community resources 2 | 3 | If you have a general or technical question, you can use one of the following resources instead of submitting an issue: 4 | 5 | - [**Developer documentation:**](https://docs.iota.org/) For official information about developing with IOTA technology 6 | - [**Discord:**](https://discord.iota.org/) For real-time chats with the developers and community members 7 | - [**IOTA cafe:**](https://iota.cafe/) For technical discussions with the Research and Development Department at the IOTA Foundation 8 | - [**StackExchange:**](https://iota.stackexchange.com/) For technical and troubleshooting questions -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL Config" 2 | 3 | queries: 4 | - uses: security-and-quality 5 | paths: 6 | - src 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | day: wednesday 8 | time: "06:00" 9 | timezone: Europe/Berlin 10 | labels: 11 | - dependencies 12 | versioning-strategy: increase 13 | ignore: 14 | - dependency-name: "node-fetch" 15 | versions: ["3.x", "4.x"] 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description of change 2 | 3 | Please write a summary of your changes and why you made them. Be sure to reference any related issues by adding `fixes # (issue)`. 4 | 5 | ## Type of change 6 | 7 | Choose a type of change, and delete any options that are not relevant. 8 | 9 | - Bug fix (a non-breaking change which fixes an issue) 10 | - Enhancement (a non-breaking change which adds functionality) 11 | - Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - Documentation Fix 13 | 14 | ## How the change has been tested 15 | 16 | Describe the tests that you ran to verify your changes. 17 | 18 | Make sure to provide instructions for the maintainer as well as any relevant configurations. 19 | 20 | ## Change checklist 21 | 22 | Add an `x` to the boxes that are relevant to your changes, and delete any items that are not. 23 | 24 | - [] My code follows the contribution guidelines for this project 25 | - [] I have performed a self-review of my own code 26 | - [] I have commented my code, particularly in hard-to-understand areas 27 | - [] I have made corresponding changes to the documentation 28 | - [] I have added tests that prove my fix is effective or that my feature works 29 | - [] New and existing unit tests pass locally with my changes 30 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Library Build Main/Develop 2 | 3 | on: 4 | push: 5 | branches: [main, dev] 6 | paths: 7 | - "src/**" 8 | - ".github/workflows/build.yml" 9 | pull_request: 10 | branches: [dev] 11 | paths: 12 | - "src/**" 13 | - ".github/workflows/build.yml" 14 | 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js 12.x 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: "12" 26 | - name: Client Build 27 | run: | 28 | npm install 29 | npm run dist 30 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - dev 8 | paths: 9 | - 'src/**' 10 | - '.github/codeql/**' 11 | - '.github/workflows/codeql-analysis.yml' 12 | pull_request: 13 | paths: 14 | - 'src/**' 15 | - '.github/codeql/**' 16 | - '.github/workflows/codeql-analysis.yml' 17 | schedule: 18 | - cron: '0 0 * * *' 19 | 20 | jobs: 21 | CodeQL-Build: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v2 27 | with: 28 | fetch-depth: 2 29 | 30 | - run: git checkout HEAD^2 31 | if: ${{ github.event_name == 'pull_request' }} 32 | 33 | - name: Initialize CodeQL 34 | uses: github/codeql-action/init@v1 35 | with: 36 | languages: javascript 37 | config-file: ./.github/codeql/codeql-config.yml 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v1 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | node_modules 3 | coverage 4 | channelState.json 5 | dist 6 | es 7 | typings 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.6.3 4 | 5 | * Fix mjs exports for browser 6 | 7 | ## v1.6.2 8 | 9 | * Update iota.js dependencies to use new individual packages for some classes 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 IOTA Stiftung 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mam.js 2 | 3 | > We strongly advise that you update your apps to use [IOTA Streams](https://github.com/iotaledger/streams) - this package is unlikely to be maintained. 4 | 5 | Implementation of Masked Authentication Messaging v0 for IOTA in JavaScript, for use with IOTA network. 6 | 7 | ## Installing 8 | 9 | Install this package using the following commands: 10 | 11 | ```shell 12 | npm install @iota/mam.js 13 | ``` 14 | 15 | If you want to use this module in a browser ` 11 | 12 | 13 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /examples/listen/index.js: -------------------------------------------------------------------------------- 1 | const { createChannel, channelRoot, mamFetchAll, TrytesHelper } = require('@iota/mam.js'); 2 | const fs = require('fs'); 3 | 4 | async function run(root, mode, sideKey, interval) { 5 | const node = "https://chrysalis-nodes.iota.org"; 6 | 7 | setInterval(async () => { 8 | console.log('Fetching from tangle, please wait...'); 9 | const fetched = await mamFetchAll(node, root, mode, sideKey); 10 | if (fetched && fetched.length > 0) { 11 | for (let i = 0; i < fetched.length; i++) { 12 | console.log('Fetched', TrytesHelper.toAscii(fetched[i].message)); 13 | } 14 | root = fetched[fetched.length - 1].nextRoot; 15 | } else { 16 | console.log('Nothing was fetched from the MAM channel'); 17 | } 18 | }, interval); 19 | } 20 | 21 | // Try and load the channel state from json file 22 | try { 23 | const currentState = fs.readFileSync('../simple/channelState.json'); 24 | if (currentState) { 25 | channelState = JSON.parse(currentState.toString()); 26 | 27 | // To start reading from the beginning of the channel clone the channel details 28 | let root = channelRoot(createChannel(channelState.seed, channelState.security, channelState.mode, channelState.sideKey)); 29 | // Or to read from its current position just use the channel state 30 | // let root = channelRoot(channelState); 31 | 32 | run(root, channelState.mode, channelState.sideKey, 5000) 33 | .then(() => console.log("Running in background")) 34 | .catch((err) => console.error(err)); 35 | } else { 36 | throw new Error("The simple example has not been run so there is no channel to listen to"); 37 | } 38 | } catch (e) { 39 | console.error(e); 40 | } 41 | 42 | -------------------------------------------------------------------------------- /examples/listen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "listen", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "@iota/mam.js": "file:../.." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/simple-json/index.js: -------------------------------------------------------------------------------- 1 | const { createChannel, createMessage, parseMessage, mamAttach, mamFetch, TrytesHelper } = require('@iota/mam.js'); 2 | const crypto = require('crypto'); 3 | const fs = require('fs'); 4 | 5 | async function run(payload) { 6 | // Setup the details for the channel. 7 | const mode = 'restricted'; 8 | const sideKey = 'MYKEY'; 9 | let channelState; 10 | 11 | // Try and load the channel state from json file 12 | try { 13 | const currentState = fs.readFileSync('./channelState.json'); 14 | if (currentState) { 15 | channelState = JSON.parse(currentState.toString()); 16 | } 17 | } catch (e) { } 18 | 19 | // If we couldn't load the details then create a new channel. 20 | if (!channelState) { 21 | channelState = createChannel(generateSeed(81), 2, mode, sideKey) 22 | } 23 | 24 | // Create a MAM message using the channel state. 25 | const mamMessage = createMessage(channelState, TrytesHelper.fromAscii(JSON.stringify(payload))); 26 | 27 | // Display the details for the MAM message. 28 | console.log('Seed:', channelState.seed); 29 | console.log('Address:', mamMessage.address); 30 | console.log('Root:', mamMessage.root); 31 | console.log('NextRoot:', channelState.nextRoot); 32 | 33 | // Decode the message using the root and sideKey. 34 | // The decode is for demonstration purposes, there is no reason to decode at this point. 35 | const decodedMessage = parseMessage(mamMessage.payload, mamMessage.root, sideKey); 36 | 37 | // Display the decoded data. 38 | console.log('Decoded NextRoot', decodedMessage.nextRoot); 39 | console.log('Decoded Message', decodedMessage.message); 40 | 41 | // Store the channel state. 42 | try { 43 | fs.writeFileSync('./channelState.json', JSON.stringify(channelState, undefined, "\t")); 44 | } catch (e) { 45 | console.error(e) 46 | } 47 | 48 | // So far we have shown how to create and parse a message 49 | // but now we actually want to attach the message to the tangle 50 | const node = "https://chrysalis-nodes.iota.org"; 51 | 52 | // Attach the message. 53 | console.log('Attaching to tangle, please wait...') 54 | const { messageId } = await mamAttach(node, mamMessage, "MY9MAM"); 55 | console.log(`Message Id`, messageId); 56 | console.log(`You can view the mam channel here https://explorer.iota.org/mainnet/streams/0/${mamMessage.root}/${mode}/${sideKey}`); 57 | 58 | // Try fetching it as well. 59 | console.log('Fetching from tangle, please wait...'); 60 | const fetched = await mamFetch(node, mamMessage.root, mode, sideKey) 61 | if (fetched) { 62 | console.log('Fetched', JSON.parse(TrytesHelper.toAscii(fetched.message))); 63 | } else { 64 | console.log('Nothing was fetched from the MAM channel'); 65 | } 66 | } 67 | 68 | function generateSeed(length) { 69 | const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ9'; 70 | let seed = ''; 71 | while (seed.length < length) { 72 | const byte = crypto.randomBytes(1) 73 | if (byte[0] < 243) { 74 | seed += charset.charAt(byte[0] % 27); 75 | } 76 | } 77 | return seed; 78 | } 79 | 80 | const payload = { 81 | message: 'Hello MAM World!', 82 | timestamp: (new Date()).toLocaleString() 83 | }; 84 | 85 | run(payload) 86 | .then(() => console.log("done")) 87 | .catch((err) => console.error(err)); 88 | -------------------------------------------------------------------------------- /examples/simple-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-json", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "@iota/mam.js": "file:../.." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | const { SingleNodeClient } = require("@iota/iota.js") 2 | const { createChannel, createMessage, parseMessage, mamAttach, mamFetch, TrytesHelper } = require('@iota/mam.js'); 3 | const crypto = require('crypto'); 4 | const fs = require('fs'); 5 | 6 | async function run(asciiMessage) { 7 | // Setup the details for the channel. 8 | const mode = 'restricted'; 9 | const sideKey = 'MYKEY'; 10 | let channelState; 11 | 12 | // Try and load the channel state from json file 13 | try { 14 | const currentState = fs.readFileSync('./channelState.json'); 15 | if (currentState) { 16 | channelState = JSON.parse(currentState.toString()); 17 | } 18 | } catch (e) { } 19 | 20 | // If we couldn't load the details then create a new channel. 21 | if (!channelState) { 22 | channelState = createChannel(generateSeed(81), 2, mode, sideKey) 23 | } 24 | 25 | // Create a MAM message using the channel state. 26 | const mamMessage = createMessage(channelState, TrytesHelper.fromAscii(asciiMessage)); 27 | 28 | // Display the details for the MAM message. 29 | console.log('Seed:', channelState.seed); 30 | console.log('Address:', mamMessage.address); 31 | console.log('Root:', mamMessage.root); 32 | console.log('NextRoot:', channelState.nextRoot); 33 | 34 | // Decode the message using the root and sideKey. 35 | // The decode is for demonstration purposes, there is no reason to decode at this point. 36 | const decodedMessage = parseMessage(mamMessage.payload, mamMessage.root, sideKey); 37 | 38 | // Display the decoded data. 39 | console.log('Decoded NextRoot', decodedMessage.nextRoot); 40 | console.log('Decoded Message', decodedMessage.message); 41 | 42 | // Store the channel state. 43 | try { 44 | fs.writeFileSync('./channelState.json', JSON.stringify(channelState, undefined, "\t")); 45 | } catch (e) { 46 | console.error(e) 47 | } 48 | 49 | // So far we have shown how to create and parse a message 50 | // but now we actually want to attach the message to the tangle 51 | const node = "https://chrysalis-nodes.iota.org"; 52 | const explorerRoot = "https://explorer.iota.org/mainnet"; 53 | 54 | // Attach the message. 55 | console.log('Attaching to tangle, please wait...') 56 | const { messageId } = await mamAttach(new SingleNodeClient(node), mamMessage, "MY9MAM"); 57 | console.log(`Message Id`, messageId); 58 | console.log(`You can view the stored message here ${explorerRoot}/message/${messageId}`); 59 | console.log(`You can view the mam channel here ${explorerRoot}/streams/0/${mamMessage.root}/${mode}/${sideKey}`); 60 | 61 | // Try fetching it as well. 62 | console.log('Fetching from tangle, please wait...'); 63 | const fetched = await mamFetch(node, mamMessage.root, mode, sideKey) 64 | if (fetched) { 65 | console.log('Fetched Root', fetched.root); 66 | console.log('Fetched', TrytesHelper.toAscii(fetched.message)); 67 | console.log('Fetched Next Root', fetched.nextRoot); 68 | } else { 69 | console.log('Nothing was fetched from the MAM channel'); 70 | } 71 | } 72 | 73 | function generateSeed(length) { 74 | const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ9'; 75 | let seed = ''; 76 | while (seed.length < length) { 77 | const byte = crypto.randomBytes(1) 78 | if (byte[0] < 243) { 79 | seed += charset.charAt(byte[0] % 27); 80 | } 81 | } 82 | return seed; 83 | } 84 | 85 | run("Hello MAM World!") 86 | .then(() => console.log("done")) 87 | .catch((err) => console.error(err)); 88 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "@iota/mam.js": "file:../.." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "testMatch": [ 3 | "/test/**/*.(test|spec).ts" 4 | ], 5 | "transform": { 6 | "^.+\\.ts$": "ts-jest" 7 | }, 8 | "collectCoverage": true, 9 | "collectCoverageFrom": [ 10 | '/src/**/*.ts' 11 | ], 12 | "testEnvironment": "node" 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iota/mam.js", 3 | "version": "1.6.3", 4 | "description": "JavaScript implementation of Masked Authentication Message v0 for IOTA", 5 | "author": "Martyn Janes ", 6 | "license": "MIT", 7 | "scripts": { 8 | "build-clean": "rimraf ./es/* ./typings/*", 9 | "build-lint": "eslint src --ext .ts", 10 | "build-compile": "tsc", 11 | "build-watch": "tsc --watch", 12 | "build": "run-s build-clean build-lint build-compile", 13 | "test-lint": "eslint test --ext .ts", 14 | "test-run": "jest", 15 | "test": "run-s test-lint test-run", 16 | "package-clean": "rimraf ./dist/*", 17 | "package-esm": "copyfiles -u 1 es/**/* dist/esm", 18 | "package-esm-rename": "node esm-modules.js ./dist/esm", 19 | "package-umd": "rollup --config rollup.config.js", 20 | "package-umd-min": "rollup --config rollup.config.js --environment MINIFY:true", 21 | "package-umd-browser": "rollup --config rollup.config.js --environment BROWSER:true", 22 | "package-umd-browser-min": "rollup --config rollup.config.js --environment BROWSER:true --environment MINIFY:true", 23 | "package": "run-s package-clean package-esm package-esm-rename package-umd package-umd-min package-umd-browser package-umd-browser-min", 24 | "docs-clean": "rimraf ./docs/*", 25 | "docs-build": "typedoc --disableSources --excludePrivate --excludeInternal --excludeNotDocumented --theme markdown --hideBreadcrumbs --entryDocument api.md --readme none --hideGenerator --sort source-order --exclude ./**/src/index.ts,./**/src/index.*.ts,./**/src/polyfill.*.ts --out ./docs ./src/index.ts", 26 | "docs": "npm-run-all docs-clean docs-build", 27 | "dist": "run-s build test package docs" 28 | }, 29 | "dependencies": { 30 | "@iota/crypto.js": "^1.8.6", 31 | "@iota/iota.js": "^1.8.6", 32 | "@iota/util.js": "^1.8.6", 33 | "big-integer": "^1.6.51" 34 | }, 35 | "devDependencies": { 36 | "@rollup/plugin-commonjs": "^21.0.2", 37 | "@rollup/plugin-node-resolve": "^13.1.3", 38 | "@types/jest": "^27.4.1", 39 | "@typescript-eslint/eslint-plugin": "^5.14.0", 40 | "@typescript-eslint/parser": "^5.14.0", 41 | "copyfiles": "^2.4.1", 42 | "eslint": "^8.10.0", 43 | "eslint-plugin-header": "^3.1.1", 44 | "eslint-plugin-import": "^2.25.4", 45 | "eslint-plugin-jsdoc": "^37.9.7", 46 | "eslint-plugin-unicorn": "^41.0.0", 47 | "jest": "^27.5.1", 48 | "npm-run-all": "^4.1.5", 49 | "rimraf": "^3.0.2", 50 | "rollup": "^2.70.0", 51 | "rollup-plugin-terser": "^7.0.2", 52 | "ts-jest": "^27.1.3", 53 | "typedoc": "^0.22.13", 54 | "typedoc-plugin-markdown": "^3.11.14", 55 | "typescript": "^4.6.2" 56 | }, 57 | "main": "dist/cjs/index-node.js", 58 | "browser": "dist/cjs/index-browser.js", 59 | "module": "dist/esm/index-browser.mjs", 60 | "exports": { 61 | ".": { 62 | "node": { 63 | "import": "./dist/esm/index-node.mjs", 64 | "require": "./dist/cjs/index-node.js" 65 | }, 66 | "browser": { 67 | "import": "./dist/esm/index-browser.mjs", 68 | "require": "./dist/cjs/index-browser.js" 69 | } 70 | }, 71 | "./package.json": "./package.json" 72 | }, 73 | "typings": "typings/index.d.ts", 74 | "files": [ 75 | "dist", 76 | "lib", 77 | "es", 78 | "src", 79 | "typings" 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | 5 | const plugins = [ 6 | commonjs(), 7 | resolve({ 8 | browser: process.env.BROWSER 9 | }), 10 | ]; 11 | 12 | if (process.env.MINIFY) { 13 | plugins.push(terser()); 14 | } 15 | 16 | export default { 17 | input: `./es/index${process.env.BROWSER ? '-browser' : '-node'}.js`, 18 | output: { 19 | file: `dist/cjs/index${process.env.BROWSER ? '-browser' : '-node'}${process.env.MINIFY ? '.min' : ''}.js`, 20 | format: 'umd', 21 | name: 'Mam', 22 | compact: process.env.MINIFY, 23 | globals: { 24 | "big-integer": "bigInt", 25 | "crypto": "crypto", 26 | '@iota/crypto.js': 'IotaCrypto', 27 | '@iota/iota.js': 'Iota', 28 | '@iota/util.js': 'IotaUtil' 29 | } 30 | }, 31 | plugins, 32 | external: process.env.BROWSER 33 | ? ['@iota/crypto.js', '@iota/iota.js', '@iota/util.js', 'big-integer', 'crypto'] 34 | : ['@iota/crypto.js', '@iota/iota.js', '@iota/util.js', 'big-integer', 'crypto'] 35 | } -------------------------------------------------------------------------------- /src/index-browser.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | export * from "./index"; 4 | -------------------------------------------------------------------------------- /src/index-node.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | export * from "./index"; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | export * from "./mam/channel"; 4 | export * from "./mam/client"; 5 | export * from "./mam/parser"; 6 | export * from "./models/IMamChannelState"; 7 | export * from "./models/IMamFetchedMessage"; 8 | export * from "./models/IMamMessage"; 9 | export * from "./models/mamMode"; 10 | export * from "./utils/trytesHelper"; 11 | 12 | -------------------------------------------------------------------------------- /src/mam/channel.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { Curl } from "@iota/crypto.js"; 4 | import { MerkleTree } from "../merkle/merkleTree"; 5 | import type { IMamChannelState } from "../models/IMamChannelState"; 6 | import type { IMamMessage } from "../models/IMamMessage"; 7 | import type { MamMode } from "../models/mamMode"; 8 | import { HammingDiver } from "../pearlDiver/hammingDiver"; 9 | import { signature } from "../signing/iss-p27"; 10 | import { concatenate } from "../utils/arrayHelper"; 11 | import { validateModeKey } from "../utils/guards"; 12 | import { mask, maskHash } from "../utils/mask"; 13 | import { pascalEncode } from "../utils/pascal"; 14 | import { TrytesHelper } from "../utils/trytesHelper"; 15 | 16 | /** 17 | * Create a new channel object. 18 | * @param seed The seed for the channel. 19 | * @param security The security level for the channel. 20 | * @param mode The mode for the channel. 21 | * @param sideKey The side key to use for restricted mode. 22 | * @returns The new channel state. 23 | */ 24 | export function createChannel(seed: string, security: number, mode: MamMode, sideKey?: string): IMamChannelState { 25 | if (!TrytesHelper.isHash(seed)) { 26 | throw new Error("The seed must be 81 trytes long"); 27 | } 28 | if (security < 1 || security > 3) { 29 | throw new Error(`Security must be between 1 and 3, it is ${security}`); 30 | } 31 | validateModeKey(mode, sideKey); 32 | 33 | return { 34 | seed, 35 | mode, 36 | sideKey: mode === "restricted" ? (sideKey ?? "").padEnd(81, "9") : undefined, 37 | security, 38 | start: 0, 39 | count: 1, 40 | nextCount: 1, 41 | index: 0 42 | }; 43 | } 44 | 45 | /** 46 | * Get the root of the channel. 47 | * @param channelState The channel state to get the root. 48 | * @returns The root. 49 | */ 50 | export function channelRoot(channelState: IMamChannelState): string { 51 | if (!channelState) { 52 | throw new Error("channelState must be provided"); 53 | } 54 | if (channelState.start < 0) { 55 | throw new Error("channelState.start must be >= 0"); 56 | } 57 | if (channelState.count <= 0) { 58 | throw new Error("channelState.count must be > 0"); 59 | } 60 | if (channelState.security < 1 || channelState.security > 3) { 61 | throw new Error(`channelState.security must be between 1 and 3, it is ${channelState.security}`); 62 | } 63 | 64 | const tree = new MerkleTree( 65 | channelState.seed, 66 | channelState.start, 67 | channelState.count, 68 | channelState.security); 69 | 70 | return TrytesHelper.fromTrits(tree.root.addressTrits); 71 | } 72 | 73 | /** 74 | * Prepare a message on the mam channel. 75 | * @param channelState The channel to prepare the message for. 76 | * @param message The trytes to include in the message. 77 | * @returns The prepared message, the channel state will also be updated. 78 | */ 79 | export function createMessage(channelState: IMamChannelState, message: string): IMamMessage { 80 | if (!TrytesHelper.isTrytes(message)) { 81 | throw new Error("The message must be in trytes"); 82 | } 83 | const tree = new MerkleTree( 84 | channelState.seed, 85 | channelState.start, 86 | channelState.count, 87 | channelState.security); 88 | const nextRootTree = new MerkleTree( 89 | channelState.seed, 90 | channelState.start + channelState.count, 91 | channelState.nextCount, 92 | channelState.security); 93 | 94 | const nextRootTrits = nextRootTree.root.addressTrits; 95 | 96 | const messageTrits = TrytesHelper.toTrits(message); 97 | const indexTrits = pascalEncode(channelState.index); 98 | const messageLengthTrits = pascalEncode(messageTrits.length); 99 | 100 | const subtree = tree.getSubtree(channelState.index); 101 | 102 | const sponge = new Curl(27); 103 | 104 | const sideKeyTrits = TrytesHelper.toTrits(channelState.sideKey ?? "9".repeat(81)); 105 | sponge.absorb(sideKeyTrits, 0, sideKeyTrits.length); 106 | sponge.absorb(tree.root.addressTrits, 0, tree.root.addressTrits.length); 107 | 108 | let payload = concatenate([indexTrits, messageLengthTrits]); 109 | 110 | sponge.absorb(payload, 0, payload.length); 111 | 112 | // Encrypt the next root along with the message 113 | const maskedNextRoot = mask(concatenate([nextRootTrits, messageTrits]), sponge); 114 | payload = concatenate([payload, maskedNextRoot]); 115 | 116 | // Calculate the nonce for the message so far 117 | const hammingDiver = new HammingDiver(); 118 | const nonceTrits = hammingDiver.search( 119 | sponge.rate(Curl.STATE_LENGTH), 120 | channelState.security, 121 | Curl.HASH_LENGTH / 3, 0 122 | ); 123 | mask(nonceTrits, sponge); 124 | payload = concatenate([payload, nonceTrits]); 125 | 126 | // Create the signature and add the sibling information 127 | const sig = signature(sponge.rate(), subtree.key); 128 | const subtreeTrits = concatenate(subtree.leaves.map(l => l.addressTrits)); 129 | const siblingsCount = subtreeTrits.length / Curl.HASH_LENGTH; 130 | 131 | const encryptedSignature = mask(concatenate([sig, pascalEncode(siblingsCount), subtreeTrits]), sponge); 132 | 133 | // Insert the signature and pad if necessary 134 | payload = concatenate([payload, encryptedSignature]); 135 | const nextThird = payload.length % 3; 136 | if (nextThird !== 0) { 137 | payload = concatenate([payload, new Int8Array(3 - nextThird).fill(0)]); 138 | } 139 | 140 | const messageAddress = channelState.mode === "public" 141 | ? tree.root.addressTrits : maskHash(tree.root.addressTrits); 142 | 143 | const maskedAuthenticatedMessage: IMamMessage = { 144 | payload: TrytesHelper.fromTrits(payload), 145 | root: TrytesHelper.fromTrits(tree.root.addressTrits), 146 | address: TrytesHelper.fromTrits(messageAddress) 147 | }; 148 | 149 | if (channelState.index === channelState.count - 1) { 150 | channelState.start = channelState.nextCount + channelState.start; 151 | channelState.index = 0; 152 | } else { 153 | channelState.index++; 154 | } 155 | 156 | channelState.nextRoot = TrytesHelper.fromTrits(nextRootTrits); 157 | 158 | return maskedAuthenticatedMessage; 159 | } 160 | -------------------------------------------------------------------------------- /src/mam/client.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { Blake2b } from "@iota/crypto.js"; 4 | import { IClient, IIndexationPayload, IMessage, IMessagesResponse, INDEXATION_PAYLOAD_TYPE, SingleNodeClient } from "@iota/iota.js"; 5 | import { Converter } from "@iota/util.js"; 6 | import type { IMamFetchedMessage } from "../models/IMamFetchedMessage"; 7 | import type { IMamMessage } from "../models/IMamMessage"; 8 | import type { MamMode } from "../models/mamMode"; 9 | import { validateModeKey } from "../utils/guards"; 10 | import { maskHash } from "../utils/mask"; 11 | import { TrytesHelper } from "../utils/trytesHelper"; 12 | import { parseMessage } from "./parser"; 13 | 14 | /** 15 | * Attach the mam message to the tangle. 16 | * @param client The client or node endpoint to use for sending. 17 | * @param mamMessage The message to attach. 18 | * @param tag Optional tag for the transactions. 19 | * @returns The transactions that were attached. 20 | */ 21 | export async function mamAttach( 22 | client: IClient | string, 23 | mamMessage: IMamMessage, 24 | tag?: string): Promise<{ 25 | messageId: string; 26 | message: IMessage; 27 | }> { 28 | if (tag !== undefined && typeof tag !== "string") { 29 | throw new Error("MWM and depth are no longer needed when calling mamAttach"); 30 | } 31 | const tagLength = tag ? tag.length : 0; 32 | if (tagLength > 27) { 33 | throw new Error("The tag length is too long"); 34 | } 35 | 36 | const packedTag = tag ? TrytesHelper.packTrytes(tag) : undefined; 37 | const packedTaglength = packedTag ? packedTag.length : 0; 38 | const packedData = TrytesHelper.packTrytes(mamMessage.payload); 39 | 40 | const data = new Uint8Array(1 + packedTaglength + packedData.length); 41 | data[0] = packedTaglength; 42 | if (packedTag) { 43 | data.set(packedTag, 1); 44 | } 45 | data.set(packedData, 1 + packedTaglength); 46 | 47 | const hashedAddress = Blake2b.sum256(Converter.utf8ToBytes(mamMessage.address)); 48 | 49 | const indexationPayload: IIndexationPayload = { 50 | type: INDEXATION_PAYLOAD_TYPE, 51 | index: Converter.bytesToHex(hashedAddress), 52 | data: Converter.bytesToHex(data) 53 | }; 54 | 55 | const message: IMessage = { 56 | payload: indexationPayload 57 | }; 58 | 59 | const localClient = typeof client === "string" ? new SingleNodeClient(client) : client; 60 | const messageId = await localClient.messageSubmit(message); 61 | 62 | return { 63 | message, 64 | messageId 65 | }; 66 | } 67 | 68 | /** 69 | * Fetch a mam message from a channel. 70 | * @param client The client or node endpoint to use for fetching. 71 | * @param root The root within the mam channel to fetch the message. 72 | * @param mode The mode to use for fetching. 73 | * @param sideKey The sideKey if mode is restricted. 74 | * @returns The decoded message and the nextRoot if successful, undefined if no messages found, 75 | * throws exception if transactions found on address are invalid. 76 | */ 77 | export async function mamFetch( 78 | client: IClient | string, 79 | root: string, 80 | mode: MamMode, 81 | sideKey?: string): Promise { 82 | validateModeKey(mode, sideKey); 83 | const localClient = typeof client === "string" ? new SingleNodeClient(client) : client; 84 | 85 | const messageAddress = decodeAddress(root, mode); 86 | 87 | const hashedAddress = Blake2b.sum256(Converter.utf8ToBytes(messageAddress)); 88 | 89 | try { 90 | const messagesResponse: IMessagesResponse = await localClient.messagesFind(hashedAddress); 91 | 92 | const messages: IMessage[] = []; 93 | 94 | for (const messageId of messagesResponse.messageIds) { 95 | try { 96 | const message = await localClient.message(messageId); 97 | messages.push(message); 98 | } catch { } 99 | } 100 | 101 | return await decodeMessages(messages, root, sideKey); 102 | } catch { } 103 | } 104 | 105 | /** 106 | * Decodes the root to its associated address. 107 | * @param root The root to device. 108 | * @param mode The mode for the channel. 109 | * @returns The decoded address. 110 | */ 111 | export function decodeAddress(root: string, mode: MamMode): string { 112 | return mode === "public" 113 | ? root 114 | : TrytesHelper.fromTrits(maskHash(TrytesHelper.toTrits(root))); 115 | } 116 | 117 | /** 118 | * Fetch all the mam message from a channel. 119 | * If limit is undefined we use Number.MAX_VALUE, this could potentially take a long time to complete. 120 | * It is preferable to specify the limit so you read the data in chunks, then if you read and get the 121 | * same amount of messages as your limit you should probably read again. 122 | * @param client The client or node endpoint to use for fetching. 123 | * @param root The root within the mam channel to fetch the message. 124 | * @param mode The mode to use for fetching. 125 | * @param sideKey The sideKey if mode is restricted. 126 | * @param limit Limit the number of messages retrieved. 127 | * @returns The array of retrieved messages. 128 | */ 129 | export async function mamFetchAll( 130 | client: IClient | string, 131 | root: string, 132 | mode: MamMode, 133 | sideKey?: string, 134 | limit?: number): Promise { 135 | const localClient = typeof client === "string" ? new SingleNodeClient(client) : client; 136 | validateModeKey(mode, sideKey); 137 | 138 | const localLimit = limit === undefined ? Number.MAX_VALUE : limit; 139 | const messages: IMamFetchedMessage[] = []; 140 | 141 | let fetchRoot: string | undefined = root; 142 | 143 | do { 144 | const fetched: IMamFetchedMessage | undefined = await mamFetch(localClient, fetchRoot, mode, sideKey); 145 | if (fetched) { 146 | messages.push(fetched); 147 | fetchRoot = fetched.nextRoot; 148 | } else { 149 | fetchRoot = undefined; 150 | } 151 | } while (fetchRoot && messages.length < localLimit); 152 | 153 | return messages; 154 | } 155 | 156 | /** 157 | * Decode messages from an address to try and find a MAM message. 158 | * @param messages The objects returned from the fetch. 159 | * @param root The root within the mam channel to fetch the message. 160 | * @param sideKey The sideKey if mode is restricted. 161 | * @returns The decoded message and the nextRoot if successful, undefined if no messages found, 162 | * throws exception if transactions found on address are invalid. 163 | */ 164 | export async function decodeMessages( 165 | messages: IMessage[], 166 | root: string, 167 | sideKey?: string): 168 | Promise { 169 | if (!messages || messages.length === 0) { 170 | return; 171 | } 172 | 173 | for (const message of messages) { 174 | // We only use indexation payload for storing mam messages 175 | if (message.payload?.type === INDEXATION_PAYLOAD_TYPE && message.payload.data) { 176 | const payloadBytes = Converter.hexToBytes(message.payload.data); 177 | 178 | // We have a minimum size for the message payload 179 | if (payloadBytes.length > 100) { 180 | const packedTagLength = payloadBytes[0]; 181 | const packedTag = packedTagLength > 0 ? payloadBytes.slice(1, 1 + packedTagLength) : undefined; 182 | const packedData = payloadBytes.slice(1 + packedTagLength); 183 | 184 | const tag = packedTag ? TrytesHelper.unpackTrytes(packedTag) : ""; 185 | const data = TrytesHelper.unpackTrytes(packedData); 186 | 187 | try { 188 | const parsed = parseMessage(data, root, sideKey); 189 | return { 190 | root, 191 | ...parsed, 192 | tag 193 | }; 194 | } catch { } 195 | } 196 | } 197 | } 198 | } 199 | 200 | -------------------------------------------------------------------------------- /src/mam/parser.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { Curl } from "@iota/crypto.js"; 4 | import { MerkleTree } from "../merkle/merkleTree"; 5 | import { checksumSecurity, digestFromSignature, PRIVATE_KEY_FRAGMENT_LENGTH } from "../signing/iss-p27"; 6 | import { unmask } from "../utils/mask"; 7 | import { pascalDecode } from "../utils/pascal"; 8 | import { TrytesHelper } from "../utils/trytesHelper"; 9 | 10 | /** 11 | * Parse the trytes back to the original message. 12 | * @param payload The trytes to decode. 13 | * @param root The root for the message. 14 | * @param channelKey The key used to encode the data. 15 | * @returns The decoded message. 16 | */ 17 | export function parseMessage(payload: string, root: string, channelKey?: string): { 18 | /** 19 | * The next root. 20 | */ 21 | nextRoot: string; 22 | /** 23 | * The decoded message. 24 | */ 25 | message: string; 26 | } { 27 | const payloadTrits = TrytesHelper.toTrits(payload); 28 | const rootTrits = TrytesHelper.toTrits(root); 29 | const channelKeyTrits = TrytesHelper.toTrits(channelKey ?? "9".repeat(81)); 30 | 31 | // Get data positions in payload 32 | const indexData = pascalDecode(payloadTrits); 33 | const index = indexData.value; 34 | const messageData = pascalDecode(payloadTrits.slice(indexData.end)); 35 | const messageLength = messageData.value; 36 | const nextRootStart = indexData.end + messageData.end; 37 | const messageStart = nextRootStart + Curl.HASH_LENGTH; 38 | const messageEnd = messageStart + messageLength; 39 | 40 | // Hash the key, root and payload 41 | const sponge = new Curl(27); 42 | sponge.absorb(channelKeyTrits, 0, channelKeyTrits.length); 43 | sponge.absorb(rootTrits, 0, rootTrits.length); 44 | sponge.absorb(payloadTrits, 0, nextRootStart); 45 | 46 | // Decrypt the metadata 47 | const nextRoot = unmask(payloadTrits.slice(nextRootStart, nextRootStart + Curl.HASH_LENGTH), sponge); 48 | const message = unmask(payloadTrits.slice(messageStart, messageStart + messageLength), sponge); 49 | const nonce = unmask(payloadTrits.slice(messageEnd, messageEnd + (Curl.HASH_LENGTH / 3)), sponge); 50 | const hmac = sponge.rate(); 51 | 52 | // Check the security level is valid 53 | const securityLevel = checksumSecurity(hmac); 54 | if (securityLevel === 0) { 55 | throw new Error("Message Hash did not have a hamming weight of zero, security level is invalid"); 56 | } 57 | 58 | // Decrypt the rest of the payload 59 | const decryptedMetadata = unmask(payloadTrits.slice(messageEnd + nonce.length), sponge); 60 | sponge.reset(); 61 | 62 | // Get the signature and absorb its digest 63 | const signature = decryptedMetadata.slice(0, securityLevel * PRIVATE_KEY_FRAGMENT_LENGTH); 64 | const digest = digestFromSignature(hmac, signature); 65 | sponge.absorb(digest, 0, digest.length); 66 | 67 | // Get the sibling information and validate it 68 | const siblingsCountData = pascalDecode(decryptedMetadata.slice(securityLevel * PRIVATE_KEY_FRAGMENT_LENGTH)); 69 | const siblingsCount = siblingsCountData.value; 70 | let recalculatedRoot = sponge.rate(); 71 | if (siblingsCount !== 0) { 72 | const siblingsStart = (securityLevel * PRIVATE_KEY_FRAGMENT_LENGTH) + siblingsCountData.end; 73 | const siblings = decryptedMetadata.slice(siblingsStart, siblingsStart + (siblingsCount * Curl.HASH_LENGTH)); 74 | 75 | recalculatedRoot = MerkleTree.root(recalculatedRoot, siblings, index); 76 | } 77 | 78 | // Make sure the root matches the calculated one 79 | if (TrytesHelper.fromTrits(recalculatedRoot) !== root) { 80 | throw new Error("Signature did not match expected root"); 81 | } 82 | 83 | return { 84 | nextRoot: TrytesHelper.fromTrits(nextRoot), 85 | message: TrytesHelper.fromTrits(message) 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/merkle/merkleHashGenerator.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { address, digestFromSubseed, privateKeyFromSubseed, subseed } from "../signing/iss-p27"; 4 | 5 | /** 6 | * Generate an address for the merklr tree. 7 | * @param seedTrits The trits for the seed. 8 | * @param index The index of the address to generate. 9 | * @param security The security level of the address to generate. 10 | * @returns The address and the private key. 11 | * @internal 12 | */ 13 | export function generateAddress(seedTrits: Int8Array, index: number, security: number): { 14 | /** 15 | * The address generated. 16 | */ 17 | address: Int8Array; 18 | /** 19 | * The private key generated with the address. 20 | */ 21 | privateKey: Int8Array; 22 | } { 23 | const ss = subseed(seedTrits, index); 24 | const dg = digestFromSubseed(ss, security); 25 | 26 | return { 27 | address: address(dg), 28 | privateKey: privateKeyFromSubseed(ss, security) 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/merkle/merkleNode.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | /** 4 | * Class to represent a node in a merkle tree. 5 | * @internal 6 | */ 7 | export class MerkleNode { 8 | /** 9 | * The left element for the node. 10 | */ 11 | public left?: MerkleNode; 12 | 13 | /** 14 | * The right element for the node. 15 | */ 16 | public right?: MerkleNode; 17 | 18 | /** 19 | * The size of the node. 20 | */ 21 | public size: number; 22 | 23 | /** 24 | * The address hash of the node. 25 | */ 26 | public addressTrits: Int8Array; 27 | 28 | /** 29 | * The private key hash of the node. 30 | */ 31 | public privateKeyTrits?: Int8Array; 32 | 33 | /** 34 | * Create a new instance of MerkleNode. 35 | * @param left The left node. 36 | * @param right The right node. 37 | * @param addressTrits The trits representing the address. 38 | * @param privateKeyTrits The trits for the private key. 39 | */ 40 | constructor( 41 | left: MerkleNode | undefined, 42 | right: MerkleNode | undefined, 43 | addressTrits: Int8Array, 44 | privateKeyTrits: Int8Array | undefined) { 45 | this.left = left; 46 | this.right = right; 47 | this.size = (left ? left.size : 0) + (right ? right.size : 0); 48 | this.addressTrits = addressTrits; 49 | this.privateKeyTrits = privateKeyTrits; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/merkle/merkleTree.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { Curl } from "@iota/crypto.js"; 4 | import { TrytesHelper } from "../utils/trytesHelper"; 5 | import { generateAddress } from "./merkleHashGenerator"; 6 | import { MerkleNode } from "./merkleNode"; 7 | 8 | /** 9 | * Class to represent a merkle tree. 10 | * @internal 11 | */ 12 | export class MerkleTree { 13 | /** 14 | * The root of the tree. 15 | */ 16 | public root: MerkleNode; 17 | 18 | /** 19 | * Create a new instance of the merkle tree. 20 | * @param seed The seed to use for the tree. 21 | * @param index The start index for the creation. 22 | * @param count The count for the creation. 23 | * @param security The security level to create the hashes. 24 | */ 25 | constructor(seed: string, index: number, count: number, security: number) { 26 | const seedTrits = TrytesHelper.toTrits(seed); 27 | const leaves = []; 28 | 29 | for (let i = 0; i < count; i++) { 30 | const addressPrivateKey = generateAddress(seedTrits, index + i, security); 31 | leaves.push(new MerkleNode(undefined, undefined, addressPrivateKey.address, addressPrivateKey.privateKey)); 32 | leaves[i].size = 1; 33 | } 34 | 35 | this.root = this.buildTree(leaves); 36 | } 37 | 38 | /** 39 | * Recalculate the root for the siblings. 40 | * @param rate The current address. 41 | * @param siblings The siblings data. 42 | * @param index The index in the tree. 43 | * @returns The new sibling root. 44 | */ 45 | public static root(rate: Int8Array, siblings: Int8Array, index: number): Int8Array { 46 | const sponge = new Curl(27); 47 | let i = 1; 48 | const numChunks = Math.ceil(siblings.length / Curl.HASH_LENGTH); 49 | for (let c = 0; c < numChunks; c++) { 50 | const chunk = siblings.slice(c * Curl.HASH_LENGTH, (c + 1) * Curl.HASH_LENGTH); 51 | sponge.reset(); 52 | 53 | // eslint-disable-next-line no-bitwise 54 | if ((i & index) === 0) { 55 | sponge.absorb(rate, 0, rate.length); 56 | sponge.absorb(chunk, 0, chunk.length); 57 | } else { 58 | sponge.absorb(chunk, 0, chunk.length); 59 | sponge.absorb(rate, 0, rate.length); 60 | } 61 | 62 | // eslint-disable-next-line no-bitwise 63 | i <<= 1; 64 | 65 | rate = sponge.rate(); 66 | } 67 | 68 | return sponge.rate(); 69 | } 70 | 71 | /** 72 | * Get a sub tree. 73 | * @param index The index of the subtree. 74 | * @returns The key and leaves for the sub tree. 75 | */ 76 | public getSubtree(index: number): { 77 | /** 78 | * The combined key for the subtree. 79 | */ 80 | key: Int8Array; 81 | /** 82 | * The leaves of the subtree. 83 | */ 84 | leaves: MerkleNode[]; 85 | } { 86 | if (this.root.size === 1) { 87 | return { 88 | key: this.root.left?.privateKeyTrits 89 | ? this.root.left.privateKeyTrits : new Int8Array(), leaves: [] 90 | }; 91 | } 92 | 93 | const leaves: MerkleNode[] = []; 94 | let node: MerkleNode | undefined = this.root; 95 | let size = this.root.size; 96 | let privateKey: Int8Array | undefined; 97 | 98 | if (index < size) { 99 | while (node) { 100 | if (!node.left) { 101 | privateKey = node.privateKeyTrits; 102 | break; 103 | } 104 | 105 | size = node.left.size; 106 | if (index < size) { 107 | leaves.push(node.right ? node.right : node.left); 108 | node = node.left; 109 | } else { 110 | leaves.push(node.left); 111 | node = node.right; 112 | index -= size; 113 | } 114 | } 115 | } 116 | 117 | leaves.reverse(); 118 | 119 | return { 120 | key: privateKey ?? new Int8Array(), 121 | leaves 122 | }; 123 | } 124 | 125 | /** 126 | * Build tree from the leaf hashes. 127 | * @param leaves The leaves to build the tree from. 128 | * @returns The root node. 129 | */ 130 | private buildTree(leaves: MerkleNode[]): MerkleNode { 131 | const subnodes: MerkleNode[] = []; 132 | 133 | for (let i = 0; i < leaves.length; i += 2) { 134 | const left = leaves[i]; 135 | const right = (i + 1 < leaves.length) ? leaves[i + 1] : undefined; 136 | let addressTrits; 137 | 138 | if (right) { 139 | const sponge = new Curl(27); 140 | 141 | sponge.absorb(left.addressTrits, 0, left.addressTrits.length); 142 | sponge.absorb(right.addressTrits, 0, right.addressTrits.length); 143 | 144 | addressTrits = new Int8Array(Curl.HASH_LENGTH); 145 | sponge.squeeze(addressTrits, 0, addressTrits.length); 146 | } else { 147 | addressTrits = left.addressTrits; 148 | } 149 | 150 | subnodes.push(new MerkleNode(left, right, addressTrits, undefined)); 151 | } 152 | 153 | if (subnodes.length === 1) { 154 | return subnodes[0]; 155 | } 156 | 157 | return this.buildTree(subnodes); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/models/IMamChannelState.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import type { MamMode } from "./mamMode"; 4 | 5 | /** 6 | * Definition of a channel object. 7 | */ 8 | export interface IMamChannelState { 9 | /** 10 | * The seed for the channel. 11 | */ 12 | seed: string; 13 | /** 14 | * The mode for the channel. 15 | */ 16 | mode: MamMode; 17 | /** 18 | * Side key used for restricted mode. 19 | */ 20 | sideKey?: string; 21 | /** 22 | * The security level for the channel. 23 | */ 24 | security: number; 25 | /** 26 | * The start index for the channel. 27 | */ 28 | start: number; 29 | /** 30 | * The count for the channel. 31 | */ 32 | count: number; 33 | /** 34 | * The next root for the channel. 35 | */ 36 | nextRoot?: string; 37 | /** 38 | * The next count for the channel. 39 | */ 40 | nextCount: number; 41 | /** 42 | * The index for the channel. 43 | */ 44 | index: number; 45 | } 46 | -------------------------------------------------------------------------------- /src/models/IMamFetchedMessage.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | /** 4 | * Definition of fetch messages. 5 | */ 6 | export interface IMamFetchedMessage { 7 | /** 8 | * The root the message was fetched from. 9 | */ 10 | root: string; 11 | /** 12 | * The message content fetched. 13 | */ 14 | message: string; 15 | /** 16 | * The next root for the message. 17 | */ 18 | nextRoot: string; 19 | /** 20 | * The tag of the transactions. 21 | */ 22 | tag: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/models/IMamMessage.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | /** 4 | * Definition for MAM message. 5 | */ 6 | export interface IMamMessage { 7 | /** 8 | * The address for the message. 9 | */ 10 | address: string; 11 | /** 12 | * The trytes payload for the message. 13 | */ 14 | payload: string; 15 | /** 16 | * The root for the message. 17 | */ 18 | root: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/models/mamMode.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | /** 4 | * The modes for MAM. 5 | */ 6 | export type MamMode = "public" | "private" | "restricted"; 7 | -------------------------------------------------------------------------------- /src/pearlDiver/hammingDiver.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { Curl } from "@iota/crypto.js"; 4 | import bigInt from "big-integer"; 5 | import { roundThird } from "../utils/pascal"; 6 | import type { PearlDiverSearchStates } from "./pearlDiverSearchStates"; 7 | 8 | /** 9 | * Class to perform Hamming calculation for nonce. 10 | * @internal 11 | */ 12 | export class HammingDiver { 13 | /** 14 | * Max 64 bit value. 15 | */ 16 | private static readonly MAX_VALUE: bigInt.BigInteger = bigInt("FFFFFFFFFFFFFFFF", 16); 17 | 18 | /** 19 | * Min 64 bit value. 20 | */ 21 | private static readonly MIN_VALUE: bigInt.BigInteger = bigInt("0000000000000000", 16); 22 | 23 | /** 24 | * High 0. 25 | */ 26 | private static readonly HIGH_0: bigInt.BigInteger = bigInt("B6DB6DB6DB6DB6DB", 16); 27 | 28 | /** 29 | * High 1. 30 | */ 31 | private static readonly HIGH_1: bigInt.BigInteger = bigInt("8FC7E3F1F8FC7E3F", 16); 32 | 33 | /** 34 | * High 2. 35 | */ 36 | private static readonly HIGH_2: bigInt.BigInteger = bigInt("FFC01FFFF803FFFF", 16); 37 | 38 | /** 39 | * High 3. 40 | */ 41 | private static readonly HIGH_3: bigInt.BigInteger = bigInt("003FFFFFFFFFFFFF", 16); 42 | 43 | /** 44 | * Low 0. 45 | */ 46 | private static readonly LOW_0: bigInt.BigInteger = bigInt("DB6DB6DB6DB6DB6D", 16); 47 | 48 | /** 49 | * Low 1. 50 | */ 51 | private static readonly LOW_1: bigInt.BigInteger = bigInt("F1F8FC7E3F1F8FC7", 16); 52 | 53 | /** 54 | * Low 2. 55 | */ 56 | private static readonly LOW_2: bigInt.BigInteger = bigInt("7FFFE00FFFFC01FF", 16); 57 | 58 | /** 59 | * Low 3. 60 | */ 61 | private static readonly LOW_3: bigInt.BigInteger = bigInt("FFC0000007FFFFFF", 16); 62 | 63 | /** 64 | * Number of rounds. 65 | */ 66 | private static readonly ROUNDS: number = 27; 67 | 68 | /** 69 | * Search for the nonce. 70 | * @param trits The trits to calculate the nonce. 71 | * @param securityLevel The security level to calculate at. 72 | * @param length The length of the data to search. 73 | * @param offset The offset to start the search. 74 | * @returns The trits of the nonce. 75 | */ 76 | public search(trits: Int8Array, securityLevel: number, length: number, offset: number): Int8Array { 77 | const state = this.prepareTrits(trits, offset); 78 | let size = Math.min(length, Curl.HASH_LENGTH) - offset; 79 | 80 | let index = 0; 81 | 82 | while (index === 0) { 83 | const incrementResult = this.increment(state, offset + (size * 2 / 3), offset + size); 84 | size = Math.min(roundThird(offset + (size * 2 / 3) + incrementResult), Curl.HASH_LENGTH) - offset; 85 | 86 | const curlCopy: PearlDiverSearchStates = { 87 | low: state.low.slice(), 88 | high: state.high.slice() 89 | }; 90 | 91 | this.transform(curlCopy); 92 | 93 | index = this.check(securityLevel, curlCopy.low, curlCopy.high); 94 | } 95 | 96 | return this.trinaryGet(state.low, state.high, size, index); 97 | } 98 | 99 | /** 100 | * Prepare the trits for calculation. 101 | * @param trits The trits. 102 | * @param offset The offset to start. 103 | * @returns The prepared trits. 104 | */ 105 | private prepareTrits(trits: Int8Array, offset: number): PearlDiverSearchStates { 106 | const initialState = this.tritsToBigInt(trits, Curl.STATE_LENGTH); 107 | 108 | initialState.low[offset] = HammingDiver.LOW_0; 109 | initialState.low[offset + 1] = HammingDiver.LOW_1; 110 | initialState.low[offset + 2] = HammingDiver.LOW_2; 111 | initialState.low[offset + 3] = HammingDiver.LOW_3; 112 | 113 | initialState.high[offset] = HammingDiver.HIGH_0; 114 | initialState.high[offset + 1] = HammingDiver.HIGH_1; 115 | initialState.high[offset + 2] = HammingDiver.HIGH_2; 116 | initialState.high[offset + 3] = HammingDiver.HIGH_3; 117 | 118 | return initialState; 119 | } 120 | 121 | /** 122 | * Convert the trits to bigint form. 123 | * @param input The input trits. 124 | * @param length The length of the input. 125 | * @returns The trits in big int form. 126 | */ 127 | private tritsToBigInt(input: Int8Array, length: number): PearlDiverSearchStates { 128 | const result: PearlDiverSearchStates = { 129 | low: [], 130 | high: [] 131 | }; 132 | 133 | for (let i = 0; i < input.length; i++) { 134 | switch (input[i]) { 135 | case 0: 136 | result.low[i] = HammingDiver.MAX_VALUE; 137 | result.high[i] = HammingDiver.MAX_VALUE; 138 | break; 139 | case 1: 140 | result.low[i] = HammingDiver.MIN_VALUE; 141 | result.high[i] = HammingDiver.MAX_VALUE; 142 | break; 143 | default: 144 | result.low[i] = HammingDiver.MAX_VALUE; 145 | result.high[i] = HammingDiver.MIN_VALUE; 146 | break; 147 | } 148 | } 149 | 150 | if (input.length >= length) { 151 | return result; 152 | } 153 | 154 | for (let i = input.length; i < length; i++) { 155 | result.low[i] = HammingDiver.MAX_VALUE; 156 | result.high[i] = HammingDiver.MAX_VALUE; 157 | } 158 | 159 | return result; 160 | } 161 | 162 | /** 163 | * Increment the state values. 164 | * @param states The state to increment. 165 | * @param fromIndex The index to start from. 166 | * @param toIndex The index to end at. 167 | * @returns The increment length. 168 | */ 169 | private increment(states: PearlDiverSearchStates, fromIndex: number, toIndex: number): number { 170 | for (let i = fromIndex; i < toIndex; i++) { 171 | const low = states.low[i]; 172 | const high = states.high[i]; 173 | 174 | states.low[i] = high.xor(low); 175 | states.high[i] = low; 176 | 177 | if ((high.and(low.not())).equals(0)) { 178 | return toIndex - fromIndex; 179 | } 180 | } 181 | 182 | return toIndex - fromIndex + 1; 183 | } 184 | 185 | /** 186 | * Transform the states. 187 | * @param searchStates The states to transform. 188 | */ 189 | private transform(searchStates: PearlDiverSearchStates): void { 190 | let curlScratchpadIndex = 0; 191 | for (let round = 0; round < HammingDiver.ROUNDS; round++) { 192 | const curlScratchpad: PearlDiverSearchStates = { 193 | low: searchStates.low.slice(0, Curl.STATE_LENGTH), 194 | high: searchStates.high.slice(0, Curl.STATE_LENGTH) 195 | }; 196 | 197 | for (let stateIndex = 0; stateIndex < Curl.STATE_LENGTH; stateIndex++) { 198 | const alpha = curlScratchpad.low[curlScratchpadIndex]; 199 | const beta = curlScratchpad.high[curlScratchpadIndex]; 200 | curlScratchpadIndex += curlScratchpadIndex < 365 ? 364 : -365; 201 | const gamma = curlScratchpad.high[curlScratchpadIndex]; 202 | const lowXorBeta = curlScratchpad.low[curlScratchpadIndex].xor(beta); 203 | const notGamma = this.bitWiseNot(gamma); 204 | const alphaOrNotGamma = alpha.or(notGamma); 205 | const delta = alphaOrNotGamma.and(lowXorBeta); 206 | 207 | searchStates.low[stateIndex] = this.bitWiseNot(delta); 208 | const alphaXorGamma = alpha.xor(gamma); 209 | searchStates.high[stateIndex] = alphaXorGamma.or(delta); 210 | } 211 | } 212 | } 213 | 214 | /** 215 | * Perform a bitwise not for 64 bit, not twos complement. 216 | * @param value The value to bitwise not. 217 | * @returns The bitwise not of the value. 218 | */ 219 | private bitWiseNot(value: bigInt.BigInteger): bigInt.BigInteger { 220 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 221 | return bigInt(1).shiftLeft(64) 222 | .subtract(bigInt(1)) 223 | .subtract(value); 224 | } 225 | 226 | /** 227 | * Check if we have found the nonce. 228 | * @param securityLevel The security level to check. 229 | * @param low The low bits. 230 | * @param high The high bits. 231 | * @returns The nonce if found. 232 | */ 233 | private check( 234 | securityLevel: number, low: bigInt.BigInteger[], high: bigInt.BigInteger[]): number { 235 | for (let i = 0; i < 64; i++) { 236 | let sum = 0; 237 | 238 | for (let j = 0; j < securityLevel; j++) { 239 | for (let k = j * 243 / 3; k < (j + 1) * 243 / 3; k++) { 240 | const bIndex = bigInt(1).shiftLeft(i); 241 | 242 | if (low[k].and(bIndex).equals(0)) { 243 | sum--; 244 | } else if (high[k].and(bIndex).equals(0)) { 245 | sum++; 246 | } 247 | } 248 | 249 | if (sum === 0 && j < securityLevel - 1) { 250 | sum = 1; 251 | break; 252 | } 253 | } 254 | 255 | if (sum === 0) { 256 | return i; 257 | } 258 | } 259 | 260 | return 0; 261 | } 262 | 263 | /** 264 | * Get data from the tinary bits. 265 | * @param low The low bits. 266 | * @param high The high bits. 267 | * @param arrLength The array length to get from. 268 | * @param index The index to get the values. 269 | * @returns The values stored at the index. 270 | */ 271 | private trinaryGet( 272 | low: bigInt.BigInteger[], high: bigInt.BigInteger[], arrLength: number, index: number): Int8Array { 273 | const result: Int8Array = new Int8Array(arrLength); 274 | 275 | for (let i = 0; i < arrLength; i++) { 276 | const bIndex = bigInt(index); 277 | const l = low[i].shiftRight(bIndex).and(1); 278 | const h = high[i].shiftRight(bIndex).and(1); 279 | 280 | if (l.equals(1) && h.equals(0)) { 281 | result[i] = -1; 282 | } else if (l.equals(0) && h.equals(1)) { 283 | result[i] = 1; 284 | } else { 285 | result[i] = 0; 286 | } 287 | } 288 | 289 | return result; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/pearlDiver/pearlDiverSearchStates.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import type bigInt from "big-integer"; 4 | 5 | /** 6 | * PearDiverSearchStates for storing states during search. 7 | * @internal 8 | */ 9 | export interface PearlDiverSearchStates { 10 | /** 11 | * The low bits of the state. 12 | */ 13 | low: bigInt.BigInteger[]; 14 | 15 | /** 16 | * The high bits of the state. 17 | */ 18 | high: bigInt.BigInteger[]; 19 | } 20 | -------------------------------------------------------------------------------- /src/signing/iss-p27.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { Curl } from "@iota/crypto.js"; 4 | 5 | const PRIVATE_KEY_NUM_FRAGMENTS: number = 27; 6 | // @internal 7 | export const PRIVATE_KEY_FRAGMENT_LENGTH: number = PRIVATE_KEY_NUM_FRAGMENTS * Curl.HASH_LENGTH; 8 | const MIN_TRYTE_VALUE: number = -13; 9 | const MAX_TRYTE_VALUE: number = 13; 10 | const MIN_TRIT_VALUE: number = -1; 11 | const MAX_TRIT_VALUE: number = 1; 12 | 13 | /** 14 | * Calculate the subseed for the seed. 15 | * @param seed The seed trits. 16 | * @param index The index for the subseed. 17 | * @returns The subseed trits. 18 | * @internal 19 | */ 20 | export function subseed(seed: Int8Array, index: number): Int8Array { 21 | const sponge = new Curl(27); 22 | 23 | const subseedPreimage = seed.slice(); 24 | let localIndex = index; 25 | 26 | while (localIndex-- > 0) { 27 | for (let i = 0; i < subseedPreimage.length; i++) { 28 | if (subseedPreimage[i]++ >= MAX_TRIT_VALUE) { 29 | subseedPreimage[i] = MIN_TRIT_VALUE; 30 | } else { 31 | break; 32 | } 33 | } 34 | } 35 | 36 | sponge.absorb(subseedPreimage, 0, subseedPreimage.length); 37 | const ss = new Int8Array(Curl.HASH_LENGTH); 38 | sponge.squeeze(ss, 0, ss.length); 39 | 40 | return ss; 41 | } 42 | 43 | /** 44 | * Get the digest from the subseed. 45 | * @param subSeed The subseed to get the digest for. 46 | * @param securityLevel The security level to get the digest. 47 | * @returns The digest trits. 48 | * @internal 49 | */ 50 | export function digestFromSubseed(subSeed: Int8Array, securityLevel: number): Int8Array { 51 | const curl1 = new Curl(27); 52 | const curl2 = new Curl(27); 53 | const curl3 = new Curl(27); 54 | 55 | const length = securityLevel * PRIVATE_KEY_FRAGMENT_LENGTH / Curl.HASH_LENGTH; 56 | const digest = new Int8Array(Curl.HASH_LENGTH); 57 | 58 | curl1.absorb(subSeed, 0, subSeed.length); 59 | 60 | for (let i = 0; i < length; i++) { 61 | curl1.squeeze(digest, 0, digest.length); 62 | 63 | for (let k = 0; k < MAX_TRYTE_VALUE - MIN_TRYTE_VALUE + 1; k++) { 64 | curl2.reset(); 65 | curl2.absorb(digest, 0, digest.length); 66 | curl2.squeeze(digest, 0, digest.length); 67 | } 68 | 69 | curl3.absorb(digest, 0, digest.length); 70 | } 71 | 72 | curl3.squeeze(digest, 0, digest.length); 73 | 74 | return digest; 75 | } 76 | 77 | /** 78 | * Get the address from the digests. 79 | * @param digests The digests to get the address for. 80 | * @returns The address trits. 81 | * @internal 82 | */ 83 | export function address(digests: Int8Array): Int8Array { 84 | const sponge = new Curl(27); 85 | 86 | sponge.absorb(digests, 0, digests.length); 87 | 88 | const addressTrits = new Int8Array(Curl.HASH_LENGTH); 89 | sponge.squeeze(addressTrits, 0, addressTrits.length); 90 | 91 | return addressTrits; 92 | } 93 | 94 | /** 95 | * Get the private key from the subseed. 96 | * @param subSeed The subseed to get the private key for. 97 | * @param securityLevel The security level for the private key. 98 | * @returns The private key trits. 99 | * @internal 100 | */ 101 | export function privateKeyFromSubseed(subSeed: Int8Array, securityLevel: number): Int8Array { 102 | const keyLength = securityLevel * PRIVATE_KEY_FRAGMENT_LENGTH; 103 | const keyTrits = new Int8Array(keyLength); 104 | const actualKeyTrits: Int8Array = new Int8Array(keyLength); 105 | 106 | const sponge = new Curl(27); 107 | 108 | sponge.absorb(subSeed, 0, subSeed.length); 109 | sponge.squeeze(keyTrits, 0, keyTrits.length); 110 | 111 | for (let i = 0; i < keyLength / Curl.HASH_LENGTH; i++) { 112 | const offset = i * Curl.HASH_LENGTH; 113 | 114 | sponge.reset(); 115 | sponge.absorb(keyTrits, offset, Curl.HASH_LENGTH); 116 | 117 | actualKeyTrits.set(sponge.rate(), offset); 118 | } 119 | 120 | return actualKeyTrits; 121 | } 122 | 123 | /** 124 | * Create a signature for the trits. 125 | * @param hashTrits The trits to create the signature for. 126 | * @param key The key to use for signing. 127 | * @returns The signature trits. 128 | * @internal 129 | */ 130 | export function signature(hashTrits: Int8Array, key: Int8Array): Int8Array { 131 | const signatures: Int8Array = new Int8Array(key.length); 132 | const sponge = new Curl(27); 133 | 134 | for (let i = 0; i < key.length / Curl.HASH_LENGTH; i++) { 135 | let buffer = key.subarray(i * Curl.HASH_LENGTH, (i + 1) * Curl.HASH_LENGTH); 136 | 137 | for (let k = 0; 138 | k < MAX_TRYTE_VALUE - (hashTrits[i * 3] + (hashTrits[(i * 3) + 1] * 3) + (hashTrits[(i * 3) + 2] * 9)); 139 | k++) { 140 | sponge.reset(); 141 | sponge.absorb(buffer, 0, buffer.length); 142 | buffer = sponge.rate(); 143 | } 144 | 145 | signatures.set(buffer, i * Curl.HASH_LENGTH); 146 | } 147 | 148 | return signatures; 149 | } 150 | 151 | /** 152 | * Check the security level. 153 | * @param hash The hash to check. 154 | * @returns The security level 155 | * @internal 156 | */ 157 | export function checksumSecurity(hash: Int8Array): number { 158 | const dataSum1 = hash.slice(0, Curl.HASH_LENGTH / 3); 159 | let sum1 = 0; 160 | for (let i = 0; i < dataSum1.length; i++) { 161 | sum1 += dataSum1[i]; 162 | } 163 | if (sum1 === 0) { 164 | return 1; 165 | } 166 | 167 | const dataSum2 = hash.slice(0, 2 * Curl.HASH_LENGTH / 3); 168 | let sum2 = 0; 169 | for (let i = 0; i < dataSum2.length; i++) { 170 | sum2 += dataSum2[i]; 171 | } 172 | if (sum2 === 0) { 173 | return 2; 174 | } 175 | 176 | let sum3 = 0; 177 | for (let i = 0; i < hash.length; i++) { 178 | sum3 += hash[i]; 179 | } 180 | return sum3 === 0 ? 3 : 0; 181 | } 182 | 183 | /** 184 | * Get the digest from the signature 185 | * @param hash The hash to get the digest. 186 | * @param sig The signature. 187 | * @returns The digest. 188 | * @internal 189 | */ 190 | export function digestFromSignature(hash: Int8Array, sig: Int8Array): Int8Array { 191 | const sponge = new Curl(27); 192 | const bytes: Int8Array = new Int8Array(sig.length); 193 | 194 | for (let i = 0; i < (sig.length / Curl.HASH_LENGTH); i++) { 195 | let innerBytes = sig.slice(i * Curl.HASH_LENGTH, (i + 1) * Curl.HASH_LENGTH); 196 | 197 | for (let j = 0; j < (hash[i * 3] + (hash[(i * 3) + 1] * 3) + (hash[(i * 3) + 2] * 9)) - MIN_TRYTE_VALUE; j++) { 198 | sponge.reset(); 199 | sponge.absorb(innerBytes, 0, innerBytes.length); 200 | innerBytes = sponge.rate(); 201 | } 202 | 203 | bytes.set(innerBytes, i * Curl.HASH_LENGTH); 204 | } 205 | 206 | sponge.reset(); 207 | sponge.absorb(bytes, 0, bytes.length); 208 | 209 | return sponge.rate(); 210 | } 211 | -------------------------------------------------------------------------------- /src/utils/arrayHelper.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | /** 4 | * Concatentate a list of arrays. 5 | * @param arrays The arrays to concatenate. 6 | * @returns The concatenated arrays. 7 | * @internal 8 | */ 9 | export function concatenate(arrays: Int8Array[]): Int8Array { 10 | let totalLength = 0; 11 | for (const arr of arrays) { 12 | totalLength += arr.length; 13 | } 14 | const result = new Int8Array(totalLength); 15 | let offset = 0; 16 | for (const arr of arrays) { 17 | result.set(arr, offset); 18 | offset += arr.length; 19 | } 20 | return result; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/guards.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import type { MamMode } from "../models/mamMode"; 4 | import { TrytesHelper } from "./trytesHelper"; 5 | 6 | /** 7 | * Validate the mode and key. 8 | * @param mode The mamMode to validate. 9 | * @param sideKey The sideKey to validate. 10 | * @internal 11 | */ 12 | export function validateModeKey(mode: MamMode, sideKey?: string): void { 13 | if (mode !== "public" && mode !== "private" && mode !== "restricted") { 14 | throw new Error(`The mode must be public, private or restricted, it is '${mode}'`); 15 | } 16 | if (mode === "restricted") { 17 | if (!sideKey) { 18 | throw new Error("You must provide a sideKey for restricted mode"); 19 | } 20 | if (!TrytesHelper.isTrytes(sideKey)) { 21 | throw new Error("The sideKey must be in trytes"); 22 | } 23 | if (sideKey.length > 81) { 24 | throw new Error("The sideKey must be maximum length 81 trytes"); 25 | } 26 | } 27 | if (mode !== "restricted" && sideKey) { 28 | throw new Error("sideKey is only used in restricted mode"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/mask.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { Curl } from "@iota/crypto.js"; 4 | 5 | /** 6 | * Create the mask hash for the key. 7 | * @param keyTrits The key to create the mask hash for. 8 | * @returns The masked hash. 9 | * @internal 10 | */ 11 | export function maskHash(keyTrits: Int8Array): Int8Array { 12 | const sponge = new Curl(81); 13 | 14 | sponge.absorb(keyTrits, 0, keyTrits.length); 15 | 16 | const finalKeyTrits = new Int8Array(Curl.HASH_LENGTH); 17 | sponge.squeeze(finalKeyTrits, 0, finalKeyTrits.length); 18 | 19 | return finalKeyTrits; 20 | } 21 | 22 | /** 23 | * Apply mask to the payload. 24 | * @param payload The payload to apply the mask to. 25 | * @param sponge The sponge to use. 26 | * @returns The masked payload. 27 | * @internal 28 | */ 29 | export function mask(payload: Int8Array, sponge: Curl): Int8Array { 30 | const keyChunk = sponge.rate(); 31 | 32 | const numChunks = Math.ceil(payload.length / Curl.HASH_LENGTH); 33 | for (let c = 0; c < numChunks; c++) { 34 | const chunk = payload.slice(c * Curl.HASH_LENGTH, (c + 1) * Curl.HASH_LENGTH); 35 | 36 | sponge.absorb(chunk, 0, chunk.length); 37 | 38 | const state = sponge.rate(); 39 | for (let i = 0; i < chunk.length; i++) { 40 | payload[(c * Curl.HASH_LENGTH) + i] = tritSum(chunk[i], keyChunk[i]); 41 | keyChunk[i] = state[i]; 42 | } 43 | } 44 | 45 | return payload; 46 | } 47 | 48 | /** 49 | * Unmask a payload. 50 | * @param payload The payload to unmask. 51 | * @param sponge The sponge to use. 52 | * @returns The unmasked payload. 53 | * @internal 54 | */ 55 | export function unmask(payload: Int8Array, sponge: Curl): Int8Array { 56 | const unmasked: Int8Array = new Int8Array(payload); 57 | 58 | const limit = Math.ceil(unmasked.length / Curl.HASH_LENGTH) * Curl.HASH_LENGTH; 59 | let state; 60 | for (let c = 0; c < limit; c++) { 61 | const indexInChunk = c % Curl.HASH_LENGTH; 62 | 63 | if (indexInChunk === 0) { 64 | state = sponge.rate(); 65 | } 66 | 67 | if (state) { 68 | unmasked[c] = tritSum(unmasked[c], -state[indexInChunk]); 69 | } 70 | 71 | if (indexInChunk === Curl.HASH_LENGTH - 1) { 72 | sponge.absorb(unmasked, Math.floor(c / Curl.HASH_LENGTH) * Curl.HASH_LENGTH, Curl.HASH_LENGTH); 73 | } 74 | } 75 | 76 | return unmasked; 77 | } 78 | 79 | /** 80 | * Sum the parts of a trit. 81 | * @param left The left part. 82 | * @param right The right part. 83 | * @returns The sum. 84 | * @internal 85 | */ 86 | function tritSum(left: number, right: number): number { 87 | const sum = left + right; 88 | 89 | switch (sum) { 90 | case 2: 91 | return -1; 92 | case -2: 93 | return 1; 94 | default: 95 | return sum; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/utils/pascal.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { TrytesHelper } from "./trytesHelper"; 4 | 5 | /* eslint-disable no-bitwise */ 6 | // @internal 7 | const ZERO = new Int8Array([1, 0, 0, -1]); 8 | // @internal 9 | const RADIX: number = 3; 10 | // @internal 11 | const TRITS_PER_TRYTE: number = 3; 12 | 13 | /** 14 | * Perform pascal encoding of the value. 15 | * @param value The value to encode. 16 | * @returns The trits for the encoded value. 17 | * @internal 18 | */ 19 | export function pascalEncode(value: number): Int8Array { 20 | if (value === 0) { 21 | return ZERO; 22 | } 23 | 24 | const length = roundThird(minTrits(Math.abs(value), 1)); 25 | const trits = new Int8Array(encodedLength(value)); 26 | valueToTrits(value, trits); 27 | 28 | let encoding = 0; 29 | let index = 0; 30 | 31 | for (let i = 0; i < length - TRITS_PER_TRYTE; i += TRITS_PER_TRYTE) { 32 | const tritValue = trits.slice(i, i + TRITS_PER_TRYTE); 33 | const tritsAsInt = TrytesHelper.tritsValue(tritValue); 34 | 35 | if (tritsAsInt >= 0) { 36 | encoding |= 1 << index; 37 | for (let j = 0; j < tritValue.length; j++) { 38 | trits[i + j] = -tritValue[j]; 39 | } 40 | } 41 | 42 | index++; 43 | } 44 | 45 | const v = trits.slice(length - TRITS_PER_TRYTE, length - TRITS_PER_TRYTE + length); 46 | if (TrytesHelper.tritsValue(v) < 0) { 47 | encoding |= 1 << index; 48 | for (let k = 0; k < v.length; k++) { 49 | trits[k + length - TRITS_PER_TRYTE] = -trits[k + length - TRITS_PER_TRYTE]; 50 | } 51 | } 52 | 53 | const checksumTrits = new Int8Array(trits.length - length); 54 | valueToTrits(encoding, checksumTrits); 55 | 56 | for (let i = 0; i < checksumTrits.length; i++) { 57 | trits[length + i] = checksumTrits[i]; 58 | } 59 | 60 | return trits; 61 | } 62 | 63 | /** 64 | * Decode the pascal encoded trits. 65 | * @param value The value to decode. 66 | * @returns The decoded value. 67 | * @internal 68 | */ 69 | export function pascalDecode(value: Int8Array): { 70 | /** 71 | * The value from the decode. 72 | */ 73 | value: number; 74 | /** 75 | * The end of the input. 76 | */ 77 | end: number; 78 | } { 79 | if (value.length >= ZERO.length && 80 | value[0] === ZERO[0] && 81 | value[1] === ZERO[1] && 82 | value[2] === ZERO[2] && 83 | value[3] === ZERO[3]) { 84 | return { value: 0, end: 4 }; 85 | } 86 | const encoderStart = end(value); 87 | const inputEnd = encoderStart + (encoderStart / TRITS_PER_TRYTE); 88 | const encoder = TrytesHelper.tritsValue(value.slice(encoderStart, inputEnd)); 89 | 90 | let result = 0; 91 | for (let i = 0; i < encoderStart / TRITS_PER_TRYTE; i++) { 92 | const tritsIntValue = ((encoder >> i) & 1) !== 0 93 | ? -TrytesHelper.tritsValue(value.slice(i * TRITS_PER_TRYTE, (i + 1) * TRITS_PER_TRYTE)) 94 | : TrytesHelper.tritsValue(value.slice(i * TRITS_PER_TRYTE, (i + 1) * TRITS_PER_TRYTE)); 95 | 96 | result += (Math.pow(27, i) * tritsIntValue); 97 | } 98 | 99 | return { value: result, end: inputEnd }; 100 | } 101 | 102 | /** 103 | * Get the encoded length of the value. 104 | * @param value The value. 105 | * @returns The length. 106 | * @internal 107 | */ 108 | function encodedLength(value: number): number { 109 | const length = roundThird(minTrits(Math.abs(value), 1)); 110 | return length + (length / RADIX); 111 | } 112 | 113 | /** 114 | * Round the number to the third. 115 | * @param value The value to round. 116 | * @returns The rounded number. 117 | * @internal 118 | */ 119 | export function roundThird(value: number): number { 120 | const rem = value % RADIX; 121 | if (rem === 0) { 122 | return value; 123 | } 124 | 125 | return value + RADIX - rem; 126 | } 127 | 128 | /** 129 | * Calculate the minimum trits for the input. 130 | * @param input The input to calculate from. 131 | * @param basis The basis of the calculation. 132 | * @returns The number of trits. 133 | * @internal 134 | */ 135 | function minTrits(input: number, basis: number): number { 136 | if (input <= basis) { 137 | return 1; 138 | } 139 | 140 | return 1 + minTrits(input, 1 + (basis * RADIX)); 141 | } 142 | 143 | /** 144 | * Calculate the end for the input. 145 | * @param input The input to calculate for. 146 | * @returns The calculated end. 147 | * @internal 148 | */ 149 | function end(input: Int8Array): number { 150 | if (TrytesHelper.tritsValue(input.slice(0, TRITS_PER_TRYTE)) > 0) { 151 | return TRITS_PER_TRYTE; 152 | } 153 | 154 | return TRITS_PER_TRYTE + end(input.slice(TRITS_PER_TRYTE)); 155 | } 156 | 157 | /** 158 | * Convert the value to trits. 159 | * @param input The input value to convert. 160 | * @param trits The trits. 161 | * @returns The end conversion. 162 | * @internal 163 | */ 164 | function valueToTrits(input: number, trits: Int8Array): number { 165 | const endWrite = writeTrits(input, trits, 0); 166 | 167 | if (input >= 0) { 168 | return endWrite; 169 | } 170 | 171 | for (let i = 0; i < trits.length; i++) { 172 | trits[i] = -trits[i]; 173 | } 174 | 175 | return endWrite; 176 | } 177 | 178 | /** 179 | * Write the trits for the value. 180 | * @param input The input value. 181 | * @param trits The trits to write to. 182 | * @param index The index to write at. 183 | * @returns The length. 184 | * @internal 185 | */ 186 | function writeTrits(input: number, trits: Int8Array, index: number): number { 187 | switch (input) { 188 | case 0: 189 | return 0; 190 | default: 191 | // eslint-disable-next-line no-case-declarations 192 | let abs = Math.floor(input / RADIX); 193 | // eslint-disable-next-line no-case-declarations 194 | let r = input % RADIX; 195 | if (r > 1) { 196 | abs += 1; 197 | r = -1; 198 | } 199 | 200 | trits[index] = r; 201 | index++; 202 | 203 | return 1 + writeTrits(abs, trits, index); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/utils/textHelper.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | /** 4 | * Helper functions for use with text. 5 | */ 6 | export class TextHelper { 7 | /** 8 | * Encode Non ASCII characters to escaped characters. 9 | * @param value The value to encode. 10 | * @returns The encoded value. 11 | */ 12 | public static encodeNonASCII(value: string | undefined): string | undefined { 13 | return typeof value === "string" 14 | ? value.replace(/[\u007F-\uFFFF]/g, chr => `\\u${(`0000${chr.charCodeAt(0).toString(16)}`).slice(-4)}`) 15 | : undefined; 16 | } 17 | 18 | /** 19 | * Decode escaped Non ASCII characters. 20 | * @param value The value to decode. 21 | * @returns The decoded value. 22 | */ 23 | public static decodeNonASCII(value: string | undefined): string | undefined { 24 | return typeof value === "string" 25 | ? value.replace(/\\u(\w{4})/gi, (match, grp) => String.fromCharCode(Number.parseInt(grp, 16))) 26 | : undefined; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/trytesHelper.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { Converter } from "@iota/util.js"; 4 | import { TextHelper } from "./textHelper"; 5 | 6 | /** 7 | * Helper functions for use with trytes. 8 | */ 9 | export class TrytesHelper { 10 | /** 11 | * All the characters that can be used in trytes. 12 | */ 13 | public static ALPHABET: string = "9ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 14 | 15 | /** 16 | * Trytes to trits lookup table. 17 | * @internal 18 | */ 19 | private static readonly TRYTES_TRITS: Int8Array[] = [ 20 | new Int8Array([0, 0, 0]), 21 | new Int8Array([1, 0, 0]), 22 | new Int8Array([-1, 1, 0]), 23 | new Int8Array([0, 1, 0]), 24 | new Int8Array([1, 1, 0]), 25 | new Int8Array([-1, -1, 1]), 26 | new Int8Array([0, -1, 1]), 27 | new Int8Array([1, -1, 1]), 28 | new Int8Array([-1, 0, 1]), 29 | new Int8Array([0, 0, 1]), 30 | new Int8Array([1, 0, 1]), 31 | new Int8Array([-1, 1, 1]), 32 | new Int8Array([0, 1, 1]), 33 | new Int8Array([1, 1, 1]), 34 | new Int8Array([-1, -1, -1]), 35 | new Int8Array([0, -1, -1]), 36 | new Int8Array([1, -1, -1]), 37 | new Int8Array([-1, 0, -1]), 38 | new Int8Array([0, 0, -1]), 39 | new Int8Array([1, 0, -1]), 40 | new Int8Array([-1, 1, -1]), 41 | new Int8Array([0, 1, -1]), 42 | new Int8Array([1, 1, -1]), 43 | new Int8Array([-1, -1, 0]), 44 | new Int8Array([0, -1, 0]), 45 | new Int8Array([1, -1, 0]), 46 | new Int8Array([-1, 0, 0]) 47 | ]; 48 | 49 | /** 50 | * Is the string trytes length 81. 51 | * @param trytes The trytes to test. 52 | * @returns True if it is trytes 81 chars long. 53 | */ 54 | public static isHash(trytes: string): boolean { 55 | return /^[9A-Z]{81}$/.test(trytes); 56 | } 57 | 58 | /** 59 | * Is the string trytes length 27. 60 | * @param trytes The trytes to test. 61 | * @returns True if it is trytes 27 chars long. 62 | */ 63 | public static isTag(trytes: string): boolean { 64 | return /^[9A-Z]{27}$/.test(trytes); 65 | } 66 | 67 | /** 68 | * Is the string trytes of any length. 69 | * @param trytes The trytes to test. 70 | * @returns True if it is trytes. 71 | */ 72 | public static isTrytes(trytes: string): boolean { 73 | return /^[9A-Z]+$/.test(trytes); 74 | } 75 | 76 | /** 77 | * Create a trits array from trytes. 78 | * @param value Trytes used to create trits. 79 | * @returns The trits array. 80 | */ 81 | public static toTrits(value: string): Int8Array { 82 | const trits: Int8Array = new Int8Array(value.length * 3); 83 | 84 | for (let i = 0; i < value.length; i++) { 85 | const idx = TrytesHelper.ALPHABET.indexOf(value.charAt(i)); 86 | trits[i * 3] = TrytesHelper.TRYTES_TRITS[idx][0]; 87 | trits[(i * 3) + 1] = TrytesHelper.TRYTES_TRITS[idx][1]; 88 | trits[(i * 3) + 2] = TrytesHelper.TRYTES_TRITS[idx][2]; 89 | } 90 | 91 | return trits; 92 | } 93 | 94 | /** 95 | * Get trytes from trits array. 96 | * @param trits The trits to convert to trytes. 97 | * @returns Trytes. 98 | */ 99 | public static fromTrits(trits: Int8Array): string { 100 | let trytes = ""; 101 | 102 | for (let i = 0; i < trits.length; i += 3) { 103 | // Iterate over all possible tryte values to find correct trit representation 104 | for (let j = 0; j < TrytesHelper.ALPHABET.length; j++) { 105 | if (TrytesHelper.TRYTES_TRITS[j][0] === trits[i] && 106 | TrytesHelper.TRYTES_TRITS[j][1] === trits[i + 1] && 107 | TrytesHelper.TRYTES_TRITS[j][2] === trits[i + 2]) { 108 | trytes += TrytesHelper.ALPHABET.charAt(j); 109 | break; 110 | } 111 | } 112 | } 113 | 114 | return trytes; 115 | } 116 | 117 | /** 118 | * Convert trits to an integer. 119 | * @param trits The trits to convert. 120 | * @returns The trits converted to number. 121 | */ 122 | public static tritsValue(trits: Int8Array): number { 123 | let value = 0; 124 | 125 | for (let i = trits.length - 1; i >= 0; i--) { 126 | value = (value * 3) + trits[i]; 127 | } 128 | 129 | return value; 130 | } 131 | 132 | /** 133 | * Convert a string value into trytes. 134 | * @param value The value to convert into trytes. 135 | * @returns The trytes representation of the value. 136 | */ 137 | public static fromAscii(value: string): string { 138 | let trytes = ""; 139 | 140 | for (let i = 0; i < value.length; i++) { 141 | const asciiValue = value.charCodeAt(i); 142 | 143 | const firstValue = asciiValue % 27; 144 | const secondValue = (asciiValue - firstValue) / 27; 145 | 146 | trytes += TrytesHelper.ALPHABET[firstValue] + TrytesHelper.ALPHABET[secondValue]; 147 | } 148 | 149 | return trytes; 150 | } 151 | 152 | /** 153 | * Convert trytes into a string value. 154 | * @param trytes The trytes to convert into a string value. 155 | * @returns The string value converted from the trytes. 156 | */ 157 | public static toAscii(trytes: string): string { 158 | const trytesString = trytes; 159 | 160 | if (trytesString.length % 2 === 1) { 161 | throw new Error("The trytes length must be an even number"); 162 | } 163 | 164 | let ascii = ""; 165 | 166 | for (let i = 0; i < trytesString.length; i += 2) { 167 | const firstValue = TrytesHelper.ALPHABET.indexOf(trytesString[i]); 168 | const secondValue = TrytesHelper.ALPHABET.indexOf(trytesString[i + 1]); 169 | 170 | const decimalValue = firstValue + (secondValue * 27); 171 | 172 | ascii += String.fromCharCode(decimalValue); 173 | } 174 | 175 | return ascii; 176 | } 177 | 178 | /** 179 | * Convert an object to Trytes. 180 | * @param obj The obj to encode. 181 | * @returns The encoded trytes value. 182 | */ 183 | public static objectToTrytes(obj: unknown): string { 184 | const json = JSON.stringify(obj); 185 | const encoded = TextHelper.encodeNonASCII(json); 186 | return encoded ? TrytesHelper.fromAscii(encoded) : ""; 187 | } 188 | 189 | /** 190 | * Convert an object from Trytes. 191 | * @param trytes The trytes to decode. 192 | * @returns The decoded object. 193 | */ 194 | public static objectFromTrytes(trytes: string): T | undefined { 195 | if (typeof (trytes) !== "string") { 196 | throw new TypeError("fromTrytes can only convert strings"); 197 | } 198 | 199 | // Trim trailing 9s 200 | const trimmed = trytes.replace(/\9+$/, ""); 201 | 202 | if (trimmed.length === 0) { 203 | throw new Error("fromTrytes trytes does not contain any data"); 204 | } 205 | 206 | const ascii = TrytesHelper.toAscii(trimmed); 207 | const json = TextHelper.decodeNonASCII(ascii); 208 | return json ? JSON.parse(json) as T : undefined; 209 | } 210 | 211 | /** 212 | * Convert a string to Trytes. 213 | * @param str The string to encode. 214 | * @returns The encoded trytes value. 215 | */ 216 | public static stringToTrytes(str: string): string { 217 | const encoded = TextHelper.encodeNonASCII(str); 218 | return encoded ? TrytesHelper.fromAscii(encoded) : ""; 219 | } 220 | 221 | /** 222 | * Convert a string from Trytes. 223 | * @param trytes The trytes to decode. 224 | * @returns The decoded string. 225 | */ 226 | public static stringFromTrytes(trytes: string): string | undefined { 227 | // Trim trailing 9s 228 | let trimmed = trytes.replace(/\9+$/, ""); 229 | 230 | // And make sure it is even length (2 trytes per ascii char) 231 | if (trimmed.length % 2 === 1) { 232 | trimmed += "9"; 233 | } 234 | 235 | const ascii = TrytesHelper.toAscii(trimmed); 236 | 237 | return TextHelper.decodeNonASCII(ascii); 238 | } 239 | 240 | /** 241 | * Pack trytes into bytes. 242 | * @param trytes The trytes to pack. 243 | * @returns The packed trytes. 244 | */ 245 | public static packTrytes(trytes: string): Uint8Array { 246 | const trytesBits: string[] = []; 247 | 248 | for (const tryte of trytes) { 249 | trytesBits.push(TrytesHelper 250 | .ALPHABET 251 | .indexOf(tryte) 252 | .toString(2) 253 | .padStart(5, "0") 254 | ); 255 | } 256 | 257 | let allBits = trytesBits.join(""); 258 | const remainder = allBits.length % 8; 259 | 260 | if (remainder > 0) { 261 | allBits += "1".repeat(8 - remainder); 262 | } 263 | 264 | return Converter.binaryToBytes(allBits); 265 | } 266 | 267 | /** 268 | * Unpack bytes into trytes. 269 | * @param packed The packed trytes to unpack. 270 | * @returns The unpacked trytes. 271 | */ 272 | public static unpackTrytes(packed: Uint8Array): string { 273 | const allBits = Converter.bytesToBinary(packed); 274 | 275 | const trytes: string[] = []; 276 | for (let i = 0; i < allBits.length; i += 5) { 277 | const charBits = allBits.slice(i, i + 5); 278 | if (charBits.length < 5 || charBits === "111111") { 279 | break; 280 | } 281 | trytes.push(TrytesHelper.ALPHABET[Number.parseInt(charBits, 2)]); 282 | } 283 | 284 | return trytes.join(""); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /test/mam/createAndParse.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { createChannel, createMessage } from "../../src/mam/channel"; 4 | import { parseMessage } from "../../src/mam/parser"; 5 | import { TrytesHelper } from "../../src/utils/trytesHelper"; 6 | 7 | test("create and parse", () => { 8 | const channel = createChannel( 9 | "MR9ABFDBQGHRUHBIJCICAVDJVQKYKTVPHDFZHGON9JGYKVMSXKZBBTME9HPZRCLYSEFCZWMPEQKAOQKTZ", 2, "restricted", "MYKEY"); 10 | 11 | for (let i = 0; i < 10; i++) { 12 | const msg = createMessage(channel, TrytesHelper.fromAscii("Hello MAM World!")); 13 | 14 | const res = parseMessage(msg.payload, msg.root, "MYKEY"); 15 | 16 | expect(TrytesHelper.toAscii(res.message)).toBe("Hello MAM World!"); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /test/mam/parser.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { channelRoot, createChannel, createMessage } from "../../src/mam/channel"; 4 | import { parseMessage } from "../../src/mam/parser"; 5 | 6 | test("parseMessage() can decode public message", () => { 7 | const channel = createChannel("A".repeat(81), 2, "public"); 8 | const root = channelRoot(channel); 9 | const msg = createMessage(channel, "FOO"); 10 | 11 | const res = parseMessage(msg.payload, root); 12 | expect(res.message).toBe("FOO"); 13 | expect(res.nextRoot).toBe("ZRBYGMGPEUBFOUMKULUBNCSQQNRH9JOMV9QJEZTAA99HCXLDHFTFOR9UYRKXSEYDRWPSDZQHJIFODHXRS"); 14 | }); 15 | 16 | test("parseMessage() can decode private message", () => { 17 | const channel = createChannel("A".repeat(81), 2, "private"); 18 | const root = channelRoot(channel); 19 | const msg = createMessage(channel, "FOO"); 20 | 21 | const res = parseMessage(msg.payload, root); 22 | expect(res.message).toBe("FOO"); 23 | expect(res.nextRoot).toBe("ZRBYGMGPEUBFOUMKULUBNCSQQNRH9JOMV9QJEZTAA99HCXLDHFTFOR9UYRKXSEYDRWPSDZQHJIFODHXRS"); 24 | }); 25 | 26 | test("parseMessage() can fail is no sideKey supplied", () => { 27 | const channel = createChannel("A".repeat(81), 2, "restricted", "S".repeat(81)); 28 | const root = channelRoot(channel); 29 | const msg = createMessage(channel, "FOO"); 30 | 31 | expect(() => parseMessage(msg.payload, root)).toThrow("Message Hash"); 32 | }); 33 | 34 | test("parseMessage() can decode restricted message", () => { 35 | const channel = createChannel("A".repeat(81), 2, "restricted", "S".repeat(81)); 36 | const root = channelRoot(channel); 37 | const msg = createMessage(channel, "FOO"); 38 | 39 | const res = parseMessage(msg.payload, root, "S".repeat(81)); 40 | expect(res.message).toBe("FOO"); 41 | expect(res.nextRoot).toBe("ZRBYGMGPEUBFOUMKULUBNCSQQNRH9JOMV9QJEZTAA99HCXLDHFTFOR9UYRKXSEYDRWPSDZQHJIFODHXRS"); 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /test/pearlDiver/hammingDiver.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { HammingDiver } from "../../src/pearlDiver/hammingDiver"; 4 | import { TrytesHelper } from "../../src/utils/trytesHelper"; 5 | 6 | test("search() returns correct nonce for trits with security level 1", () => { 7 | const diver = new HammingDiver(); 8 | const res = diver.search( 9 | TrytesHelper.toTrits("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"), 10 | 1, 11 | 27, 12 | 0); 13 | expect(TrytesHelper.fromTrits(res)).toBe("H9L9SMXRV"); 14 | }); 15 | 16 | test("search() returns correct nonce for trits with security level 2", () => { 17 | const diver = new HammingDiver(); 18 | const res = diver.search( 19 | TrytesHelper.toTrits("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"), 20 | 2, 21 | 27, 22 | 0); 23 | expect(TrytesHelper.fromTrits(res)).toBe("C9L9SMXRV"); 24 | }); 25 | 26 | test("search() returns correct nonce for trits with security level 3", () => { 27 | const diver = new HammingDiver(); 28 | const res = diver.search( 29 | TrytesHelper.toTrits("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"), 30 | 3, 31 | 27, 32 | 0); 33 | expect(TrytesHelper.fromTrits(res)).toBe("DZL9SMYRV"); 34 | }); 35 | -------------------------------------------------------------------------------- /test/utils/mask.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | /* eslint-disable max-len */ 4 | import { Curl } from "@iota/crypto.js"; 5 | import * as crypto from "crypto"; 6 | import { mask, maskHash, unmask } from "../../src/utils/mask"; 7 | import { TrytesHelper } from "../../src/utils/trytesHelper"; 8 | 9 | test("maskHash() returns correct hashed version of trits", () => { 10 | const res = maskHash(TrytesHelper.toTrits("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP")); 11 | expect(TrytesHelper.fromTrits(res)).toBe("KDNHTRIHZXNFJUDUXIBSBOSXNVINMENXQPQTQOCBKCPVOZO99WCMRMMLDASTPYLNCMIR99W9AFLLNUHOR"); 12 | }); 13 | 14 | test("mask() returns same trits with no other trits absorbed", () => { 15 | const curl = new Curl(81); 16 | const res = mask(TrytesHelper.toTrits("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"), curl); 17 | expect(TrytesHelper.fromTrits(res)).toBe("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"); 18 | }); 19 | 20 | test("mask() returns trits with key trits absorbed", () => { 21 | const curl = new Curl(81); 22 | const keyTrits = TrytesHelper.toTrits("A".repeat(81)); 23 | curl.absorb(keyTrits, 0, keyTrits.length); 24 | const res = mask(TrytesHelper.toTrits("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"), curl); 25 | expect(TrytesHelper.fromTrits(res)).toBe("CZO9HNDIEENBMHRRFESCS9OYZSBSFDCIIPLGDVXJFOKIEESNQCRRSZPKNAATFUJKQMMZDYIFNANIOUCMK"); 26 | }); 27 | 28 | test("mask() returns trits with key trits absorbed double payload", () => { 29 | const curl = new Curl(81); 30 | const keyTrits = TrytesHelper.toTrits("A".repeat(81)); 31 | curl.absorb(keyTrits, 0, keyTrits.length); 32 | const res = mask(TrytesHelper.toTrits("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNPABCDEFGRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"), curl); 33 | expect(TrytesHelper.fromTrits(res)).toBe("CZO9HNDIEENBMHRRFESCS9OYZSBSFDCIIPLGDVXJFOKIEESNQCRRSZPKNAATFUJKQMMZDYIFNANIOUCMK9SX9AC9HHQUMLFA9LUVNPAWZLBEOIKHOQSMNGOFCIRJPOVTVJGZCSHFBZMAD9PTYHPXVQJBL9XMJBVSIS"); 34 | }); 35 | 36 | test("mask() returns trits with key trits absorbed non boundary payload", () => { 37 | const curl = new Curl(81); 38 | const keyTrits = TrytesHelper.toTrits("A".repeat(81)); 39 | curl.absorb(keyTrits, 0, keyTrits.length); 40 | const res = mask(TrytesHelper.toTrits("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNPABCDEFGRVVMYNSIIUVHXXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"), curl); 41 | expect(TrytesHelper.fromTrits(res)).toBe("CZO9HNDIEENBMHRRFESCS9OYZSBSFDCIIPLGDVXJFOKIEESNQCRRSZPKNAATFUJKQMMZDYIFNANIOUCMK9SX9AC9HHQUMLFA9LUVNKSKHZEFURWWIOWHPSTVJXOBWGPPHZNK9AWKCLLYQXDGHKQV9RD9"); 42 | }); 43 | 44 | test("unmask() returns same trits with no other trits absorbed", () => { 45 | const curl = new Curl(81); 46 | const res = unmask( 47 | TrytesHelper.toTrits("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"), curl); 48 | expect(TrytesHelper.fromTrits(res)).toBe("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"); 49 | }); 50 | 51 | test("unmask() returns trits with key trits absorbed", () => { 52 | const curl = new Curl(81); 53 | const keyTrits = TrytesHelper.toTrits("A".repeat(81)); 54 | curl.absorb(keyTrits, 0, keyTrits.length); 55 | const res = unmask( 56 | TrytesHelper.toTrits("CZO9HNDIEENBMHRRFESCS9OYZSBSFDCIIPLGDVXJFOKIEESNQCRRSZPKNAATFUJKQMMZDYIFNANIOUCMK"), curl); 57 | expect(TrytesHelper.fromTrits(res)).toBe("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"); 58 | }); 59 | 60 | test("unmask() returns trits with key trits absorbed double payload", () => { 61 | const curl = new Curl(81); 62 | const keyTrits = TrytesHelper.toTrits("A".repeat(81)); 63 | curl.absorb(keyTrits, 0, keyTrits.length); 64 | const res = unmask( 65 | TrytesHelper.toTrits("CZO9HNDIEENBMHRRFESCS9OYZSBSFDCIIPLGDVXJFOKIEESNQCRRSZPKNAATFUJKQMMZDYIFNANIOUCMK9SX9AC9HHQUMLFA9LUVNPAWZLBEOIKHOQSMNGOFCIRJPOVTVJGZCSHFBZMAD9PTYHPXVQJBL9XMJBVSIS"), curl); 66 | expect(TrytesHelper.fromTrits(res)).toBe("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNPABCDEFGRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"); 67 | }); 68 | 69 | test("unmask() returns trits with key trits absorbed non boundary payload", () => { 70 | const curl = new Curl(81); 71 | const keyTrits = TrytesHelper.toTrits("A".repeat(81)); 72 | curl.absorb(keyTrits, 0, keyTrits.length); 73 | const res = unmask( 74 | TrytesHelper.toTrits("CZO9HNDIEENBMHRRFESCS9OYZSBSFDCIIPLGDVXJFOKIEESNQCRRSZPKNAATFUJKQMMZDYIFNANIOUCMK9SX9AC9HHQUMLFA9LUVNKSKHZEFURWWIOWHPSTVJXOBWGPPHZNK9AWKCLLYQXDGHKQV9RD9"), curl); 75 | expect(TrytesHelper.fromTrits(res)).toBe("XAL9SMWRVVMYNSIIUVHXH9LBAHYHUWXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNPABCDEFGRVVMYNSIIUVHXXRRKOTWECQULPRVVHMJXIIHAKPMZZGUFQPJNNAWBRUMZMRLFXNP"); 76 | }); 77 | 78 | test("mask() and unmask() with multiple random trytes", () => { 79 | for (let i = 0; i < 100; i++) { 80 | // tslint:disable-next-line: insecure-random 81 | const val = generateHash(); 82 | const valTrits = TrytesHelper.toTrits(val); 83 | const key = generateHash(); 84 | const keyTrits = TrytesHelper.toTrits(key); 85 | const curl = new Curl(81); 86 | curl.absorb(keyTrits, 0, keyTrits.length); 87 | const masked = mask(valTrits, curl); 88 | 89 | curl.reset(); 90 | curl.absorb(keyTrits, 0, keyTrits.length); 91 | const unmasked = unmask(masked, curl); 92 | expect(TrytesHelper.fromTrits(unmasked)).toBe(val); 93 | } 94 | }); 95 | 96 | /** 97 | * Generate a random hash. 98 | * @param length The length of the hash. 99 | * @returns The hash. 100 | */ 101 | function generateHash(length: number = 81): string { 102 | let hash = ""; 103 | 104 | const randomValues = new Uint32Array(crypto.randomBytes(length)); 105 | 106 | for (let i = 0; i < length; i++) { 107 | hash += TrytesHelper.ALPHABET.charAt(randomValues[i] % 27); 108 | } 109 | 110 | return hash; 111 | } 112 | -------------------------------------------------------------------------------- /test/utils/pascal.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { pascalDecode, pascalEncode } from "../../src/utils/pascal"; 4 | 5 | test("pascalEncode() with 0 returns fixed array", () => { 6 | const res = pascalEncode(0); 7 | expect(Array.from(res)).toEqual([1, 0, 0, -1]); 8 | }); 9 | 10 | test("pascalEncode() with 1234 returns known result", () => { 11 | const res = pascalEncode(1234); 12 | expect(Array.from(res)).toEqual([1, 0, -1, 1, 0, -1, -1, 1, 0, 0, 0, 0]); 13 | }); 14 | 15 | test("pascalDecode() with fixed array returns 0", () => { 16 | const trits = new Int8Array([1, 0, 0, -1]); 17 | const res = pascalDecode(trits); 18 | expect(res.end).toBe(4); 19 | expect(res.value).toBe(0); 20 | }); 21 | 22 | test("pascalDecode() with 1234 array returns known result", () => { 23 | const trits = new Int8Array([1, 0, -1, 1, 0, -1, -1, 1, 0, 0, 0, 0]); 24 | const res = pascalDecode(trits); 25 | expect(res.end).toBe(12); 26 | expect(res.value).toBe(1234); 27 | }); 28 | 29 | test("pascalEncode() and pascalDecode() with multiple random numbers", () => { 30 | for (let i = 0; i < 1000; i++) { 31 | // tslint:disable-next-line: insecure-random 32 | const val = Math.floor(Math.random() * 1000); 33 | const encoded = pascalEncode(val); 34 | const decoded = pascalDecode(encoded); 35 | expect(decoded.end).toBe(encoded.length); 36 | expect(decoded.value).toBe(val); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /test/utils/textHelper.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { TextHelper } from "../../src/utils/textHelper"; 4 | 5 | describe("The textHelper", () => { 6 | test("encodeNonASCII an undefined value will return undefined", () => { 7 | expect(TextHelper.encodeNonASCII(undefined as never)).toEqual(undefined); 8 | }); 9 | 10 | test("encodeNonASCII an null value will return undefined", () => { 11 | expect(TextHelper.encodeNonASCII(null as never)).toEqual(undefined); 12 | }); 13 | 14 | test("encodeNonASCII a false value will return undefined", () => { 15 | expect(TextHelper.encodeNonASCII(false as never)).toEqual(undefined); 16 | }); 17 | 18 | test("encodeNonASCII a true value will return undefined", () => { 19 | expect(TextHelper.encodeNonASCII(true as never)).toEqual(undefined); 20 | }); 21 | 22 | test("encodeNonASCII a 0 value will return undefined", () => { 23 | expect(TextHelper.encodeNonASCII(0 as never)).toEqual(undefined); 24 | }); 25 | 26 | test("encodeNonASCII a 1 value will return undefined", () => { 27 | expect(TextHelper.encodeNonASCII(1 as never)).toEqual(undefined); 28 | }); 29 | 30 | test("encodeNonASCII an object value will return undefined", () => { 31 | expect(TextHelper.encodeNonASCII({ a: 123 } as never)).toEqual(undefined); 32 | }); 33 | 34 | test("encodeNonASCII an array value will return undefined", () => { 35 | expect(TextHelper.encodeNonASCII([1, 2, 3] as never)).toEqual(undefined); 36 | }); 37 | 38 | test("encodeNonASCII a pure ASCII string will the same encoding", () => { 39 | expect(TextHelper.encodeNonASCII("hello")).toEqual("hello"); 40 | }); 41 | 42 | test("encodeNonASCII a non ASCII string will return the encoded version", () => { 43 | expect(TextHelper.encodeNonASCII("Привет, мир")) 44 | .toEqual("\\u041f\\u0440\\u0438\\u0432\\u0435\\u0442, \\u043c\\u0438\\u0440"); 45 | }); 46 | 47 | test("decodeNonASCII an undefined value will return undefined", () => { 48 | expect(TextHelper.decodeNonASCII(undefined as never)).toEqual(undefined); 49 | }); 50 | 51 | test("decodeNonASCII an null value will return undefined", () => { 52 | expect(TextHelper.decodeNonASCII(null as never)).toEqual(undefined); 53 | }); 54 | 55 | test("decodeNonASCII a false value will return undefined", () => { 56 | expect(TextHelper.decodeNonASCII(false as never)).toEqual(undefined); 57 | }); 58 | 59 | test("decodeNonASCII a true value will return undefined", () => { 60 | expect(TextHelper.decodeNonASCII(true as never)).toEqual(undefined); 61 | }); 62 | 63 | test("decodeNonASCII a 0 value will return undefined", () => { 64 | expect(TextHelper.decodeNonASCII(0 as never)).toEqual(undefined); 65 | }); 66 | 67 | test("decodeNonASCII a 1 value will return undefined", () => { 68 | expect(TextHelper.decodeNonASCII(1 as never)).toEqual(undefined); 69 | }); 70 | 71 | test("decodeNonASCII an object value will return undefined", () => { 72 | expect(TextHelper.decodeNonASCII({ a: 123 } as never)).toEqual(undefined); 73 | }); 74 | 75 | test("decodeNonASCII an array value will return undefined", () => { 76 | expect(TextHelper.decodeNonASCII([1, 2, 3] as never)).toEqual(undefined); 77 | }); 78 | 79 | test("decodeNonASCII a pure ASCII string will the same encoding", () => { 80 | expect(TextHelper.decodeNonASCII("hello")).toEqual("hello"); 81 | }); 82 | 83 | test("decodeNonASCII a non ASCII string will return the decoded version", () => { 84 | expect(TextHelper.decodeNonASCII("\\u041f\\u0440\\u0438\\u0432\\u0435\\u0442, \\u043c\\u0438\\u0440")) 85 | .toEqual("Привет, мир"); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/utils/trytesHelper.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 IOTA Stiftung 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { TrytesHelper } from "../../src/utils/trytesHelper"; 4 | 5 | describe("The trytestHelper", () => { 6 | test("objectToTrytes an undefined value will return empty string", () => { 7 | expect(TrytesHelper.objectToTrytes(undefined as never)).toEqual(""); 8 | }); 9 | 10 | test("objectToTrytes an null value will return JSON encoded as trytes", () => { 11 | expect(TrytesHelper.objectToTrytes(null)).toEqual("BDID9D9D"); 12 | }); 13 | 14 | test("objectToTrytes an false boolean value will return JSON encoded as trytes", () => { 15 | expect(TrytesHelper.objectToTrytes(false)).toEqual("UCPC9DGDTC"); 16 | }); 17 | 18 | test("objectToTrytes an true boolean value will return JSON encoded as trytes", () => { 19 | expect(TrytesHelper.objectToTrytes(true)).toEqual("HDFDIDTC"); 20 | }); 21 | 22 | test("objectToTrytes a 0 number value will return JSON encoded as trytes", () => { 23 | expect(TrytesHelper.objectToTrytes(0)).toEqual("UA"); 24 | }); 25 | 26 | test("objectToTrytes a string value will return JSON encoded as trytes", () => { 27 | expect(TrytesHelper.objectToTrytes("hello")).toEqual("GAWCTC9D9DCDGA"); 28 | }); 29 | 30 | test("objectToTrytes a non ASCII string value will return JSON encoded as trytes", () => { 31 | expect(TrytesHelper.objectToTrytes("Привет, мир")) 32 | .toEqual( 33 | // eslint-disable-next-line max-len 34 | "GAKCIDUAYAVAUCKCIDUAYAYAUAKCIDUAYAXABBKCIDUAYAXAWAKCIDUAYAXAZAKCIDUAYAYAWAQAEAKCIDUAYAXARCKCIDUAYAXABBKCIDUAYAYAUAGA" 35 | ); 36 | }); 37 | 38 | test("objectToTrytes an array will return JSON encoded as trytes", () => { 39 | expect(TrytesHelper.objectToTrytes("[1,2,3]")).toEqual("GAJCVAQAWAQAXALCGA"); 40 | }); 41 | 42 | test("objectToTrytes an object value will return JSON encoded as trytes", () => { 43 | expect(TrytesHelper.objectToTrytes("{\"a\":123,\"b\":true}")) 44 | .toEqual("GAODKCGAPCKCGADBVAWAXAQAKCGAQCKCGADBHDFDIDTCQDGA"); 45 | }); 46 | 47 | test("objectFromTrytes an undefined value will throw", () => { 48 | expect(() => TrytesHelper.objectFromTrytes(undefined as never)).toThrow("convert strings"); 49 | }); 50 | 51 | test("objectFromTrytes an empty string value will throw", () => { 52 | expect(() => TrytesHelper.objectFromTrytes("")).toThrow("does not contain any data"); 53 | }); 54 | 55 | test("objectFromTrytes with all 9s trytes will throw", () => { 56 | expect(() => TrytesHelper.objectFromTrytes("99")).toThrow("does not contain any data"); 57 | }); 58 | 59 | test("objectFromTrytes an odd length will throw", () => { 60 | expect(() => TrytesHelper.objectFromTrytes("Z")).toThrow("even"); 61 | }); 62 | 63 | test("objectFromTrytes an null value will be returned when decoding trytes", () => { 64 | expect(TrytesHelper.objectFromTrytes("BDID9D9D")).toEqual(null); 65 | }); 66 | 67 | test("objectFromTrytes an false boolean value will be returned when decoding trytes", () => { 68 | expect(TrytesHelper.objectFromTrytes("UCPC9DGDTC")).toEqual(false); 69 | }); 70 | 71 | test("objectFromTrytes an true boolean value will be returned when decoding trytes", () => { 72 | expect(TrytesHelper.objectFromTrytes("HDFDIDTC")).toEqual(true); 73 | }); 74 | 75 | test("objectFromTrytes a 0 number value will be returned when decoding trytes", () => { 76 | expect(TrytesHelper.objectFromTrytes("UA")).toEqual(0); 77 | }); 78 | 79 | test("objectFromTrytes a string value will be returned when decoding trytes", () => { 80 | expect(TrytesHelper.objectFromTrytes("GAWCTC9D9DCDGA")).toEqual("hello"); 81 | }); 82 | 83 | test("objectFromTrytes a non ASCII string will be returned when decoding trytes", () => { 84 | expect(TrytesHelper.objectFromTrytes( 85 | // eslint-disable-next-line max-len 86 | "GAKCIDUAYAVAUCKCIDUAYAYAUAKCIDUAYAXABBKCIDUAYAXAWAKCIDUAYAXAZAKCIDUAYAYAWAQAEAKCIDUAYAXARCKCIDUAYAXABBKCIDUAYAYAUAGA" 87 | )) 88 | .toEqual("Привет, мир"); 89 | }); 90 | 91 | test("objectFromTrytes an array will be returned when decoding trytes", () => { 92 | expect(TrytesHelper.objectFromTrytes("GAJCVAQAWAQAXALCGA")).toEqual("[1,2,3]"); 93 | }); 94 | 95 | test("objectFromTrytes an object value will be returned when decoding trytes", () => { 96 | expect(TrytesHelper.objectFromTrytes("GAODKCGAPCKCGADBVAWAXAQAKCGAQCKCGADBHDFDIDTCQDGA")) 97 | .toEqual("{\"a\":123,\"b\":true}"); 98 | }); 99 | 100 | test("packTrytes can pack and unpack trytes", () => { 101 | const packed = TrytesHelper.packTrytes("MY9MAM"); 102 | const unpacked = TrytesHelper.unpackTrytes(packed); 103 | expect(unpacked).toEqual("MY9MAM"); 104 | }); 105 | 106 | test("packTrytes can pack and unpack trytes of different length", () => { 107 | for (let i = 0; i < TrytesHelper.ALPHABET.length; i++) { 108 | const trytes = TrytesHelper.ALPHABET.slice(0, i); 109 | const packed = TrytesHelper.packTrytes(trytes); 110 | const unpacked = TrytesHelper.unpackTrytes(packed); 111 | expect(unpacked).toEqual(trytes); 112 | } 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": [ 4 | "./src", 5 | "./test" 6 | ] 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "strict": true, 5 | "module": "es2020", 6 | "moduleResolution": "node", 7 | "importsNotUsedAsValues": "error", 8 | "outDir": "./es", 9 | "inlineSourceMap": true, 10 | "esModuleInterop": true, 11 | "stripInternal": true, 12 | "resolveJsonModule": true, 13 | "declaration": true, 14 | "declarationDir": "typings", 15 | "forceConsistentCasingInFileNames": true, 16 | "importHelpers": true 17 | }, 18 | "include": ["src"] 19 | } 20 | --------------------------------------------------------------------------------