UNPKG

mam.ts

Version:
315 lines (288 loc) 14.8 kB
import { createHttpClient } from '@iota/http-client'; import { keyGen } from './KeyGen'; import { isTrytesOfExactLength, isTrytes } from '@iota/validators'; import { Mam, MamDetails } from './node'; import * as converter from '@iota/converter'; import { Transaction, Transfer, Provider, AttachToTangle } from '@iota/core/typings/types'; import { createPrepareTransfers, createSendTrytes, createFindTransactions, createAttachToTangle } from '@iota/core'; import { hash } from './hash'; import { MAM_MODE, MAM_SECURITY } from './Settings'; import { CreateAttachToTangleWithPwrSvr } from './PwrSrv'; interface channel { side_key : string | null; mode : MAM_MODE; next_root : string | null; security : MAM_SECURITY; //Enum? start : number; count : number; next_count : number; index : number; } /** * The Masked Authenticated Messaging (MAM) Writer class is a simplistic class that allows easy MAM use. * It has an internal state that handles most complicated logic, which is a lot easier compared to other MAM implementations. * A MamReader instance can track the succes of the MamWriter functions. * It is recommended to use createAndAttach as the function handles all logic for the user. * * Masked Authenticated Messaging are 0-value IOTA transaction that contain data messages. * This introduces many possibilities for data integrity and communication, but comes with the caveat that message-only signatures are not checked. * What the IOTA Foundation introduced 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. * This is a wrapper library of the WASM/ASM.js output from the IOTA Bindings repository. * For a more in depth look at how Masked Authenticated Messaging works please check out the Overview. * This wrapper library is based on IOTA Foundations mam.client.js, updated for Typescript and using OOP to ease the use of MAM. */ export class MamWriter { //private provider : Partial<Settings>; private provider : Provider; private channel : channel; private seed : string; private tag : string; private attachFunction : AttachToTangle | undefined; /** * Creates a MamWriter channel for the seed. It defaults to a UNSECURE random seed with minimum security 1 and the Public channel mode. * @param provider The node URL that connects to the IOTA network to send the requests to. * @param seed The seed for the MAM stream, should be kept private. String should contain 81 valid Tryte characters (A-Z+9), otherwise the seed is replaced with a random seed. * To keep building on the same stream, the same seed is required. A random UNSECURE seed is generated if no seed is supplied. * @param security Security level for the stream. Security 1 is a bit unsecure, but fast and recommended for MAM. Security 2 is secure. Security 3 is for accessive security. */ constructor(provider: string, seed : string = keyGen(81), mode : MAM_MODE, sideKey ?: string, security : MAM_SECURITY = MAM_SECURITY.LEVEL_1) { //Set IOTA provider this.provider = createHttpClient( { provider : provider} ); //Check for a valid seed if(!isTrytesOfExactLength(seed, 81)) { console.log('ERROR: Invalid Seed has been submitted. The seed has been replaced with a random seed!'); seed = keyGen(81); } this.seed = seed; this.tag = undefined; this.EnablePowSrv(false); //Set default Attach function //Set the next root this.changeMode(mode, sideKey, security); } /** * Changes the channel mode. The previous stream on other modes do not "carry over". Restricted mode requires a sidekey, otherwise the mode is not changed. * @param mode The new channel mode to set the stream to. * @param sideKey The sidekey for Restricted mode use. Does nothing for Public and Private mode. */ public changeMode(mode : MAM_MODE, sideKey ?: string, security : MAM_SECURITY = MAM_SECURITY.LEVEL_1) : void { if(mode == MAM_MODE.RESTRICTED && sideKey == undefined) { return console.log('You must specify a side key for a restricted channel'); } //Recreate the channel this.channel = { side_key: null, mode: mode, next_root: null, security : security, start: 0, count: 1, next_count: 1, index: 0 }; //Only set sidekey if it isn't undefined (It is allowed to be null, but not undefined) if(sideKey) { this.channel.side_key = converter.asciiToTrytes(sideKey); } //Set new stuff this.channel.mode = mode; this.channel.next_root = Mam.getMamRoot(this.seed, this.channel); } /** * * @param message * @returns The result of the Attach function. */ public async createAndAttach(message : string) { let Result : {payload : string, root : string, address : string} = this.create(message); let Result2 = await this.attach(Result.payload, Result.address); return Result2; } /** * Prepares the message by converting it into a valid payload. It also generates the root and address. * The payload can be attached to the IOTA network later through the Attach function. * It is recommended to use createAndAttach in most cases, unless more direct control is needed or the app runs on an instable internet connection. * @param message The message to add to the MAM stream. Expectes a plaintext string or trinary string, depending on inputTrinary. * @param inputTrinary A boolean that changes the behavior with the message parameter. If true, the message is considerd a trinary string, otherwise a plaintext string. * @returns Returns an object with 3 variables: * Payload: The masked message that can be put on the IOTA network as the next MAM message. * Root: The root of the message, required to find and decode the message with MamReader. * Address: The address were the message will be sent to on the IOTA network. Needed for the Attach function. */ public create(message : string, inputTrinary : boolean = false) : {payload : string, root : string, address : string} { //Interact with MAM Lib let TrytesMsg = message; if(!inputTrinary) { TrytesMsg = converter.asciiToTrytes(message); } //Only send the side_key when MAM_MODE is not Public const mam = Mam.createMessage(this.seed, TrytesMsg, (this.channel.mode != MAM_MODE.PUBLIC)?this.channel.side_key:undefined, this.channel); //If the tree is exhausted this.AdvanceChannel(mam.next_root); //Generate attachment address let address : string; if(this.channel.mode !== MAM_MODE.PUBLIC) { address = hash(mam.root); } else { address = mam.root; } return { //Removed state as it is now updated in the class payload: mam.payload, root: mam.root, address } } /** * Attaches a previously prepared payload to the IOTA network as part of the MAM stream. * @param payload A trinary encoded masked payload created by the create function. * @param address The address where the MAM transaction is sent to. * @param depth The depth that is used for Tip selection by the node. A depth of 3 is recommended. * @param mwm The Proof-of-Work difficulty used. Recommended to use 12 on testnetwork and 14 on the mainnet. (Might be changed later) * @returns An array of transactions that have been send to the network. */ public async attach(payload : string, address : string, depth : number = 3, mwm : number = 14) : Promise<Transaction[]> { return new Promise<Transaction[]> ( (resolve, reject) => { let transfers : Transfer[]; transfers = [ { address : address, value : 0, message : payload, tag : this.tag }]; const sendTrytes : any = createSendTrytes(this.provider, this.attachFunction); const prepareTransfers : any = createPrepareTransfers(); prepareTransfers(this.seed, transfers, {}) .then( (transactionTrytes) => { sendTrytes(transactionTrytes, depth, mwm) .then(transactions => { resolve(<Array<Transaction>>transactions); }) .catch(error => { reject(`sendTrytes failed: ${error}; Try to switch nodes, this one might not support PoW`); }); }) .catch(error => { reject(`failed to attach message: ${error}`); }); }); } /** * Enabled the PowSrv remote PoW service from powsrv.io. With an API key the initial limitations are removed. Ask powsrv for an API key to use this server. * @param enable Boolean value to either enable or disable the service. * @param apiKey powsrv API key, required if you want to enable the service. * @param timeout Timeout for API request to do the PoW in MS. * @param apiServer The server of powsrv, default should be fine unless they move servers. */ public EnablePowSrv(enable : boolean, apiKey ?: string, timeout : number = 3000, apiServer : string = "https://api.powsrv.io:443") { if(enable && apiKey) { this.attachFunction = CreateAttachToTangleWithPwrSvr( apiKey, timeout, apiServer ); } else { //Resets to default Attach function this.attachFunction = createAttachToTangle(this.provider); } } /** * Useful to call after a MamWriter is created and the input seed has been previously used. * This function makes sure that the next message that is added to the MAM stream is appended at the end of the MAM stream. * It is required that the entire MAM stream of this seed + mode is avaliable by the given node. * @returns An array of the previous roots of all messages used in the stream so far. */ public async catchUpThroughNetwork() : Promise<string[]> { return new Promise<string[]> (async (resolve, reject) => { //Set variables let previousRootes : string[] = []; let consumedAll : boolean = false; while(!consumedAll) { //Apply channel mode let address : string = this.channel.next_root; if(this.channel.mode == MAM_MODE.PRIVATE || this.channel.mode == MAM_MODE.RESTRICTED) { address = hash(this.channel.next_root); } const findTransactions = createFindTransactions( this.provider ); await findTransactions({addresses : [address]}) .then((transactionHashes) => { //If no hashes are found, we are at the end of the stream if(transactionHashes.length == 0) { consumedAll = true; } else { //Add the root previousRootes.push(this.channel.next_root); //Find the next root - Straight up stolen from node.ts atm. let next_root_merkle = MamDetails.iota_merkle_create( MamDetails.string_to_ctrits_trits(this.seed), this.channel.start + this.channel.count, this.channel.next_count, this.channel.security ); let next_root = MamDetails.iota_merkle_slice(next_root_merkle); this.AdvanceChannel ( MamDetails.ctrits_trits_to_string(next_root) ); } }) .catch((error) => { reject(`findTransactions failed with ${error}`); }); } resolve(previousRootes); }); } /** * Sets the tag for every mam transaction that will be published afterwards. * The tag can be translated to a maximum of 27 trytes and will be pruned if too long. * @param tag The tag in plaintext. Only accepts trytes. */ public setTag(tag : string | undefined ) { //If statement is too handle undefined as argument if(tag) { //Check for valid Trytes if(isTrytes(tag)) { //Trim to correct length if(tag.length > 27) { console.log("Warning Tag is too long"); tag = tag.slice(0,26); } //Append to correct length tag += "9".repeat(27-tag.length); this.tag = tag; } else { console.log("Warning, tag doesn't consist of trytes"); } } } /** * @returns The root of the next message. Can be used to later retrieve the message with the MamReader. */ public getNextRoot() : string { return Mam.getMamRoot(this.seed, this.channel); } /** * @returns The mode of type MAM_MODE of the currently set channel. */ public getMode() : MAM_MODE { return this.channel.mode; } /** * @returns The seed of the channel. Don't leak this seed as it gives access to your MAM stream! */ public getSeed() : string { return this.seed; } /** * @returns The currently set tag that is posted with new MAM tx's. */ public getTag() : string { return this.tag; } /** * Private function that advanced the merkle tree to the next step for the MAM stream. Sets the channel settings appropriatly. * @param root The root of the next MAM transaction. */ private AdvanceChannel(root : string) { //If the tree is exhausted if(this.channel.index == this.channel.count - 1) { //change start to beginning of next tree. this.channel.start = this.channel.next_count + this.channel.start; //Reset index. this.channel.index = 0; } else { //Else step the tree. this.channel.index++; } //Advance Channel this.channel.next_root = root; } }