├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── docs └── overview.md ├── example ├── api │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── now.json │ ├── package-lock.json │ ├── package.json │ ├── spec │ │ ├── _changelog.md │ │ ├── _example.md │ │ ├── api.json │ │ └── logo.png │ ├── src │ │ ├── data │ │ │ ├── config.dev.json │ │ │ ├── config.local.json │ │ │ └── config.template.json │ │ ├── docs │ │ │ └── index.html │ │ ├── errors │ │ │ └── httpError.ts │ │ ├── factories │ │ │ └── serviceFactory.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── api │ │ │ │ ├── IDataResponse.ts │ │ │ │ ├── IResponse.ts │ │ │ │ └── v0 │ │ │ │ │ ├── IFetchRequest.ts │ │ │ │ │ ├── IFetchResponse.ts │ │ │ │ │ ├── IPublishRequest.ts │ │ │ │ │ └── IPublishResponse.ts │ │ │ ├── app │ │ │ │ └── IRoute.ts │ │ │ └── configuration │ │ │ │ ├── IConfiguration.ts │ │ │ │ └── INodeConfiguration.ts │ │ ├── routes │ │ │ ├── init.ts │ │ │ └── v0 │ │ │ │ ├── fetch.ts │ │ │ │ └── publish.ts │ │ └── utils │ │ │ ├── appHelper.ts │ │ │ ├── textHelper.ts │ │ │ ├── trytesHelper.ts │ │ │ └── validationHelper.ts │ ├── test │ │ ├── docs.http │ │ ├── example.js │ │ ├── fetch.http │ │ ├── publish.http │ │ └── version.http │ ├── tsconfig.json │ └── tslint.json ├── publishAndFetchPublic.html ├── publishAndFetchPublic.js ├── publishAndFetchPublicCustomTag.js └── publishAndFetchRestricted.js ├── lib ├── mam.client.d.ts ├── mam.client.js ├── mam.client.min.js ├── mam.d.ts ├── mam.js ├── mam.js.map └── mam.web.min.js ├── package-lock.json ├── package.json ├── src ├── IOTA.js ├── encryption.js ├── index.js └── node.js ├── webpack.config.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node.js 2 | IOTA.js 3 | webpack.config.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2018 14 | }, 15 | "rules": { 16 | } 17 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | npm-debug.log* 3 | yarn-error.log 4 | 5 | # Dependency directory 6 | node_modules 7 | 8 | .git 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | npm-debug.log* 3 | yarn-error.log 4 | 5 | # Dependency directory 6 | node_modules 7 | 8 | .git 9 | .DS_Store 10 | .history 11 | 12 | example/api/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright (c) 2018 IOTA Foundation 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DISCLAIMER 2 | 3 | > This is a work in progress. The library is usable, however it is still evolving and may have some breaking changes in the future. These will most likely be minor, in addition to extending functionality. 4 | 5 | In the future this library will be a wrapper around the new implementation of MAM [https://github.com/iotaledger/entangled/tree/develop/mam](https://github.com/iotaledger/entangled/tree/develop/mam) 6 | 7 | **There is now a pure JavaScript implementation of MAMv0 which is compatible with this library [https://github.com/iotaledger/mam.js](https://github.com/iotaledger/mam.js)** 8 | 9 | # MAM Client JS Library 10 | 11 | It is possible to publish transactions to the Tangle that contain only messages, with no value. This introduces many possibilities for data integrity and communication, but comes with the caveat that message-only signatures are not checked. What we introduce is a method of symmetric-key encrypted, signed data that takes advantage of merkle-tree winternitz signatures for extended public key usability, that can be found trivially by those who know to look for it. 12 | 13 | This is wrapper library for the WASM/ASM.js output of the [IOTA Bindings repository](https://github.com/iotaledger/iota-bindings). For a more in depth look at how Masked Authenticated Messaging works please check out the [Overview](../master/docs/overview.md) 14 | 15 | ## Getting Started 16 | 17 | Add the package to your project with: 18 | 19 | ```shell 20 | npm install @iota/mam 21 | 22 | or 23 | 24 | yarn add @iota/mam 25 | ``` 26 | 27 | After adding the package it will provide access to the functions described below. To import the module simple use one of the following methods, depending on which version of JavaScript you are using. 28 | 29 | ```javascript 30 | const Mam = require('@iota/mam'); 31 | Mam.init(...); 32 | 33 | or 34 | 35 | import * as Mam from '@iota/mam'; 36 | Mam.init(...); 37 | ``` 38 | 39 | or in the browser using 40 | 41 | ```html 42 | 43 | 46 | ``` 47 | 48 | For a simple user experience you are advised to call the `init()` function to enable to tracking of state in your channels. When calling `init()` you should pass in the `provider` which is the address of an IRI node. This will provide access to some extra functionality including attaching, fetching and subscribing. 49 | 50 | ## Examples 51 | 52 | * [Publishing in Public mode](../master/example/publishAndFetchPublic.js) 53 | * [Publishing in Public mode with Custom Tag](../master/example/publishAndFetchPublicCustomTag.js) 54 | * [Publishing in Restricted mode](../master/example/publishAndFetchRestricted.js) 55 | 56 | * [Publishing in Public mode from Browser](../master/example/publishAndFetchPublic.html) 57 | 58 | ## API 59 | 60 | ### `init` 61 | 62 | This initialises the state. This will return a state object that tracks the progress of your channel and channels you are following 63 | 64 | #### Input 65 | 66 | ```javascript 67 | Mam.init(settings, seed, security) 68 | ``` 69 | 70 | 1. **settings**: `Object` or `String` Configuration object or network provider URL. 71 | Configuration object: 72 | 1. **provider**: `String` Network provider URL. 73 | 2. **attachToTangle** `Function` function to override default `attachToTangle` to use another Node to do the PoW or use a PoW service. 74 | 2. **seed**: `String` Optional tryte-encoded seed. *Null value generates a random seed* 75 | 3. **security**: `Integer` Optional security of the keys used. *Null value defaults to `2`* 76 | 77 | #### Return 78 | 79 | 1. **Object** - Initialised state object to be used in future actions 80 | 81 | ------ 82 | 83 | ### `changeMode` 84 | 85 | This takes the state object and changes the default channel mode from `public` to the specified mode and `sidekey`. There are only three possible modes: `public`, `private`, & `restricted`. If you fail to pass one of these modes it will default to `public`. This will return a state object that tracks the progress of your channel and channels you are following 86 | 87 | #### Input 88 | 89 | ```javascript 90 | Mam.changeMode(state, mode, sidekey) 91 | ``` 92 | 93 | 1. **state**: `Object` Initialised IOTA library with a provider set. 94 | 2. **mode**: `String` Intended channel mode. Can be only: `public`, `private` or `restricted` 95 | 3. **sideKey**: `String` Tryte-encoded encryption key, `81 trytes` long. *Required for restricted mode* 96 | 97 | #### Return 98 | 99 | 1. **Object** - Initialised state object to be used in future actions 100 | 101 | ------ 102 | 103 | ### `getRoot` 104 | 105 | This method will return the root for the supplied mam state. 106 | 107 | #### Input 108 | 109 | ```javascript 110 | Mam.getRoot(state) 111 | ``` 112 | 113 | 1. **state**: `Object` Initialised IOTA library with a provider set. 114 | 115 | #### Return 116 | 117 | 1. **string** - The root calculated from the provided state. 118 | 119 | ------ 120 | 121 | ### `create` 122 | 123 | Creates a MAM message payload from a state object, tryte-encoded message and an optional side key. Returns an updated state and the payload for sending. 124 | 125 | #### Input 126 | 127 | ```javascript 128 | Mam.create(state, message) 129 | ``` 130 | 131 | 1. **state**: `Object` Initialised IOTA library with a provider set. 132 | 2. **message**: `String` Tryte-encoded payload to be encrypted. Tryte-encoded payload can be generated by calling `asciiToTrytes` from the `@iota/converter` and passing a stringified JSON object 133 | 134 | #### Return 135 | 136 | 1. **state**: `Object` Updated state object to be used with future actions. 137 | 2. **payload**: `String` Tryte-encoded payload. 138 | 3. **root**: `String` Tryte-encoded root of the payload. 139 | 4. **address**: `String` Tryte-encoded address used as an location to attach the payload. 140 | 141 | ------ 142 | 143 | ### `decode` 144 | 145 | Enables a user to decode a payload 146 | 147 | #### Input 148 | 149 | ```javascript 150 | Mam.decode(payload, sideKey, root) 151 | ``` 152 | 153 | 1. **payload**: `String` Tryte-encoded payload. 154 | 2. **sideKey**: `String` Tryte-encoded encryption key. *Null value falls back to default key* 155 | 3. **root**: `String` Tryte-encoded string used as the address to attach the payload. 156 | 157 | #### Return 158 | 159 | 1. **state**: `Object` Updated state object to be used with future actions. 160 | 2. **payload**: `String` Tryte-encoded payload. 161 | 3. **root**: `String` Tryte-encoded root used as an address to attach the payload. 162 | 163 | ------ 164 | 165 | ### `subscribe` 166 | 167 | This method will add a subscription to your state object using the provided channel details. 168 | 169 | #### Input 170 | 171 | ```javascript 172 | Mam.subscribe(state, channelRoot, channelMode, channelKey) 173 | ``` 174 | 175 | 1. **state**: `Object` Initialised IOTA library with a provider set. 176 | 2. **channelRoot**: `String` The root of the channel to subscribe to. 177 | 3. **channelMode**: `String` Optional, can one of `public`, `private` or `restricted` *Null value falls back to public* 178 | 4. **channelKey**: `String` Optional, The key of the channel to subscribe to. 179 | 180 | #### Return 181 | 182 | 1. **Object** - Updated state object to be used with future actions. 183 | 184 | ------ 185 | 186 | ### `listen` 187 | 188 | Listen to a channel for new messages. 189 | 190 | #### Input 191 | 192 | ```javascript 193 | Mam.listen(channel, callback) 194 | ``` 195 | 196 | 1. **channel**: `Object` The channel object to listen to. 197 | 2. **callback**: `String` Callback called when new messages arrive. 198 | 199 | #### Return 200 | 201 | Nothing 202 | 203 | ------ 204 | 205 | ### `attach` - async 206 | 207 | Attaches a payload to the Tangle. 208 | 209 | #### Input 210 | 211 | ```javascript 212 | await Mam.attach(payload, address, depth, minWeightMagnitude, tag) 213 | ``` 214 | 215 | 1. **payload**: `String` Tryte-encoded payload to be attached to the Tangle. 216 | 2. **root**: `String` Tryte-encoded string returned from the `Mam.create()` function. 217 | 3. **depth**: `number` Optional depth at which Random Walk starts. A value of 3 is typically used by wallets, meaning that RW starts 3 milestones back. *Null value will set depth to 3* 218 | 4. **minWeightMagnitude**: `number` Optional minimum number of trailing zeros in transaction hash. This is used by `attachToTangle` function to search for a valid nonce. Currently is 14 on mainnet & spamnnet and 9 on most other devnets. *Null value will set minWeightMagnitude to 9* 219 | 5. **tag**: `String` Optional tag of 0-27 trytes. *Null value will set tag to empty string* 220 | 221 | #### Return 222 | 223 | 1. `Array` Transaction objects that have been attached to the network. 224 | 225 | ------ 226 | 227 | ### `fetch` - async 228 | 229 | Fetches the channel sequentially from a known `root` and optional `sidekey`. This call can be used in two ways: **Without a callback** will cause the function to read the entire channel before returning. **With a callback** the application will return data through the callback and finally the `nextroot` when finished. 230 | 231 | #### Input 232 | 233 | ```javascript 234 | await Mam.fetch(root, mode, sidekey, callback, limit) 235 | ``` 236 | 237 | 1. **root**: `String` Tryte-encoded string used as the entry point to a channel. *NOT the address!* 238 | 2. **mode**: `String` Channel mode. Can one of `public`, `private` or `restricted` *Null value falls back to public* 239 | 3. **sideKey**: `String` Tryte-encoded encryption key. *Null value falls back to default key* 240 | 4. **callback**: `Function` Optional callback. *Null value will cause the function to push payload into the messages array.* 241 | 5. **limit**: `Number` Optional limits the number of items returned, defaults to all. 242 | 243 | #### Return 244 | 245 | 1. **nextRoot**: `String` Tryte-encoded string pointing to the next root. 246 | 2. **messages**: `Array` Array of Tryte-encoded messages from the channel. 247 | 248 | ------ 249 | 250 | ### `fetchSingle` - async 251 | 252 | Fetches a single message from a known `root` and optional `sidekey`. 253 | 254 | #### Input 255 | 256 | ```javascript 257 | await Mam.fetchSingle(root, mode, sidekey) 258 | ``` 259 | 260 | 1. **root**: `String` Tryte-encoded string used as the entry point to a channel. *NOT the address!* 261 | 2. **mode**: `String` Channel mode. Can one of `public`, `private` or `restricted` *Null value falls back to public* 262 | 3. **sideKey**: `String` Tryte-encoded encryption key. *Null value falls back to default key* 263 | 264 | #### Return 265 | 266 | 1. **nextRoot**: `String` Tryte-encoded string pointing to the next root. 267 | 2. **payload**: `String` Tryte-encoded messages from the channel. 268 | 269 | ## Building the library 270 | 271 | Compiled libs are included in the repository. 272 | Compiling the Rust bindings can require some complex environmental setup to get to work, so if you are unfamiliar just stick to the compiled files. 273 | 274 | This repo provides wrappers for both Browser and Node environments. The build script discriminates between a WASM.js and ASM.js build methods and returns files that are includable in your project. 275 | 276 | ### CommonJS Module for NodeJS/Browser with Module Loader 277 | 278 | The below commands will build a file called `mam.client.js` in the `lib/` directory. You can then include the pacakge in your code using `require/import`. 279 | 280 | ```shell 281 | // Install dependencies 282 | yarn 283 | // Build a development version lib/mam.client.js 284 | yarn build-node-dev 285 | 286 | // Build a production/minified version lib/mam.client.min.js 287 | yarn build-node-prod 288 | ``` 289 | 290 | ### Browser Script Include 291 | 292 | The below commands will build a file called `mam.web.js` in the `lib/` directory. You can then include the package in your code using ` 11 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /example/publishAndFetchPublic.js: -------------------------------------------------------------------------------- 1 | const Mam = require('../lib/mam.client.js') 2 | const { asciiToTrytes, trytesToAscii } = require('@iota/converter') 3 | 4 | const mode = 'public' 5 | const provider = 'https://nodes.devnet.iota.org' 6 | 7 | const mamExplorerLink = `https://mam-explorer.firebaseapp.com/?provider=${encodeURIComponent(provider)}&mode=${mode}&root=` 8 | 9 | // Initialise MAM State 10 | let mamState = Mam.init(provider) 11 | 12 | // Publish to tangle 13 | const publish = async packet => { 14 | // Create MAM Payload - STRING OF TRYTES 15 | const trytes = asciiToTrytes(JSON.stringify(packet)) 16 | const message = Mam.create(mamState, trytes) 17 | 18 | // Save new mamState 19 | mamState = message.state 20 | 21 | // Attach the payload 22 | await Mam.attach(message.payload, message.address, 3, 9) 23 | 24 | console.log('Published', packet, '\n'); 25 | return message.root 26 | } 27 | 28 | const publishAll = async () => { 29 | const root = await publish({ 30 | message: 'Message from Alice', 31 | timestamp: (new Date()).toLocaleString() 32 | }) 33 | 34 | await publish({ 35 | message: 'Message from Bob', 36 | timestamp: (new Date()).toLocaleString() 37 | }) 38 | 39 | await publish({ 40 | message: 'Message from Charlie', 41 | timestamp: (new Date()).toLocaleString() 42 | }) 43 | 44 | return root 45 | } 46 | 47 | // Callback used to pass data out of the fetch 48 | const logData = data => console.log('Fetched and parsed', JSON.parse(trytesToAscii(data)), '\n') 49 | 50 | publishAll() 51 | .then(async root => { 52 | 53 | // Output asyncronously using "logData" callback function 54 | await Mam.fetch(root, mode, null, logData) 55 | 56 | // Output syncronously once fetch is completed 57 | const result = await Mam.fetch(root, mode) 58 | result.messages.forEach(message => console.log('Fetched and parsed', JSON.parse(trytesToAscii(message)), '\n')) 59 | 60 | console.log(`Verify with MAM Explorer:\n${mamExplorerLink}${root}\n`); 61 | }) 62 | -------------------------------------------------------------------------------- /example/publishAndFetchPublicCustomTag.js: -------------------------------------------------------------------------------- 1 | const Mam = require('../lib/mam.client.js') 2 | const { asciiToTrytes, trytesToAscii } = require('@iota/converter') 3 | 4 | const mode = 'public' 5 | const provider = 'https://nodes.devnet.iota.org' 6 | 7 | const mamExplorerLink = `https://mam-explorer.firebaseapp.com/?provider=${encodeURIComponent(provider)}&mode=${mode}&root=` 8 | 9 | // Initialise MAM State 10 | let mamState = Mam.init(provider) 11 | 12 | // Publish to tangle 13 | const publish = async packet => { 14 | // Create MAM Payload - STRING OF TRYTES 15 | const trytes = asciiToTrytes(JSON.stringify(packet)) 16 | const message = Mam.create(mamState, trytes) 17 | 18 | // Save new mamState 19 | mamState = message.state 20 | 21 | // Attach the payload 22 | await Mam.attach(message.payload, message.address, 3, 9, 'CUSTOMTAG') 23 | 24 | console.log('Published', packet, '\n'); 25 | return message.root 26 | } 27 | 28 | const publishAll = async () => { 29 | const root = await publish({ 30 | message: 'Message from Alice', 31 | timestamp: (new Date()).toLocaleString() 32 | }) 33 | 34 | await publish({ 35 | message: 'Message from Bob', 36 | timestamp: (new Date()).toLocaleString() 37 | }) 38 | 39 | await publish({ 40 | message: 'Message from Charlie', 41 | timestamp: (new Date()).toLocaleString() 42 | }) 43 | 44 | return root 45 | } 46 | 47 | // Callback used to pass data out of the fetch 48 | const logData = data => console.log('Fetched and parsed', JSON.parse(trytesToAscii(data)), '\n') 49 | 50 | publishAll() 51 | .then(async root => { 52 | 53 | // Output asyncronously using "logData" callback function 54 | await Mam.fetch(root, mode, null, logData) 55 | 56 | // Output syncronously once fetch is completed 57 | const result = await Mam.fetch(root, mode) 58 | result.messages.forEach(message => console.log('Fetched and parsed', JSON.parse(trytesToAscii(message)), '\n')) 59 | 60 | console.log(`Verify with MAM Explorer:\n${mamExplorerLink}${root}\n`); 61 | }) 62 | -------------------------------------------------------------------------------- /example/publishAndFetchRestricted.js: -------------------------------------------------------------------------------- 1 | const Mam = require('../lib/mam.client.js') 2 | const { asciiToTrytes, trytesToAscii } = require('@iota/converter') 3 | 4 | const mode = 'restricted' 5 | const secretKey = 'VERYSECRETKEY' 6 | const provider = 'https://nodes.devnet.iota.org' 7 | 8 | const mamExplorerLink = `https://mam-explorer.firebaseapp.com/?provider=${encodeURIComponent(provider)}&mode=${mode}&key=${secretKey.padEnd(81, '9')}&root=` 9 | 10 | // Initialise MAM State 11 | let mamState = Mam.init(provider) 12 | 13 | // Set channel mode 14 | mamState = Mam.changeMode(mamState, mode, secretKey) 15 | 16 | // Publish to tangle 17 | const publish = async packet => { 18 | // Create MAM Payload - STRING OF TRYTES 19 | const trytes = asciiToTrytes(JSON.stringify(packet)) 20 | const message = Mam.create(mamState, trytes) 21 | 22 | // Save new mamState 23 | mamState = message.state 24 | 25 | // Attach the payload 26 | await Mam.attach(message.payload, message.address, 3, 9) 27 | 28 | console.log('Published', packet, '\n'); 29 | return message.root 30 | } 31 | 32 | const publishAll = async () => { 33 | const root = await publish({ 34 | message: 'Message from Alice', 35 | timestamp: (new Date()).toLocaleString() 36 | }) 37 | 38 | await publish({ 39 | message: 'Message from Bob', 40 | timestamp: (new Date()).toLocaleString() 41 | }) 42 | 43 | await publish({ 44 | message: 'Message from Charlie', 45 | timestamp: (new Date()).toLocaleString() 46 | }) 47 | 48 | return root 49 | } 50 | 51 | // Callback used to pass data out of the fetch 52 | const logData = data => console.log('Fetched and parsed', JSON.parse(trytesToAscii(data)), '\n') 53 | 54 | publishAll() 55 | .then(async root => { 56 | 57 | // Output asyncronously using "logData" callback function 58 | await Mam.fetch(root, mode, secretKey, logData) 59 | 60 | // Output syncronously once fetch is completed 61 | const result = await Mam.fetch(root, mode, secretKey) 62 | result.messages.forEach(message => console.log('Fetched and parsed', JSON.parse(trytesToAscii(message)), '\n')) 63 | 64 | console.log(`Verify with MAM Explorer:\n${mamExplorerLink}${root}\n`); 65 | }) 66 | -------------------------------------------------------------------------------- /lib/mam.client.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @iota/mam definition file. 3 | */ 4 | declare module "@iota/mam" { 5 | import { Transaction, AttachToTangle } from "@iota/core"; 6 | 7 | /** 8 | * The mode for the mam channel. 9 | */ 10 | export type MamMode = "public" | "private" | "restricted"; 11 | 12 | /** 13 | * A mam channel. 14 | */ 15 | export interface MamChannel { 16 | side_key: string; 17 | mode: MamMode; 18 | next_root: string; 19 | security: number; 20 | start: number; 21 | count: number; 22 | next_count: number; 23 | index: number; 24 | } 25 | 26 | /** 27 | * A mam subscribed channel. 28 | */ 29 | export interface MamSubscribedChannel { 30 | mode: string; 31 | channelKey: string; 32 | timeout: number; 33 | root: string; 34 | next_root: string; 35 | active: boolean; 36 | } 37 | 38 | /** 39 | * The mam state. 40 | */ 41 | export interface MamState { 42 | subscribed: MamSubscribedChannel[]; 43 | channel: MamChannel; 44 | seed: string; 45 | } 46 | 47 | /** 48 | * The mam settings. 49 | */ 50 | export interface MamSettings { 51 | /** 52 | * The IRI Node provider string. 53 | */ 54 | provider: string; 55 | /** 56 | * Override the attach to tangle method. 57 | */ 58 | attachToTangle?: AttachToTangle 59 | } 60 | 61 | /** 62 | * A mam message 63 | */ 64 | export interface MamMessage { 65 | /** 66 | * The update mam state. 67 | */ 68 | state: MamState; 69 | /** 70 | * The payload to attach. 71 | */ 72 | payload: string; 73 | /** 74 | * The root for the message. 75 | */ 76 | root: string; 77 | /** 78 | * The address for the message. 79 | */ 80 | address: string; 81 | } 82 | 83 | /** 84 | * Initialisation function which returns a state object 85 | * @param settings Either a provider string or an object with provider string and attachToTangle method. 86 | * @param seed The seed to initialise with. 87 | * @param security The security level, defaults to 2. 88 | * @returns State object to be used with future actions. 89 | */ 90 | function init(settings: string | MamSettings, 91 | seed?: string, security?: number): MamState; 92 | 93 | /** 94 | * Add a subscription to your state object 95 | * @param state The mam state. 96 | * @param channelRoot The channel root. 97 | * @param channelMode The channel mode, defaults to public. 98 | * @param channelKey The optional channel key. 99 | * @returns Updated state object to be used with future actions. 100 | */ 101 | function subscribe(state: MamState, channelRoot: string, channelMode?: MamMode, channelKey?: string): MamState; 102 | 103 | /** 104 | * Change the mode of the channel. 105 | * @param state The mam state. 106 | * @param mode The new mode. 107 | * @param sidekey The sideKey required for restricted. 108 | * @returns Updated state object to be used with future actions. 109 | */ 110 | function changeMode(state: MamState, mode: MamMode, sidekey?: string): MamState; 111 | 112 | /** 113 | * Create a message to use on the mam channel. 114 | * @param state The mam state. 115 | * @param message The Tryte encoded message. 116 | * @returns An object containing the payload and updated state. 117 | */ 118 | function create(state: MamState, message: string): MamMessage; 119 | 120 | /** 121 | * Decode a message. 122 | * @param payload The payload of the message. 123 | * @param sideKey The sideKey used in the message. 124 | * @param root The root used for the message. 125 | * @returns The decoded payload. 126 | */ 127 | function decode(payload: string, sideKey: string, root: string): string; 128 | 129 | /** 130 | * Fetch the messages asynchronously. 131 | * @param root The root key to use. 132 | * @param mode The mode of the channel. 133 | * @param sideKey The sideKey used in the messages, only required for restricted. 134 | * @param callback Optional callback to receive each payload. 135 | * @param limit Limit the number of messages that are fetched. 136 | * @returns The nextRoot and the messages if no callback was supplied, or an Error. 137 | */ 138 | function fetch(root: string, mode: MamMode, sideKey?: string, callback?: (payload: string) => void, limit?: number): Promise<{ 139 | /** 140 | * The root for the next message. 141 | */ 142 | nextRoot: string; 143 | /** 144 | * All the message payloads. 145 | */ 146 | messages?: string[]; 147 | } | Error>; 148 | 149 | /** 150 | * Fetch a single message asynchronously. 151 | * @param root The root key to use. 152 | * @param mode The mode of the channel. 153 | * @param sideKey The sideKey used in the messages. 154 | * @returns The nextRoot and the payload, or an Error. 155 | */ 156 | function fetchSingle(root: string, mode: MamMode, sideKey?: string): Promise<{ 157 | /** 158 | * The root for the next message. 159 | */ 160 | nextRoot: string; 161 | /** 162 | * The payload for the message. 163 | */ 164 | payload?: string; 165 | } | Error>; 166 | 167 | /** 168 | * Attach the mam trytes to the tangle. 169 | * @param trytes The trytes to attach. 170 | * @param root The root to attach them to. 171 | * @param depth The depth to attach them with, defaults to 3. 172 | * @param mwm The minimum weight magnitude to attach with, defaults to 9 for devnet, 14 required for mainnet. 173 | * @param tag Trytes to tag the message with. 174 | * @returns The transaction objects. 175 | */ 176 | function attach(trytes: string, root: string, depth?: number, mwm?: number, tag?: string): Promise>; 177 | 178 | /** 179 | * Listen for new message on the channel. 180 | * @param channel The channel to listen on. 181 | * @param callback The callback to receive any messages, 182 | */ 183 | function listen(channel: MamSubscribedChannel, callback: (messages: string[]) => void): void; 184 | 185 | /** 186 | * Get the root from the mam state. 187 | * @param state The mam state. 188 | * @returns The root. 189 | */ 190 | function getRoot(state: MamState): string; 191 | 192 | /** 193 | * Set the provider. 194 | * @param provider The IOTA provider to use. 195 | */ 196 | function setIOTA(provider?: string): void; 197 | 198 | /** 199 | * Set the attachToTangle. 200 | * @param attachToTangle The attach to tangle method to use. 201 | */ 202 | function setAttachToTangle(attachToTangle?: AttachToTangle): void; 203 | 204 | export { 205 | init, 206 | subscribe, 207 | changeMode, 208 | create, 209 | decode, 210 | fetch, 211 | fetchSingle, 212 | attach, 213 | listen, 214 | getRoot, 215 | setIOTA, 216 | setAttachToTangle 217 | }; 218 | } 219 | -------------------------------------------------------------------------------- /lib/mam.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export class NativeContext { 3 | private _wasm; 4 | constructor(_wasm: WebAssembly.ResultObject); 5 | fns(): any; 6 | } 7 | export interface Native { 8 | native(): any; 9 | discard(): void; 10 | } 11 | export class Seed implements Native { 12 | private _ctx; 13 | private _seed; 14 | private _security; 15 | private _ptr?; 16 | constructor(_ctx: NativeContext, _seed: string, _security: number); 17 | seed(): string; 18 | security(): number; 19 | native(): any; 20 | discard(): void; 21 | } 22 | export class IndexedSeed implements Native { 23 | private _seed; 24 | private _index; 25 | constructor(_seed: Seed, _index: number); 26 | seed(): Seed; 27 | index(): number; 28 | security(): number; 29 | next(): IndexedSeed; 30 | native(): any; 31 | discard(): void; 32 | } 33 | export class MerkleTree implements Native { 34 | private _ctx; 35 | private _seed; 36 | private _size; 37 | private _root?; 38 | private _ptr?; 39 | constructor(_ctx: NativeContext, _seed: IndexedSeed, _size: number); 40 | seed(): IndexedSeed; 41 | root(): string; 42 | size(): number; 43 | branch(index: number): MerkleBranch; 44 | native(): any; 45 | discard(): void; 46 | } 47 | export class MerkleBranch implements Native { 48 | private _ctx; 49 | private _tree; 50 | private _index; 51 | private _siblings; 52 | private _native; 53 | constructor(_ctx: NativeContext, _tree: MerkleTree, _index: number, _siblings: Array, _native: any); 54 | tree(): MerkleTree; 55 | index(): number; 56 | siblings(): Array; 57 | native(): any; 58 | discard(): void; 59 | } 60 | export class EncodedMessage { 61 | payload: string; 62 | sideKey: string; 63 | tree: MerkleTree; 64 | nextTree: MerkleTree; 65 | constructor(payload: string, sideKey: string, tree: MerkleTree, nextTree: MerkleTree); 66 | } 67 | export enum Error { 68 | None = 0, 69 | InvalidHash = 1, 70 | InvalidSignature = 2, 71 | ArrayOutOfBounds = 3, 72 | TreeDepleted = 4, 73 | InvalidSideKeyLength = 5 74 | } 75 | export enum Mode { 76 | Public = 0, 77 | Old = 1, 78 | Private = 2, 79 | Restricted = 3 80 | } 81 | export function getIDForMode(mode: Mode, root: string, sideKey?: string): string; 82 | export class Channel { 83 | private _ctx; 84 | private _mode; 85 | private _currentTree; 86 | private _nextTree; 87 | private _currentIndex; 88 | constructor(_ctx: NativeContext, _mode: Mode, _currentTree: MerkleTree, _nextTree: MerkleTree); 89 | mode(): Mode; 90 | id(sideKey?: string): string; 91 | transition(next: MerkleTree): Channel; 92 | encode(message: string, sideKey?: string): EncodedMessage | Error; 93 | } 94 | export class DecodedMessage { 95 | payload: string; 96 | nextRoot: string; 97 | constructor(payload: string, nextRoot: string); 98 | } 99 | export type MaybeMessage = DecodedMessage | Error; 100 | export function decodeMessage(ctx: NativeContext, root: string, payload: string, sideKey?: string): MaybeMessage; 101 | 102 | import 'fast-text-encoding'; 103 | import { NativeContext } from './bindings'; 104 | export function assertHash(s: string): void; 105 | export function stringToCTrits(ctx: NativeContext, str: string): any; 106 | export function ctritsToString(ctx: NativeContext, ct: any): string; 107 | export function padKey(key: any): string; 108 | 109 | import 'idempotent-babel-polyfill'; 110 | export * from './bindings'; 111 | export * from './helpers'; 112 | export * from './wrapper'; 113 | import { NativeContext } from './bindings'; 114 | export function createContext(opts?: any): Promise; 115 | 116 | import { Provider } from '@iota/core'; 117 | import { MaybeMessage, NativeContext, Mode } from './bindings'; 118 | export class ReadResult { 119 | } 120 | export class ReadCandidate { 121 | tail: string; 122 | message: MaybeMessage; 123 | constructor(tail: string, message: MaybeMessage); 124 | } 125 | export class Reader implements AsyncIterator { 126 | private _ctx; 127 | private _provider; 128 | private _mode; 129 | private _root; 130 | private _sideKey; 131 | constructor(_ctx: NativeContext, _provider: Provider, _mode: Mode, _root: string, _sideKey?: string); 132 | listenAddress(): string; 133 | changeRoot(nextRoot: string): void; 134 | next(arg?: any): Promise>; 135 | } 136 | 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iota/mam", 3 | "version": "0.7.3", 4 | "description": "Masked Authentication Messaging wrapper for Javascript (Browser and Node)", 5 | "main": "lib/mam.client.js", 6 | "typings": "lib/mam.client.d.ts", 7 | "scripts": { 8 | "clean": "rimraf ./lib/mam.client.js ./lib/mam.client.min.js ./lib/mam.web.js ./lib/mam.web.min.js", 9 | "lint": "eslint ./src", 10 | "build-node-dev": "webpack --target node --mode development", 11 | "build-node-prod": "webpack --target node --mode production", 12 | "build-web-dev": "webpack --target web --mode development", 13 | "build-web-prod": "webpack --target web --mode production", 14 | "dist": "run-s clean lint build-node-dev build-node-prod build-web-prod" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/iotaledger/mam.client.js.git" 19 | }, 20 | "keywords": [ 21 | "IOTA", 22 | "MAM", 23 | "Masked Authentication Messaging" 24 | ], 25 | "author": "IOTA Foundation (https://iota.org)", 26 | "license": "Apache-2.0", 27 | "bugs": { 28 | "url": "https://github.com/iotaledger/mam.client.js/issues" 29 | }, 30 | "homepage": "https://iota.org", 31 | "engines": { 32 | "node": ">=6" 33 | }, 34 | "dependencies": { 35 | "@iota/converter": "^1.0.0-beta.12", 36 | "@iota/core": "^1.0.0-beta.12", 37 | "@iota/curl": "^1.0.0-beta.12" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.4.5", 41 | "@babel/preset-env": "^7.4.5", 42 | "babel-loader": "^8.0.6", 43 | "encoding": "^0.1.12", 44 | "eslint": "^5.16.0", 45 | "npm-run-all": "^4.1.5", 46 | "rimraf": "^2.6.3", 47 | "webpack": "^4.32.2", 48 | "webpack-cli": "^3.3.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/encryption.js: -------------------------------------------------------------------------------- 1 | const Curl = require('@iota/curl').default 2 | 3 | function hash(rounds, ...keys) { 4 | const curl = new Curl(rounds) 5 | const key = new Int8Array(Curl.HASH_LENGTH) 6 | curl.initialize() 7 | keys.map(k => curl.absorb(k, 0, Curl.HASH_LENGTH)) 8 | curl.squeeze(key, 0, Curl.HASH_LENGTH) 9 | return key 10 | } 11 | 12 | module.exports = { 13 | hash 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const Encryption = require('./encryption') 3 | const converter = require('@iota/converter') 4 | const { composeAPI } = require('@iota/core') 5 | const { createHttpClient } = require('@iota/http-client') 6 | const { createContext, Reader, Mode } = require('../lib/mam') 7 | 8 | // Setup Provider 9 | let provider = null; 10 | let attachToTangle = null; 11 | let Mam = {} 12 | 13 | /** 14 | * Initialisation function which returns a state object 15 | * @param {object || string} settings object or external provider 16 | * @param {string} [settings.provider] - Http `uri` of IRI node 17 | * @param {Provider} [settings.network] - Network provider to override with 18 | * @param {function} [settings.attachToTangle] - AttachToTangle function to override with 19 | * @param {string} seed 20 | * @param {integer} security 21 | * @returns {object} State object to be used with future actions. 22 | */ 23 | const init = (settings, seed = keyGen(81), security = 2) => { 24 | if (typeof settings === 'object') { 25 | // Set IOTA provider 26 | provider = settings.provider 27 | 28 | if (settings.attachToTangle) { 29 | // Set alternative attachToTangle function 30 | attachToTangle = settings.attachToTangle 31 | } 32 | } else { 33 | // Set IOTA provider 34 | provider = settings 35 | } 36 | 37 | // Setup Personal Channel 38 | const channel = { 39 | side_key: null, 40 | mode: 'public', 41 | next_root: null, 42 | security, 43 | start: 0, 44 | count: 1, 45 | next_count: 1, 46 | index: 0 47 | } 48 | 49 | return { 50 | subscribed: [], 51 | channel, 52 | seed 53 | } 54 | } 55 | /** 56 | * Add a subscription to your state object 57 | * @param {object} state The state object to add the subscription to. 58 | * @param {string} channelRoot The root of the channel to subscribe to. 59 | * @param {string} channelMode Can be `public`, `private` or `restricted`. 60 | * @param {string} channelKey Optional, the key of the channel to subscribe to. 61 | * @returns {object} Updated state object to be used with future actions. 62 | */ 63 | const subscribe = (state, channelRoot, channelMode = "public", channelKey = null) => { 64 | state.subscribed[channelRoot] = { 65 | channelKey, 66 | mode: channelMode, 67 | timeout: 5000, 68 | root: channelRoot, 69 | next_root: null, 70 | active: true 71 | } 72 | return state 73 | } 74 | 75 | /** 76 | * Change the mode for the mam state 77 | * @param {object} state 78 | * @param {string} mode [public/private/restricted] 79 | * @param {string} sidekey, required for restricted mode 80 | * @returns {object} Updated state object to be used with future actions. 81 | */ 82 | const changeMode = (state, mode, sidekey = null) => { 83 | if (mode !== 'public' && mode !== 'private' && mode !== 'restricted') { 84 | throw new Error('The mode parameter should be public, private or restricted') 85 | } 86 | if (mode === 'restricted' && !sidekey) { 87 | throw new Error('You must specify a side key for restricted mode'); 88 | } 89 | if (sidekey) { 90 | state.channel.side_key = typeof sidekey === 'string' ? sidekey.padEnd(81, '9') : sidekey 91 | } 92 | state.channel.mode = mode 93 | return state 94 | } 95 | 96 | /** 97 | * Creates a MAM message payload from a state object. 98 | * @param {object} state The current mam state. 99 | * @param {string} message Tryte encoded string. 100 | * @returns {object} Updated state object to be used with future actions. 101 | */ 102 | const create = (state, message) => { 103 | const channel = state.channel 104 | // Interact with MAM Lib 105 | const mam = Mam.createMessage(state.seed, message, channel.side_key, channel) 106 | 107 | // If the tree is exhausted. 108 | if (channel.index === channel.count - 1) { 109 | // change start to begining of next tree. 110 | channel.start = channel.next_count + channel.start 111 | // Reset index. 112 | channel.index = 0 113 | } else { 114 | // Else step the tree. 115 | channel.index++ 116 | } 117 | 118 | // Advance Channel 119 | channel.next_root = mam.next_root 120 | state.channel = channel 121 | 122 | // Generate attachement address 123 | let address 124 | if (channel.mode !== 'public') { 125 | address = converter.trytes( 126 | Encryption.hash(81, converter.trits(mam.root.slice())) 127 | ) 128 | } else { 129 | address = mam.root 130 | } 131 | 132 | return { 133 | state, 134 | payload: mam.payload, 135 | root: mam.root, 136 | address 137 | } 138 | } 139 | 140 | /** 141 | * Get the root from the mam state. 142 | * @param {object} state The mam state. 143 | * @returns {string} The root. 144 | */ 145 | const getRoot = state => Mam.getMamRoot(state.seed, state.channel) 146 | 147 | /** 148 | * Enables a user to decode a payload 149 | * @param {string} payload Tryte-encoded payload. 150 | * @param {string} sidekey Tryte-encoded encryption key. Null value falls back to default key 151 | * @param {string} root Tryte-encoded string used as the address to attach the payload. 152 | */ 153 | const decode = (payload, sidekey, root) => { 154 | const key = typeof sidekey === 'string' ? sidekey.padEnd(81, '9') : sidekey 155 | return Mam.decodeMessage(payload, key, root) 156 | } 157 | 158 | /** 159 | * Fetches the channel sequentially from a known `root` and optional `sidekey` 160 | * @param {string} root Tryte-encoded string used as the entry point to a channel. 161 | * @param {string} selectedMode Can one of `public`, `private` or `restricted` 162 | * @param {string} sidekey Tryte-encoded encryption key for restricted mode 163 | * @param {function} callback Optional callback for each payload retrieved 164 | * @param {number} limit Optional, limits the number of items returned 165 | * @returns {object} List of messages and the next root. 166 | */ 167 | const fetch = async (root, selectedMode, sidekey, callback, limit) => { 168 | let client = createHttpClient({ provider, attachToTangle }) 169 | let ctx = await createContext() 170 | const messages = [] 171 | const mode = selectedMode === 'public' ? Mode.Public : Mode.Old 172 | let hasMessage = false 173 | let nextRoot = root 174 | let localLimit = limit || Math.pow(2, 53) - 1 175 | 176 | try { 177 | do { 178 | let reader = new Reader(ctx, client, mode, nextRoot, sidekey || '') 179 | const message = await reader.next() 180 | hasMessage = message && message.value && message.value[0] 181 | if (hasMessage) { 182 | nextRoot = message.value[0].message.nextRoot 183 | const payload = message.value[0].message.payload 184 | 185 | // Push payload into the messages array 186 | messages.push(payload) 187 | 188 | // Call callback function if provided 189 | if (callback) { 190 | callback(payload) 191 | } 192 | } 193 | } while (!!hasMessage && messages.length < localLimit) 194 | return { messages, nextRoot } 195 | } catch (e) { 196 | return e 197 | } 198 | } 199 | 200 | /** 201 | * Fetches a single message from a known `root` and optional `sidekey` 202 | * @param {string} root Tryte-encoded string used as the entry point to a channel. 203 | * @param {string} selectedMode Can one of `public`, `private` or `restricted` 204 | * @param {string} sidekey Tryte-encoded encryption key for restricted mode 205 | * @returns {object} The payload and the next root. 206 | */ 207 | const fetchSingle = async (root, selectedMode, sidekey) => { 208 | const response = await fetch(root, selectedMode, sidekey, undefined, 1) 209 | return response && response.nextRoot ? { 210 | payload: response.messages && response.messages.length === 1 ? response.messages[0] : undefined, 211 | nextRoot: response.nextRoot 212 | } : response 213 | } 214 | 215 | /** 216 | * Listen to a channel for new messages. 217 | * @param {Object} channel The channel object to listen to. 218 | * @param {Function} callback Callback called when new messages arrive. 219 | */ 220 | const listen = (channel, callback) => { 221 | let root = channel.root 222 | return setTimeout(async () => { 223 | let resp = await fetch(root, channel.mode, channel.channelKey) 224 | root = resp.nextRoot 225 | callback(resp.messages) 226 | }, channel.timeout) 227 | } 228 | 229 | /** 230 | * Attaches a payload to the Tangle. 231 | * @param {string} trytes Tryte-encoded payload to be attached to the Tangle. 232 | * @param {string} root Tryte-encoded string returned from the `Mam.create()` function. 233 | * @param {number} depth Optional depth at which Random Walk starts, defaults to 3. 234 | * @param {number} mwm Optional minimum number of trailing zeros in transaction hash, defaults to 9. 235 | * @param {string} tag Tag to use when attaching transactions. 236 | * @returns {array} Transaction objects that have been attached to the network. 237 | */ 238 | const attach = async (trytes, root, depth = 3, mwm = 9, tag = '') => { 239 | const transfers = [ 240 | { 241 | address: root, 242 | value: 0, 243 | message: trytes, 244 | tag: tag 245 | } 246 | ] 247 | try { 248 | const { prepareTransfers, sendTrytes } = composeAPI({ provider, attachToTangle }) 249 | 250 | const trytes = await prepareTransfers('9'.repeat(81), transfers, {}) 251 | 252 | return sendTrytes(trytes, depth, mwm) 253 | } catch (e) { 254 | throw `failed to attach message: ${e}` 255 | } 256 | } 257 | 258 | const keyGen = length => { 259 | const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ9' 260 | let key = '' 261 | while (key.length < length) { 262 | let byte = crypto.randomBytes(1) 263 | if (byte[0] < 243) { 264 | key += charset.charAt(byte[0] % 27) 265 | } 266 | } 267 | return key 268 | } 269 | 270 | const setupEnv = rustBindings => (Mam = rustBindings) 271 | 272 | const setIOTA = (externalProvider = null) => (provider = externalProvider) 273 | 274 | const setAttachToTangle = (externalAttachToTangle = null) => (attachToTangle = externalAttachToTangle) 275 | 276 | module.exports = { 277 | init, 278 | subscribe, 279 | changeMode, 280 | create, 281 | decode, 282 | fetch, 283 | fetchSingle, 284 | attach, 285 | listen, 286 | getRoot, 287 | setIOTA, 288 | setAttachToTangle, 289 | setupEnv 290 | } 291 | -------------------------------------------------------------------------------- /src/node.js: -------------------------------------------------------------------------------- 1 | const IOTA = require('./IOTA.js') 2 | const Main = require('./index.js') 3 | ////////////////////////////////////////////////////////////////// 4 | /* ======= CTrits bindings ======= */ 5 | const TritEncoding = { 6 | BYTE: 1, 7 | TRIT: 2, 8 | TRYTE: 3 9 | } 10 | 11 | /* ======= Rust bindings ======= */ 12 | 13 | const iota_ctrits_drop = IOTA.cwrap('iota_ctrits_drop', '', ['number']) 14 | const iota_ctrits_convert = IOTA.cwrap('iota_ctrits_convert', 'number', [ 15 | 'number', 16 | 'number' 17 | ]) 18 | const iota_ctrits_ctrits_from_trytes = IOTA.cwrap( 19 | 'iota_ctrits_ctrits_from_trytes', 20 | 'number', 21 | ['string', 'number'] 22 | ) 23 | const iota_ctrits_ctrits_from_bytes = IOTA.cwrap( 24 | 'iota_ctrits_ctrits_from_bytes', 25 | 'number', 26 | ['number', 'number'] 27 | ) 28 | const iota_ctrits_ctrits_from_trits = IOTA.cwrap( 29 | 'iota_ctrits_ctrits_from_trits', 30 | 'number', 31 | ['number', 'number'] 32 | ) 33 | 34 | // For accessing the struct members 35 | const iota_ctrits_ctrits_encoding = IOTA.cwrap( 36 | 'iota_ctrits_ctrits_encoding', 37 | 'number', 38 | ['number'] 39 | ) 40 | const iota_ctrits_ctrits_length = IOTA.cwrap( 41 | 'iota_ctrits_ctrits_length', 42 | 'number', 43 | ['number'] 44 | ) 45 | const iota_ctrits_ctrits_data = IOTA.cwrap( 46 | 'iota_ctrits_ctrits_data', 47 | 'number', 48 | ['number'] 49 | ) 50 | const iota_ctrits_ctrits_byte_length = IOTA.cwrap( 51 | 'iota_ctrits_ctrits_byte_length', 52 | 'number', 53 | ['number'] 54 | ) 55 | 56 | const iota_mam_id = IOTA.cwrap('iota_mam_id', 'number', [ 57 | 'number', 58 | 'number' 59 | ]) 60 | 61 | // (seed, message, key, root, siblings, next_root, start, index, security) -> encoded_message 62 | const iota_mam_create = IOTA.cwrap('iota_mam_create', 'number', [ 63 | 'number', 64 | 'number', 65 | 'number', 66 | 'number', 67 | 'number', 68 | 'number', 69 | 'number', 70 | 'number', 71 | 'number' 72 | ]) 73 | // (encoded_message, key, root, index) -> message 74 | const iota_mam_parse = IOTA.cwrap('iota_mam_parse', 'number', [ 75 | 'number', 76 | 'number', 77 | 'number', 78 | 'number' 79 | ]) 80 | 81 | // (seed, index, count, securit) -> MerkleTree instance 82 | const iota_merkle_create = IOTA.cwrap('iota_merkle_create', 'number', [ 83 | 'number', 84 | 'number', 85 | 'number', 86 | 'number' 87 | ]) 88 | // (MerkleTree instance) -> () 89 | const iota_merkle_drop = IOTA.cwrap('iota_merkle_drop', '', ['number']) 90 | // (MerkleTree instance) -> (siblings as number) 91 | const iota_merkle_siblings = IOTA.cwrap('iota_merkle_siblings', 'number', [ 92 | 'number' 93 | ]) 94 | // (MerkleTree instance, index) -> (MerkleBranch instance) 95 | const iota_merkle_branch = IOTA.cwrap('iota_merkle_branch', 'number', [ 96 | 'number', 97 | 'number' 98 | ]) 99 | // (MerkleBranch instance) -> () 100 | const iota_merkle_branch_drop = IOTA.cwrap('iota_merkle_branch_drop', '', [ 101 | 'number' 102 | ]) 103 | // (MerkleBranch instance) -> (number) 104 | const iota_merkle_branch_len = IOTA.cwrap('iota_merkle_branch_len', '', [ 105 | 'number' 106 | ]) 107 | // (address, siblings, index) -> (root as number) 108 | const iota_merkle_root = IOTA.cwrap('iota_merkle_root', 'number', [ 109 | 'number', 110 | 'number', 111 | 'number' 112 | ]) 113 | // (MerkleTree instance) -> root hash 114 | const iota_merkle_slice = IOTA.cwrap('iota_merkle_slice', 'number', ['number']) 115 | 116 | const string_to_ctrits_trits = str => { 117 | let strin = iota_ctrits_ctrits_from_trytes(str, str.length) 118 | let out = iota_ctrits_convert(strin, TritEncoding.TRIT) 119 | iota_ctrits_drop(strin) 120 | return out 121 | } 122 | 123 | const ctrits_trits_to_string = ctrits => { 124 | let str_trits = iota_ctrits_convert(ctrits, TritEncoding.TRYTE) 125 | let ptr = iota_ctrits_ctrits_data(str_trits) 126 | let len = iota_ctrits_ctrits_length(str_trits) 127 | let out = IOTA.Pointer_stringify(ptr, len) 128 | iota_ctrits_drop(str_trits) 129 | return out 130 | } 131 | 132 | const getMamRoot = (SEED, CHANNEL) => { 133 | let SEED_trits = string_to_ctrits_trits(SEED) 134 | let root_merkle = iota_merkle_create( 135 | SEED_trits, 136 | CHANNEL.start, 137 | CHANNEL.count, 138 | CHANNEL.security 139 | ) 140 | return ctrits_trits_to_string(iota_merkle_slice(root_merkle)) 141 | } 142 | 143 | const getMamAddress = (KEY, ROOT) => { 144 | let KEY_trits = string_to_ctrits_trits(KEY) 145 | let ROOT_trits = string_to_ctrits_trits(ROOT) 146 | let address = iota_mam_id( 147 | KEY_trits, 148 | ROOT_trits 149 | ) 150 | return ctrits_trits_to_string(address) 151 | } 152 | 153 | const createMessage = (SEED, MESSAGE, SIDE_KEY, CHANNEL) => { 154 | if (!SIDE_KEY) 155 | SIDE_KEY = '999999999999999999999999999999999999999999999999999999999999999999999999999999999' 156 | 157 | // MAM settings 158 | let SEED_trits = string_to_ctrits_trits(SEED) 159 | let MESSAGE_trits = string_to_ctrits_trits(MESSAGE) 160 | let SIDE_KEY_trits = string_to_ctrits_trits(SIDE_KEY) 161 | 162 | const SECURITY = CHANNEL.security 163 | const START = CHANNEL.start 164 | const COUNT = CHANNEL.count 165 | const NEXT_START = START + COUNT 166 | const NEXT_COUNT = CHANNEL.next_count 167 | const INDEX = CHANNEL.index 168 | const HASH_LENGTH = 81 169 | 170 | // set up merkle tree 171 | let root_merkle = iota_merkle_create(SEED_trits, START, COUNT, SECURITY) 172 | let next_root_merkle = iota_merkle_create( 173 | SEED_trits, 174 | NEXT_START, 175 | NEXT_COUNT, 176 | SECURITY 177 | ) 178 | 179 | let root_branch = iota_merkle_branch(root_merkle, INDEX) 180 | let root_siblings = iota_merkle_siblings(root_branch) 181 | 182 | let next_root_branch = iota_merkle_branch(next_root_merkle, INDEX) 183 | 184 | let root = iota_merkle_slice(root_merkle) 185 | let next_root = iota_merkle_slice(next_root_merkle) 186 | 187 | let masked_payload = iota_mam_create( 188 | SEED_trits, 189 | MESSAGE_trits, 190 | SIDE_KEY_trits, 191 | root, 192 | root_siblings, 193 | next_root, 194 | START, 195 | INDEX, 196 | SECURITY 197 | ) 198 | 199 | const response = { 200 | payload: ctrits_trits_to_string(masked_payload), 201 | root: ctrits_trits_to_string(root), 202 | next_root: ctrits_trits_to_string(next_root), 203 | side_key: SIDE_KEY 204 | } 205 | // Clean up memory. Unneccessary for this example script, but should be done when running in a production 206 | // environment. 207 | iota_merkle_branch_drop(root_branch) 208 | iota_merkle_branch_drop(next_root_branch) 209 | iota_merkle_drop(root_merkle) 210 | iota_merkle_drop(next_root_merkle) 211 | ;[ 212 | SEED_trits, 213 | MESSAGE_trits, 214 | SIDE_KEY_trits, 215 | root, 216 | next_root, 217 | masked_payload, 218 | root_siblings 219 | ].forEach(iota_ctrits_drop) 220 | return response 221 | } 222 | 223 | const decodeMessage = (PAYLOAD, SIDE_KEY, ROOT) => { 224 | if (!SIDE_KEY) 225 | SIDE_KEY = '999999999999999999999999999999999999999999999999999999999999999999999999999999999' 226 | 227 | let PAYLOAD_trits = string_to_ctrits_trits(PAYLOAD) 228 | let SIDE_KEY_trits = string_to_ctrits_trits(SIDE_KEY) 229 | let ROOT_trits = string_to_ctrits_trits(ROOT) 230 | 231 | let parse_result = iota_mam_parse(PAYLOAD_trits, SIDE_KEY_trits, ROOT_trits) 232 | let unmasked_payload_ctrits = IOTA.getValue(parse_result, 'i32') 233 | let unmasked_payload = ctrits_trits_to_string(unmasked_payload_ctrits) 234 | 235 | let unmasked_next_root_ctrits = IOTA.getValue(parse_result + 4, 'i32') 236 | let unmasked_next_root = ctrits_trits_to_string(unmasked_next_root_ctrits) 237 | ;[ 238 | PAYLOAD_trits, 239 | SIDE_KEY_trits, 240 | ROOT_trits, 241 | unmasked_payload_ctrits, 242 | unmasked_next_root_ctrits 243 | ].forEach(iota_ctrits_drop) 244 | IOTA._free(parse_result) 245 | return { payload: unmasked_payload, next_root: unmasked_next_root } 246 | } 247 | 248 | const Mam = { 249 | decodeMessage, 250 | createMessage, 251 | getMamAddress, 252 | getMamRoot 253 | } 254 | 255 | // Feed Mam functions into the main file 256 | Main.setupEnv(Mam) 257 | 258 | // Export 259 | module.exports = Main 260 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (env, options) => ({ 2 | entry: __dirname + '/src/node.js', 3 | output: { 4 | path: __dirname + '/lib', 5 | filename: `mam${options.target === 'node' ? '.client' : '.web'}${options.mode === 'development' ? '' : '.min'}.js`, 6 | library: 'Mam', 7 | libraryTarget: 'umd', 8 | umdNamedDefine: true 9 | }, 10 | mode: options.mode, 11 | target: options.target, 12 | devtool: 'none', 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | presets: [ 22 | [ 23 | '@babel/preset-env', 24 | { 25 | targets: { 26 | browsers: '> 5%', 27 | node: '6' 28 | } 29 | } 30 | ] 31 | ] 32 | } 33 | } 34 | } 35 | ] 36 | }, 37 | node: { 38 | fs: 'empty', 39 | child_process: 'empty', 40 | path: 'empty' 41 | } 42 | }) 43 | --------------------------------------------------------------------------------