UNPKG

cosmic-lib

Version:

A JavaScript implementation of the CosmicLink protocol for Stellar

831 lines (736 loc) 29.4 kB
"use strict"; var _regeneratorRuntime = require("@babel/runtime/regenerator"); var _asyncToGenerator = require("@babel/runtime/helpers/asyncToGenerator"); var _classCallCheck = require("@babel/runtime/helpers/classCallCheck"); var _createClass = require("@babel/runtime/helpers/createClass"); var env = require("@cosmic-plus/jsutils/es5/env"); var misc = require("@cosmic-plus/jsutils/es5/misc"); var html = env.isBrowser && require("@cosmic-plus/domutils/es5/html"); var SideFrame = env.isBrowser && require("./helpers/side-frame"); var action = require("./action"); var config = require("./config"); var convert = require("./convert"); var format = env.isBrowser && require("./format"); var parse = require("./parse"); var resolve = require("./resolve"); var sep7Utils = require("./sep7-utils"); var status = require("./status"); /** * | Formats | Data | Actions | Editor | HTML * |---------------------------------------------|-------------------------------------|------------------------------------------------|----------------------------------------------|---------------------------------------- * |---------------------|---------------------|---------------------|---------------------|--------------------- * | [uri]{@link CosmicLink#uri} |[page]{@link CosmicLink#page} |[open]{@link CosmicLink#open} |[parse]{@link CosmicLink#parse} |[htmlDescription]{@link CosmicLink#htmlDescription} * | [query]{@link CosmicLink#query} |[network]{@link CosmicLink#network} |[lock]{@link CosmicLink#lock} async |[setTxFields]{@link CosmicLink#setTxFields} |[htmlLink]{@link CosmicLink#htmlLink} * | [tdesc]{@link CosmicLink#tdesc} |[horizon]{@link CosmicLink#horizon} |[sign]{@link CosmicLink#sign} |[addOperation]{@link CosmicLink#addOperation} | * | [json]{@link CosmicLink#json} |[callback]{@link CosmicLink#callback}|[send]{@link CosmicLink#send} async |[setOperation]{@link CosmicLink#setOperation} * | [transaction]{@link CosmicLink#transaction} |[source]{@link CosmicLink#source} |[signSep7]{@link CosmicLink#signSep7} |[insertOperation]{@link CosmicLink#insertOperation} * | [xdr]{@link CosmicLink#xdr} |[status]{@link CosmicLink#status} |[verifySep7]{@link CosmicLink#verifySep7} async * | [sep7]{@link CosmicLink#sep7} |[errors]{@link CosmicLink#errors} | * | |[locker]{@link CosmicLink#locker} * | |[cache]{@link CosmicLink#cache} * | |[extra]{@link CosmicLink#extra} * ----- * * The **CosmicLink** class represents Stellar * [transactions]{@link https://stellar.org/developers/guides/concepts/transactions.html} * encoded in various formats. It allows to convert between those formats, to * edit the underlying transaction, to build it, to sign it and to send it to * the blockchain. * * There are 3 main formats from which the other are derived: * * * The StellarSdk [Transaction]{@link {@link https://stellar.github.io/js-stellar-sdk/Transaction.html}} object. (**transaction**) * * The CosmicLink, which is a transaction encoded as a query. (**query**) * * The Tdesc, which is an internal JSON-compatible format in-between those two. * It is the easier format to work with. (**tdesc**) * * Those formats can be derived into other related formats: * * * The XDR, which's a base64 representation of StellarSdk Transaction. (**xdr**) * * The Sep-0007 link, in its XDR form. (**sep7**) * * The CosmicLink URL/URI, which is a page plus the query. (**uri**) * * The Tdesc JSON, which is its stringified version. (**json**) * * A CosmicLink object can be created from any of those formats. Some of the * other formats are immediately available, while others may need an * `await cosmicLink.lock()` operation to become computable: * * * If you create a CosmicLink from an **uri**, a **query**, a **tdesc** or a * **json**, only those 4 formats are available at first. Transaction, xdr & * sep7 will become available after a `cosmicLink.lock()`. (**free formats**) * * If you create a CosmicLink from a **transaction**, an **xdr** or a **sep7**, * all formats will immediately be available. (**locked formats**) * * For a better efficiency, formats are lazy-evaluated. This means that they are * computed once only if/when you call them: * * ```js * const cosmicLink = new CosmicLink(xdr, { network: 'test' }) * console.log(cosmicLink.query) * ``` * * The role of `cosmicLink.lock()` is centric to this class. In practice, the * free formats don't have to be tied to a **network**, a **source** or a * **sequence number**. For example, the CosmicQuery `?inflation` is a valid * generic transaction that can be locked to any network/source/sequence * combination. * * On the other hand, locked formats are always tied to a particular combination * of those, hence the need for a **lock** command: * * ```js * const cosmicLib = require('cosmic-lib') * cosmicLib.network = 'test' * cosmicLib.source = 'tips*cosmic.link' * * const cosmicLink = new cosmicLib.CosmicLink('?inflation') * * console.log(cosmicLink.tdesc.source) // => undefined * console.log(cosmicLink.tdesc.network) // => undefined * console.log(cosmicLink.tdesc.sequence) // => undefined * console.log(cosmicLink.xdr) // => undefined * * await cosmicLink.lock({) * * console.log(cosmicLink.tdesc.source) // => 'GC6Z...2JVW' * console.log(cosmicLink.tdesc.network) // => 'test' * console.log(cosmicLink.tdesc.sequence) // => 29...3903 * console.log(cosmicLink.xdr) // => 'AAAA....AA==' * ``` * * The **lock** command is asynchronous because free formats accept * [federated addresses]{@link https://stellar.org/developers/guides/concepts/federation.html}, * but locked formats don't. The library automatically resolve * those and this is an asynchronous operation. At the same time, it downloads * the required data from the blockchain to handle multi-signers transactions. * * After the lock operation, all free formats are updated according to the new * state of the transaction. It is now possible to `cosmicLink.sign(keypair)` * it, and to `cosmicLink.send()` it to the blockchain. */ var CosmicLink = /*#__PURE__*/function () { /** * Create a new CosmicLink object. **transaction** can be one of the accepted * format: uri, query, json, tdesc, transaction, xdr or sep7. * * @constructor * @param {string|Object|Transaction} transaction A transaction in one of * thoses formats: uri, query, json, tdesc, transaction, xdr, sep7 * @param {Object} options Additional options * @param {string} options.page The base URI to use when converting transaction * to URI format. * @param {string} options.network For Transaction/XDR formats, the network for * which it have been created * @param {string} options.strip Remove an element from the original * XDR transaction. Valid values are `source`, `sequence` and * `signatures`. Stripping out sequence means that the transaction request * can get signed anytime in the future, possibly several times. * Stripping out source means that it can get signed by any account. * @param {boolean} options.stripNeutralAccount If set, strip source account * out of SEP-0007/XDR requests when it is equal to the neutral account * (`GAAA...AWHF`). * @param {boolean} options.stripNeutralSequence If set, strip sequence out * of SEP-0007/XDR requests when it is equal to `0`. * @return {CosmicLink} */ function CosmicLink(transaction, options) { _classCallCheck(this, CosmicLink); initCosmicLink(this, transaction, options); } /** * Refer to the underlying global configuration * @private */ _createClass(CosmicLink, [{ key: "parse", /** * Re-parse this CosmicLink. Useful in implementing transaction editors. The * parameters are the same than [Constructor]{@link CosmicLink#Constructor}, * and the result is similar except that no new CosmicLink object is created. */ value: function parse(transaction, options) { initCosmicLink(this, transaction, options); } /// Formats /** * A CosmicLink is a URI that embed a Cosmic [Query]{@link CosmicLink#query}. * This format is simply the `cosmicLink.query` appended to the * `cosmicLink.page` */ }, { key: "setTxFields", /// Editor /** * Add/remove transaction fields and reparse the CosmicLink. **object** should * follow the Tdesc format, but fields values can be written using query or * StellarSdk format as well. * * @example * cosmicLink.setTxFields({ minTime: '2018-10', maxTime: '2019-01' }) * * @example * cosmicLink.setTxFields({ minTime: null, maxTime: null }) * * @example * cosmicLink.setTxFields({ memo: 'Bonjour!' }) * * @param {Object} object Transaction fields definition. Fields can be either * written using the JSON format or the query format * @return {CosmicLink} */ value: function setTxFields(object) { checkLock(this); this.parse(Object.assign(this.tdesc, object)); return this; } /** * Add a new operation to CosmicLink. **params** should follow the Tdesc format, * but fields values can be written using query or StellarSdk format as well. * * @example * cosmicLink.addOperation('changeTrust', { asset: 'CNY:admin*ripplefox' }) * * @example * cosmicLink.addOperation('changeTrust', { asset: { code: 'CNY', issuer: 'admin*ripplefox } }) * * @example * cosmicLink.addOperation('changeTrust', { asset: new StellarSdk.Asset('CNY', ...) }) * * @param {string} type The operation type. * @param {Object} params The operation parameters. * @return {CosmicLink} */ }, { key: "addOperation", value: function addOperation(type, params) { checkLock(this); var odesc = Object.assign({ type: type }, params); this.tdesc.operations.push(odesc); this.parse(this.tdesc); return this; } /** * Insert an operation at **index**. **params** should follow the * Tdesc format, but fields can be written using query or StellarSdk format * as well. * * @example * cosmicLink.insertOperation(0, 'changeTrust', { * asset: 'CNY:admin*ripplefox' * }) * * @param {integer} index The operation index. * @param {string} type The operation type. * @param {params} params The operation parameters. * @return {CosmicLink} */ }, { key: "insertOperation", value: function insertOperation(index, type, params) { checkLock(this); if (index > !this.tdesc.operations.length) { throw new Error("Can't insert opereration at position ".concat(index, ": there are only ").concat(this.tdesc.operations.length, " operations")); } var odesc = Object.assign({ type: type }, params); this.tdesc.operations.splice(index, 0, odesc); this.parse(this.tdesc); return this; } /** * Set/remove one of the CosmicLink operations. **params** should follow the * Tdesc format, but fields can be written using query or StellarSdk format * as well. If **type** is set to `null`, the operation at **index** * is deleted. * * @example * cosmicLink.setOperation(1, 'setOptions', { homeDomain: 'example.org' }) * * @example * cosmicLink.setOperation(2, null) * * @param {integer} index The operation index. * @param {string} type The operation type. * @param {params} params The operation parameters. * @return {CosmicLink} */ }, { key: "setOperation", value: function setOperation(index, type, params) { checkLock(this); if (!this.tdesc.operations[index]) { throw new Error("Operation ".concat(index, " doesn't exists")); } if (type === null) { this.tdesc.operations.splice(index, 1); } else { this.tdesc.operations[index] = Object.assign({ type: type }, params); this.parse(this.tdesc); } return this; } /// Actions /** * Select the network that this CosmicLink uses. * * @deprecated StellarSdk global `Network` setting is deprecated. */ }, { key: "selectNetwork", value: function selectNetwork() { return resolve.useNetwork(this); } }, { key: "lock", value: function lock(options) { return action.lock(this, options); } }, { key: "sign", value: function sign() { for (var _len = arguments.length, keypairs_or_preimage = new Array(_len), _key = 0; _key < _len; _key++) { keypairs_or_preimage[_key] = arguments[_key]; } return action.sign.apply(action, [this].concat(keypairs_or_preimage)); } }, { key: "send", value: function send(horizon) { return action.send(this, horizon); } /** * The HTML DOM node that displays a description of the current transaction. * This is a browser-only property. * * If your HTML page contains an element with `id="cosmiclink_description"`, * it will automatically get populated with the description of the last * CosmicLink created. */ }, { key: "open", /** * Open CosmicLink in **target**. * * - `frame` (default): Open cosmicLink in a side-frame. * - `tab`: Open cosmicLink in a new tab. * - `current`: Open cosmicLink into the current window. * - `sep7`: Open cosmicLink using user's SEP-0007 handler. * * @param {String} [target="frame"] Open `cosmicLink` into the requested * target. Valid targets are: `frame`, `tab`, `current` and `sep7`. */ value: function open() { var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "frame"; if (env.isNode) { console.error("Warning: cosmicLink.open() is not supported in Node.js environment."); return; } if (this.status) throw new Error(this.status); switch (target) { case "frame": return new SideFrame(this.uri); case "tab": window.open(this.uri); break; case "current": location.href = this.uri; break; case "sep7": if (!this.sep7) { throw new Error("Please use cosmicLink.lock() to build SEP-0007 link."); } else { location.href = this.sep7; } break; default: throw new Error("Invalid cosmicLink.open() target: ".concat(target)); } } /** * Sign SEP-0007 link for **domain**, using **keypair**. * * @see [SEP-0007 request signing](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md#request-signing) * * @param {String} domain The domain or subdomain you want to sign the request * for. (example: "a-domain.org") * @param {Keypair} keypair A StellarSdk Keypair. */ }, { key: "signSep7", value: function signSep7(domain, keypair) { if (!this.locker) throw new Error("cosmicLink is not locked."); sep7Utils.signLink(this, domain, keypair); } /** * Verify SEP-0007 signature by resolving [`cosmicLink.extra.domain`]{@link * CosmicLink#extra}, if any. * Throw an error if the signature is not valid. * * @return {undefined|String} The resolved `cosmicLink.extra.domain`, if any. */ }, { key: "verifySep7", value: function () { var _verifySep = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() { var domain; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: if (!(this.extra.originDomain instanceof Promise)) { _context.next = 7; break; } _context.next = 3; return this.extra.originDomain; case 3: domain = _context.sent; this.extra.originDomain = domain; _context.next = 8; break; case 7: if (this.extra.originDomain) { sep7Utils.verifySignature(this, this.extra.originDomain); } case 8: return _context.abrupt("return", this.extra.originDomain); case 9: case "end": return _context.stop(); } } }, _callee, this); })); function verifySep7() { return _verifySep.apply(this, arguments); } return verifySep7; }() }, { key: "config", get: function get() { return this.__proto__.__proto__; } }, { key: "uri", get: function get() { if (this.query) return this.page + this.query;else return undefined; } /** * CosmicLink's transaction encoded in the Cosmic * [Query]{@link tutorial:specs_query} format. This format allows to * conveniently pass around Stellar transactions over any URI. */ }, { key: "query", get: function get() { if (!this._query) { if (this.xdr) this._query = convert.xdrToQuery(this, this.xdr, this.tdesc);else if (this.tdesc) this._query = convert.tdescToQuery(this, this.tdesc);else return undefined; } return this._query; } /** * CosmicLink's transaction in Tdesc format. This is in-between an objectified * query representation and a simplified StellarSdk Transaction object. It has * been created to be convenient to understand, use and manipulate. * * If you need to read the transaction parameters, this is the format of * choice: * * ```js * console.log(cosmicLink.tdesc.network) // Does the transaction enforce a network? * console.log(cosmicLink.tdesc.source) // Does the transaction enforce a source? * console.log(cosmicLink.tdesc.memo) // A simplified memo object or undefined * console.log(cosmicLink.operations) // Transaction operations in simplified format * ``` * * This formats authorize [federated addresses]{@link https://stellar.org/developers/guides/concepts/federation.html} * everywhere StellarSdk Transaction accept public keys. Those addresses are * resolved when running the [lock]{@link CosmicLink#lock} method, and the * tdesc is replaced by a resolved one. * * Tdesc is also very convenient to edit. To keep the CosmicLink in sync, you * either need to [parse]{@link CosmicLink#parse} the edited tdesc, or to edit * it using the dedicated methods: * * * [setTxFields]{@link CosmicLink#setTxFields}: set/clear transaction fields * * [addOperation]{@link CosmicLink#addOperation}: add a new operation * * [setOperation]{@link CosmicLink#setOperation}: edit/clear an operation */ }, { key: "tdesc", get: function get() { if (!this._tdesc) { if (this.transaction) this._tdesc = convert.transactionToTdesc(this, this.transaction, this.locker);else return undefined; } return this._tdesc; } /** * CosmicLink's transaction in JSON format. This is a stringified version of * [Tdesc]{@link CosmicLink#tdesc} format. */ }, { key: "json", get: function get() { if (!this._json) this._json = convert.tdescToJson(this, this.tdesc); return this._json; } /** * CosmicLink's transaction in StellarSdk * [Transaction]{@link https://stellar.github.io/js-stellar-sdk/Transaction.html} * format. * * If you created the CosmicLink from uri, query, tdesc or json format, a * [lock()]{@link CosmicLink#lock} operation is needed to make this format * available. */ }, { key: "transaction", get: function get() { return this._transaction; } /** * CosmicLink's transaction in * [XDR]{@link https://stellar.org/developers/guides/concepts/xdr.html} * format. * * If you created the CosmicLink from uri, query, tdesc or json format, a * [lock()]{@link CosmicLink#lock} operation is needed to make this format * available. */ }, { key: "xdr", get: function get() { if (!this._xdr) { if (!this.transaction) return undefined; this._xdr = convert.transactionToXdr(this, this.transaction); } return this._xdr; } /** * CosmicLink transaction in * [SEP-0007]{@link https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0007.md} * link format. Only the XDR part of this protocol is currently supported by * CosmicLink, minus the signature verification. * * If you created the CosmicLink from uri, query, tdesc or json format, a * [lock()]{@link CosmicLink#lock} operation is needed to make this format * available. */ }, { key: "sep7", get: function get() { if (!this._sep7) { if (!this.xdr) return undefined; this._sep7 = convert.xdrToSep7(this, this.xdr, this.tdesc); } return this._sep7; } /// Data /** * The page this CosmicLink uses to construct its [URI]{@link CosmicLink#uri}. * * @var CosmicLink#page */ /** * The source for this transaction. This can be defined either locally * (`cosmicLink.tdesc.source`) or globally (`cosmicLib.config.source`). The * local configuration takes precedence, or, in other words, the global source * is a fallback value in case the transaction emitter doesn't set one. * * **Note:** cosmicLink.tdesc should be edited using [setTxFields]{@link * CosmicLink#setTxFields}. */ }, { key: "source", get: function get() { return this.tdesc && this.tdesc.source || this.config.source; } /** * The network for this transaction. This can be defined either locally * (`cosmicLink.tdesc.network`) or globally (`cosmicLib.config.network`). The * local configuration takes precedence, or, in other words, the global * network is a fallback value in case the transaction emitter doesn't set * one. * * **Note:** cosmicLink.tdesc should be edited using [setTxFields]{@link * CosmicLink#setTxFields}. */ }, { key: "network", get: function get() { return this.tdesc && this.tdesc.network || this.config.network; } /** * The URL of the horizon node from which ledger data will be retrieved, and * to which the signed transaction will be posted if there's no * [callback]{@link CosmicLink#callback}. * * This can be defined either locally (`cosmicLink.tdesc.horizon`) or globally * (using [setupNetwork]{@link module:config.setupNetwork}). This parameter is * special in the sense that it's the only one for which the global * configuration takes precedence. * * The rationale for this behavior is that we want transaction emitter to * provide a fallback Horizon URL in the special case none is known for a * custom network, but generally speaking it won't be right to allow the * transaction emitter to force us to use a particular Horizon node. * * **Note:** cosmicLink.tdesc should be edited using [setTxFields]{@link * CosmicLink#setTxFields}. */ }, { key: "horizon", get: function get() { return resolve.horizon(this.config, this.network) || this.tdesc && this.tdesc.horizon; } /** * The URL at which the signed transaction will be posted. This can be defined * either locally (`cosmicLink.tdesc.callback`) or globally * (`cosmicLib.config.callback`). The local configuration takes precedence. * * When no callback is defined, the signed transaction is posted to * [Horizon]{@link CosmicLink#horizon}. This is the default behavior. * * **Note:** cosmicLink.tdesc should be edited using [setTxFields]{@link * CosmicLink#setTxFields}. */ }, { key: "callback", get: function get() { return this.tdesc && this.tdesc.callback || this.config.callback; } }, { key: "htmlDescription", get: function get() { if (!this._htmlDescription) makeHtmlDescription(this); return this._htmlDescription; } /** * A link HTML Element that points to `cosmicLink.uri` */ }, { key: "htmlLink", get: function get() { if (!this._htmlLink) makeHtmlLink(this); return this._htmlLink; } }]); return CosmicLink; }(); /** * Initialize or reset a CosmicLink. * * @private */ function initCosmicLink(cosmicLink, transaction) { var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; checkLock(cosmicLink); /// Reset object in case of reparse. formatsFields.forEach(function (type) { return delete cosmicLink[type]; }); cosmicLink.page = cosmicLink.page || options.page || config.page; /** * The status of a CosmicLink. It becomes non-null in case of failure. * @var CosmicLink#status */ /** * By default `false`, or an *Array* of errors. * @var CosmicLink#errors */ status.init(cosmicLink); /** * The CosmicLink cache contains the resolved federations addresses and the * accounts object. Using the same set of data for all the CosmicLink related * computations ensure consistent results. * * @var CosmicLink#cache */ cosmicLink.cache = { destination: {}, account: {} }; /** * After parsing a SEP-0007 link, `cosmicLink.extra` contains SEP-0007 * specific information: * * - `cosmicLink.extra.type` indicates the operation encoded into the SEP-0007 * link (either `tx` or `pay`). * - `cosmicLink.extra.originDomain` is a _Promise_ that resolves to the * origin_domain parameter when the link signature is valid. It rejects an * error when the signature check fails. This property is `undefined` when * the link has no origin_domain. * - `cosmicLink.extra.signature` contains the link signature, if any. * - `cosmicLink.extra.pubkey` contains the tx operation `pubkey`, if any. * - `cosmicLink.extra.msg` contains the parsed `msg`, if any. This is * provided for compatibility purpose only. Displaying messages from * untrusted sources into trusted interfaces opens hard to mitigate attack * vectors & is discouraged. * * @var CosmicLink#extra */ cosmicLink.extra = {}; parse.dispatch(cosmicLink, transaction, options); if (env.isBrowser) { makeHtmlLink(cosmicLink); if (!cosmicLink._htmlDescription) { /// #cosmiclib_htmlNode: Backward compatibility (2018-09 -> 2019-03). cosmicLink._htmlDescription = html.grab("#cosmiclink_description") || html.grab("#CL_htmlNode"); } if (cosmicLink._htmlDescription) { if (cosmicLink.htmlDescription.id === "#CL_htmlNode") { misc.deprecated("2019-03", "id=\"#CL_htmlNode\"", "id=\"cosmiclink_description\""); } makeHtmlDescription(cosmicLink); } } } var formatsFields = ["_query", "_tdesc", "_json", "_transaction", "_xdr"]; /** * Initialize CosmicLink html nodes. * * @private */ function makeHtmlDescription(cosmicLink) { if (env.isNode) return; var htmlDescription = cosmicLink._htmlDescription; if (htmlDescription) { html.clear(htmlDescription); htmlDescription.className = "cosmiclink_description"; } else { htmlDescription = html.create("div", ".cosmiclink_description"); cosmicLink._htmlDescription = htmlDescription; } cosmicLink._transactionNode = format.tdesc(cosmicLink, cosmicLink.tdesc); cosmicLink._statusNode = status.makeHtmlNode(cosmicLink); cosmicLink._signersNode = html.create("div", ".cosmiclib_signersNode"); html.append(htmlDescription, cosmicLink._transactionNode, cosmicLink._statusNode, cosmicLink._signersNode); } /** * Make the HTML link. * @private */ function makeHtmlLink(cosmicLink) { if (env.isNode) return; var htmlLink = html.grab("#cosmiclink") || html.create("a"); htmlLink.className = ".cosmiclink"; htmlLink.href = cosmicLink.page; htmlLink.onclick = function () { return htmlLink.href = cosmicLink.uri; }; if (!htmlLink.title) htmlLink.title = "Sign transaction"; if (!htmlLink.textContent) htmlLink.textContent = "CosmicLink"; cosmicLink._htmlLink = htmlLink; return htmlLink; } /** * Throw an error if CosmicLink is locked. * @private */ function checkLock(cosmicLink) { if (cosmicLink.locker) throw new Error("Cosmic link is locked."); } CosmicLink.prototype.__proto__ = config; module.exports = CosmicLink;