@tangle-js/anchors
Version:
Anchoring messages to the Tangle. Powered by IOTA Streams
310 lines • 20.1 kB
JavaScript
import { AnchoringChannelError } from "./errors/anchoringChannelError.mjs";
import { AnchoringChannelErrorNames } from "./errors/anchoringChannelErrorNames.mjs";
import { ClientHelper } from "./helpers/clientHelper.mjs";
import { SeedHelper } from "./helpers/seedHelper.mjs";
import ValidationHelper from "./helpers/validationHelper.mjs";
import AnchorMsgService from "./services/anchorMsgService.mjs";
import ChannelService from "./services/channelService.mjs";
import FetchMsgService from "./services/fetchMsgService.mjs";
export class IotaAnchoringChannel {
constructor(channelID, nodeInfo, isPrivate, encrypted) {
this._node = nodeInfo;
this._channelID = channelID;
const components = channelID.split(":");
this._channelAddress = components[0];
this._announceMsgID = components[1];
if (isPrivate) {
this._keyLoadMsgID = components[2];
}
this._encrypted = encrypted;
this._isPrivate = isPrivate;
}
/**
* Returns the channelID ('channelAddress:announce_msg_id')
*
* @returns channel ID
*/
get channelID() {
return this._channelID;
}
/**
* Returns the channel's address
*
* @returns channel address
*/
get channelAddr() {
return this._channelAddress;
}
/**
* Returns the channel's first anchorage ID
*
* @returns anchorageID
*/
get firstAnchorageID() {
let result = this._keyLoadMsgID;
if (!result) {
result = this._announceMsgID;
}
return result;
}
/**
* Returns the channel's node
*
* @returns node
*/
get node() {
return this._node.node;
}
/**
* Returns the channel's publisher seed
*
* @returns seed
*/
get seed() {
return this._seed;
}
/**
* Returns the channel's author Public Key
*
* @returns the Author's Public key
*/
get authorPubKey() {
return this._authorPubKey;
}
/**
* Returns the channel's subscriber Public Key
*
* @returns the subscriber's Public key
*/
get subscriberPubKey() {
return this._subscriberPubKey;
}
/**
* Returns whether the channel is encrypted or not
*
* @returns boolean
*/
get encrypted() {
return this._encrypted;
}
/**
* Returns whether the channel is private or not
*
* @returns boolean
*/
get isPrivate() {
return this._isPrivate;
}
/**
* Creates a new Anchoring Channel
*
* @param seed Author's seed
* @param options The options
* @param options.node The node used to create the channel
* @returns The anchoring channel details
*/
static async create(seed, options) {
if (options?.node && !ValidationHelper.url(options?.node)) {
throw new AnchoringChannelError(AnchoringChannelErrorNames.INVALID_NODE, "The node has to be a URL");
}
const node = options?.node;
const permanode = options?.permanode;
let encrypted = false;
let isPrivate = false;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
if (options?.encrypted === true) {
encrypted = true;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
if (options?.isPrivate === true) {
isPrivate = true;
}
if (!isPrivate && options?.presharedKeys) {
throw new AnchoringChannelError(AnchoringChannelErrorNames.CHANNEL_BINDING_ERROR, "Pre-shared keys are only for Private Channels");
}
const client = await this.getClient(node, permanode);
const { channelAddress, announceMsgID, keyLoadMsgID, authorPk } = await ChannelService.createChannel(client, seed, isPrivate, options?.presharedKeys);
let firstAnchorageID = announceMsgID;
if (keyLoadMsgID) {
firstAnchorageID = keyLoadMsgID;
}
const details = {
channelAddr: channelAddress,
channelID: `${channelAddress}:${announceMsgID}${keyLoadMsgID ? `:${keyLoadMsgID}` : ""}`,
firstAnchorageID,
authorPubKey: authorPk,
authorSeed: seed,
node: node || this.DEFAULT_NODE,
encrypted,
isPrivate
};
return details;
}
/**
* Instantiates an existing Anchoring Channel from a Channel ID
*
* @param channelID in the form of 'channel_address:announce_msg_id'
* @param options Channel options
* @returns reference to the channel
*/
static fromID(channelID, options) {
const components = channelID.split(":");
let encrypted = false;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
if (options?.encrypted === true) {
encrypted = true;
}
let isPrivate = false;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
if (options?.isPrivate === true) {
isPrivate = true;
}
if (Array.isArray(components) &&
((components.length === 2 && !isPrivate) || (components.length === 3 && isPrivate))) {
let node = options?.node;
const permanode = options?.permanode;
if (!node) {
node = this.DEFAULT_NODE;
}
return new IotaAnchoringChannel(channelID, { node, permanode }, isPrivate, encrypted);
}
throw new AnchoringChannelError(AnchoringChannelErrorNames.CHANNEL_BINDING_ERROR, `Invalid channel identifier: ${channelID}`);
}
/**
* Creates a new IotaAnchoringChannel and subscribes to it using the Author's seed
*
* i.e. Author === Subscriber
* A new Seed is automatically generated
*
* @param options The channel creation options
* @returns The Anchoring Channel
*/
static async bindNew(options) {
const details = await IotaAnchoringChannel.create(SeedHelper.generateSeed(), options);
let opts = options;
if (!opts) {
opts = {};
}
return IotaAnchoringChannel.fromID(details.channelID, opts).bind(details.authorSeed);
}
static async getClient(node, permanode) {
let client;
if (!node && !permanode) {
client = await ClientHelper.getMainnetClient();
}
else if (!node) {
client = await ClientHelper.getClient(ClientHelper.DEFAULT_NODE, permanode);
}
else {
client = await ClientHelper.getClient(node, permanode);
}
return client;
}
/**
* Binds the channel so that the subscriber is instantiated using the seed passed as parameter
*
* @param seed The Subscriber (publisher) seed
* @param psk The Subscriber preshared key
* @returns a Reference to the channel
*/
async bind(seed, psk) {
if (this._subscriber) {
throw new AnchoringChannelError(AnchoringChannelErrorNames.CHANNEL_ALREADY_BOUND, `Channel already bound to ${this._channelID}`);
}
this._seed = seed;
const client = await IotaAnchoringChannel.getClient(this._node.node, this._node.permanode);
const bindRequest = {
client,
seed: this._seed,
isPrivate: this._isPrivate,
presharedKey: psk,
encrypted: this._encrypted,
channelID: this._channelID
};
// The author's PK for the time being is not set because cannot be obtained from the
// announce message
const { subscriber, authorPk } = await ChannelService.bindToChannel(bindRequest);
this._authorPubKey = authorPk;
this._subscriber = subscriber;
this._subscriberPubKey = subscriber.get_public_key();
return this;
}
/**
* Anchors a message to the anchoring channel
*
* @param message Message to be anchored
* @param anchorageID The anchorage to be used
* @returns The result of the operation
*/
async anchor(message, anchorageID) {
if (!this._subscriber) {
throw new AnchoringChannelError(AnchoringChannelErrorNames.CHANNEL_NOT_BOUND, "Unbound anchoring channel. Please call bind first");
}
const request = {
channelID: this._channelID,
encrypted: this._encrypted,
isPrivate: this._isPrivate,
subscriber: this._subscriber,
message,
anchorageID
};
const result = await AnchorMsgService.anchor(request);
return result;
}
/**
* Fetches a previously anchored message
*
* @param anchorageID The anchorage point
* @param messageID The expected ID of the anchored message
* @returns The fetch result
*/
async fetch(anchorageID, messageID) {
if (!this._subscriber) {
throw new AnchoringChannelError(AnchoringChannelErrorNames.CHANNEL_NOT_BOUND, "Unbound anchoring channel. Please call bind first");
}
const request = {
channelID: this._channelID,
encrypted: this._encrypted,
isPrivate: this._isPrivate,
subscriber: this._subscriber,
msgID: messageID,
anchorageID
};
return FetchMsgService.fetch(request);
}
/**
* Fetches the next message anchored to the channel
*
* @returns The fetch result or undefined if no more messages can be fetched
*/
async fetchNext() {
if (!this._subscriber) {
throw new AnchoringChannelError(AnchoringChannelErrorNames.CHANNEL_NOT_BOUND, "Unbound anchoring channel. Please call bind first");
}
return FetchMsgService.fetchNext(this._subscriber, this._encrypted);
}
/**
* Receives a previously anchored message
* provided its anchorage has already been seen on the channel
*
* @param messageID The ID of the message
* @param anchorageID The expected ID of message's anchorage
* @returns The message received and associated metadata
*/
async receive(messageID, anchorageID) {
if (!this._subscriber) {
throw new AnchoringChannelError(AnchoringChannelErrorNames.CHANNEL_NOT_BOUND, "Unbound anchoring channel. Please call bind first");
}
const request = {
channelID: this._channelID,
encrypted: this._encrypted,
isPrivate: this._isPrivate,
subscriber: this._subscriber,
msgID: messageID,
anchorageID
};
return FetchMsgService.receive(request);
}
}
IotaAnchoringChannel.DEFAULT_NODE = ClientHelper.DEFAULT_NODE;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW90YUFuY2hvcmluZ0NoYW5uZWwubWpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2lvdGFBbmNob3JpbmdDaGFubmVsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBQ3ZFLE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLHFDQUFxQyxDQUFDO0FBQ2pGLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUN0RCxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDbEQsT0FBTyxnQkFBZ0IsTUFBTSw0QkFBNEIsQ0FBQztBQVUxRCxPQUFPLGdCQUFnQixNQUFNLDZCQUE2QixDQUFDO0FBQzNELE9BQU8sY0FBYyxNQUFNLDJCQUEyQixDQUFDO0FBQ3ZELE9BQU8sZUFBZSxNQUFNLDRCQUE0QixDQUFDO0FBRXpELE1BQU0sT0FBTyxvQkFBb0I7SUF5QjdCLFlBQW9CLFNBQWlCLEVBQUUsUUFBbUIsRUFBRSxTQUFrQixFQUFFLFNBQWtCO1FBQzlGLElBQUksQ0FBQyxLQUFLLEdBQUcsUUFBUSxDQUFDO1FBRXRCLElBQUksQ0FBQyxVQUFVLEdBQUcsU0FBUyxDQUFDO1FBRTVCLE1BQU0sVUFBVSxHQUFHLFNBQVMsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7UUFFeEMsSUFBSSxDQUFDLGVBQWUsR0FBRyxVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDckMsSUFBSSxDQUFDLGNBQWMsR0FBRyxVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFFcEMsSUFBSSxTQUFTLEVBQUU7WUFDWCxJQUFJLENBQUMsYUFBYSxHQUFHLFVBQVUsQ0FBQyxDQUFDLENBQUMsQ0FBQztTQUN0QztRQUVELElBQUksQ0FBQyxVQUFVLEdBQUcsU0FBUyxDQUFDO1FBQzVCLElBQUksQ0FBQyxVQUFVLEdBQUcsU0FBUyxDQUFDO0lB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