"use strict"
const env = require("@cosmic-plus/jsutils/es5/env")
const misc = require("@cosmic-plus/jsutils/es5/misc")
const html = env.isBrowser && require("@cosmic-plus/domutils/es5/html")
const SideFrame = env.isBrowser && require("./helpers/side-frame")
const action = require("./action")
const config = require("./config")
const convert = require("./convert")
const format = env.isBrowser && require("./format")
const parse = require("./parse")
const resolve = require("./resolve")
const sep7Utils = require("./sep7-utils")
const 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.
*/
class CosmicLink {
/**
* 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}
*/
constructor (transaction, options) {
initCosmicLink(this, transaction, options)
}
/**
* Refer to the underlying global configuration
* @private
*/
get config () {
return this.__proto__.__proto__
}
/**
* 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.
*/
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`
*/
get uri () {
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.
*/
get query () {
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
*/
get tdesc () {
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.
*/
get json () {
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.
*/
get transaction () {
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.
*/
get xdr () {
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.
*/
get sep7 () {
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}.
*/
get source () {
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}.
*/
get network () {
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}.
*/
get horizon () {
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}.
*/
get callback () {
return this.tdesc && this.tdesc.callback || this.config.callback
}
/// 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}
*/
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}
*/
addOperation (type, params) {
checkLock(this)
const 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}
*/
insertOperation (index, type, params) {
checkLock(this)
if (index > !this.tdesc.operations.length) {
throw new Error(
`Can't insert opereration at position ${index}: there are only ${this.tdesc.operations.length} operations`
)
}
const odesc = Object.assign({ 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}
*/
setOperation (index, type, params) {
checkLock(this)
if (!this.tdesc.operations[index]) {
throw new Error(`Operation ${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.
*/
selectNetwork () {
return resolve.useNetwork(this)
}
lock (options) {
return action.lock(this, options)
}
sign (...keypairs_or_preimage) {
return action.sign(this, ...keypairs_or_preimage)
}
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.
*/
get htmlDescription () {
if (!this._htmlDescription) makeHtmlDescription(this)
return this._htmlDescription
}
/**
* A link HTML Element that points to `cosmicLink.uri`
*/
get htmlLink () {
if (!this._htmlLink) makeHtmlLink(this)
return this._htmlLink
}
/**
* 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`.
*/
open (target = "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: ${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.
*/
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.
*/
async verifySep7 () {
if (this.extra.originDomain instanceof Promise) {
const domain = await this.extra.originDomain
this.extra.originDomain = domain
} else if (this.extra.originDomain) {
sep7Utils.verifySignature(this, this.extra.originDomain)
}
return this.extra.originDomain
}
}
/**
* Initialize or reset a CosmicLink.
*
* @private
*/
function initCosmicLink (cosmicLink, transaction, options = {}) {
checkLock(cosmicLink)
/// Reset object in case of reparse.
formatsFields.forEach((type) => 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)
}
}
}
const formatsFields = ["_query", "_tdesc", "_json", "_transaction", "_xdr"]
/**
* Initialize CosmicLink html nodes.
*
* @private
*/
function makeHtmlDescription (cosmicLink) {
if (env.isNode) return
let 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
const htmlLink = html.grab("#cosmiclink") || html.create("a")
htmlLink.className = ".cosmiclink"
htmlLink.href = cosmicLink.page
htmlLink.onclick = () => 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