UNPKG

anchor-link

Version:

Library for authenticating and signing transactions using the Anchor Link protocol

1,243 lines (1,226 loc) 47.9 kB
/** * Anchor Link v3.6.0 * https://github.com/greymass/anchor-link * * @license * Copyright (c) 2020 Greymass Inc. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * 1. Redistribution of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * 2. Redistribution in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * * YOU ACKNOWLEDGE THAT THIS SOFTWARE IS NOT DESIGNED, LICENSED OR INTENDED FOR USE * IN THE DESIGN, CONSTRUCTION, OPERATION OR MAINTENANCE OF ANY MILITARY FACILITY. */ 'use strict'; var tslib = require('tslib'); var zlib = require('pako'); var antelope = require('@wharfkit/antelope'); var signingRequest = require('@wharfkit/signing-request'); var makeFetch = require('fetch-ponyfill'); var miniaes = require('@greymass/miniaes'); var uuid = require('uuid'); var WebSocket = require('isomorphic-ws'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } function _mergeNamespaces(n, m) { m.forEach(function (e) { e && typeof e !== 'string' && !Array.isArray(e) && Object.keys(e).forEach(function (k) { if (k !== 'default' && !(k in n)) { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); }); return Object.freeze(n); } var zlib__default = /*#__PURE__*/_interopDefaultLegacy(zlib); var antelope__namespace = /*#__PURE__*/_interopNamespace(antelope); var signingRequest__namespace = /*#__PURE__*/_interopNamespace(signingRequest); var makeFetch__default = /*#__PURE__*/_interopDefaultLegacy(makeFetch); var WebSocket__default = /*#__PURE__*/_interopDefaultLegacy(WebSocket); /** * Error that is thrown if a [[LinkTransport]] cancels a request. * @internal */ class CancelError extends Error { constructor(reason) { super(`User canceled request ${reason ? '(' + reason + ')' : ''}`); this.code = 'E_CANCEL'; } } /** * Error that is thrown if an identity request fails to verify. * @internal */ class IdentityError extends Error { constructor(reason) { super(`Unable to verify identity ${reason ? '(' + reason + ')' : ''}`); this.code = 'E_IDENTITY'; } } /** * Error originating from a [[LinkSession]]. * @internal */ class SessionError extends Error { constructor(reason, code, session) { super(reason); this.code = code; this.session = session; } } /** @internal */ var LinkOptions; (function (LinkOptions) { /** @internal */ LinkOptions.defaults = { service: 'https://cb.anchor.link', verifyProofs: false, encodeChainIds: true, }; })(LinkOptions || (LinkOptions = {})); let SealedMessage = class SealedMessage extends antelope.Struct { }; tslib.__decorate([ antelope.Struct.field('public_key') ], SealedMessage.prototype, "from", void 0); tslib.__decorate([ antelope.Struct.field('uint64') ], SealedMessage.prototype, "nonce", void 0); tslib.__decorate([ antelope.Struct.field('bytes') ], SealedMessage.prototype, "ciphertext", void 0); tslib.__decorate([ antelope.Struct.field('uint32') ], SealedMessage.prototype, "checksum", void 0); SealedMessage = tslib.__decorate([ antelope.Struct.type('sealed_message') ], SealedMessage); let LinkCreate = class LinkCreate extends antelope.Struct { }; tslib.__decorate([ antelope.Struct.field('name') ], LinkCreate.prototype, "session_name", void 0); tslib.__decorate([ antelope.Struct.field('public_key') ], LinkCreate.prototype, "request_key", void 0); tslib.__decorate([ antelope.Struct.field('string', { extension: true }) ], LinkCreate.prototype, "user_agent", void 0); LinkCreate = tslib.__decorate([ antelope.Struct.type('link_create') ], LinkCreate); let LinkInfo = class LinkInfo extends antelope.Struct { }; tslib.__decorate([ antelope.Struct.field('time_point_sec') ], LinkInfo.prototype, "expiration", void 0); LinkInfo = tslib.__decorate([ antelope.Struct.type('link_info') ], LinkInfo); /** @internal */ const fetch = makeFetch__default["default"]().fetch; /** * Encrypt a message using AES and shared secret derived from given keys. * @internal */ function sealMessage(message, privateKey, publicKey, nonce) { const secret = privateKey.sharedSecret(publicKey); if (!nonce) { nonce = antelope.UInt64.random(); } const key = antelope.Checksum512.hash(antelope.Serializer.encode({ object: nonce }).appending(secret.array)); const cbc = new miniaes.AES_CBC(key.array.slice(0, 32), key.array.slice(32, 48)); const ciphertext = antelope.Bytes.from(cbc.encrypt(antelope.Bytes.from(message, 'utf8').array)); const checksumView = new DataView(antelope.Checksum256.hash(key.array).array.buffer); const checksum = checksumView.getUint32(0, true); return SealedMessage.from({ from: privateKey.toPublic(), nonce, ciphertext, checksum, }); } /** * Extract session metadata from a callback payload and request. * @internal */ function sessionMetadata(payload, request) { const metadata = { // backwards compat, can be removed next major release sameDevice: request.getRawInfo()['return_path'] !== undefined, }; // append extra metadata from the signer if (payload.link_meta) { try { const parsed = JSON.parse(payload.link_meta); for (const key of Object.keys(parsed)) { // normalize key names to camelCase metadata[snakeToCamel(key)] = parsed[key]; } } catch (error) { logWarn('Unable to parse link metadata', error, payload.link_meta); } } return metadata; } /** * Return PascalCase version of snake_case string. * @internal */ function snakeToPascal(name) { return name .split('_') .map((v) => (v[0] ? v[0].toUpperCase() : '_') + v.slice(1)) .join(''); } /** * Return camelCase version of snake_case string. * @internal */ function snakeToCamel(name) { const pascal = snakeToPascal(name); return pascal[0].toLowerCase() + pascal.slice(1); } /** * Print a warning message to console. * @internal **/ function logWarn(...args) { // eslint-disable-next-line no-console console.warn('[anchor-link]', ...args); } /** * Type describing a link session that can create a eosjs compatible * signature provider and transact for a specific auth. */ class LinkSession { /** @internal */ constructor() { } // eslint-disable-line @typescript-eslint/no-empty-function /** * Convenience, remove this session from associated [[Link]] storage if set. * Equivalent to: * ```ts * session.link.removeSession(session.identifier, session.auth, session.chainId) * ``` */ remove() { return tslib.__awaiter(this, void 0, void 0, function* () { if (this.link.storage) { yield this.link.removeSession(this.identifier, this.auth, this.chainId); } }); } /** API client for the chain this session is valid on. */ get client() { return this.link.getChain(this.chainId).client; } /** Restore a previously serialized session. */ static restore(link, data) { switch (data.type) { case 'channel': return new LinkChannelSession(link, data.data, data.metadata); case 'fallback': return new LinkFallbackSession(link, data.data, data.metadata); default: throw new Error('Unable to restore, session data invalid'); } } } /** * Link session that pushes requests over a channel. * @internal */ class LinkChannelSession extends LinkSession { constructor(link, data, metadata) { super(); this.type = 'channel'; this.timeout = 2 * 60 * 1000; // ms this.link = link; this.chainId = signingRequest.ChainId.from(data.chainId); this.auth = antelope.PermissionLevel.from(data.auth); this.publicKey = antelope.PublicKey.from(data.publicKey); this.identifier = antelope.Name.from(data.identifier); const privateKey = antelope.PrivateKey.from(data.requestKey); this.channelKey = antelope.PublicKey.from(data.channel.key); this.channelUrl = data.channel.url; this.channelName = data.channel.name; this.encrypt = (request) => { return sealMessage(request.encode(true, false), privateKey, this.channelKey); }; this.metadata = Object.assign(Object.assign({}, (metadata || {})), { timeout: this.timeout, name: this.channelName, request_key: privateKey.toPublic() }); this.serialize = () => ({ type: 'channel', data: Object.assign(Object.assign({}, data), { channel: { url: this.channelUrl, key: this.channelKey, name: this.channelName, } }), metadata: this.metadata, }); } onSuccess(request, result) { if (this.link.transport.onSuccess) { this.link.transport.onSuccess(request, result); } } onFailure(request, error) { if (this.link.transport.onFailure) { this.link.transport.onFailure(request, error); } } onRequest(request, cancel) { const info = LinkInfo.from({ expiration: new Date(Date.now() + this.timeout), }); if (this.link.transport.onSessionRequest) { this.link.transport.onSessionRequest(this, request, cancel); } const timer = setTimeout(() => { cancel(new SessionError('Wallet did not respond in time', 'E_TIMEOUT', this)); }, this.timeout); request.setInfoKey('link', info); let payloadSent = false; const payload = antelope.Serializer.encode({ object: this.encrypt(request) }); if (this.link.transport.sendSessionPayload) { try { payloadSent = this.link.transport.sendSessionPayload(payload, this); } catch (error) { logWarn('Unexpected error when transport tried to send session payload', error); } } if (payloadSent) { return; } fetch(this.channelUrl, { method: 'POST', headers: { 'X-Buoy-Soft-Wait': '10', }, body: payload.array, }) .then((response) => { if (Math.floor(response.status / 100) !== 2) { clearTimeout(timer); if (response.status === 202) { logWarn('Missing delivery ack from session channel'); } cancel(new SessionError('Unable to push message', 'E_DELIVERY', this)); } }) .catch((error) => { clearTimeout(timer); cancel(new SessionError(`Unable to reach link service (${error.message || String(error)})`, 'E_DELIVERY', this)); }); } addLinkInfo(request) { const createInfo = LinkCreate.from({ session_name: this.identifier, request_key: this.metadata.request_key, user_agent: this.link.getUserAgent(), }); request.setInfoKey('link', createInfo); } prepare(request) { if (this.link.transport.prepare) { return this.link.transport.prepare(request, this); } return Promise.resolve(request); } showLoading() { if (this.link.transport.showLoading) { return this.link.transport.showLoading(); } } recoverError(error, request) { if (this.link.transport.recoverError) { return this.link.transport.recoverError(error, request); } return false; } makeSignatureProvider() { return this.link.makeSignatureProvider([this.publicKey.toString()], this.chainId, this); } transact(args, options) { return tslib.__awaiter(this, void 0, void 0, function* () { const res = yield this.link.transact(args, Object.assign(Object.assign({}, options), { chain: this.chainId }), this); // update session if callback payload contains new channel info if (res.payload.link_ch && res.payload.link_key && res.payload.link_name) { try { const metadata = Object.assign(Object.assign({}, this.metadata), sessionMetadata(res.payload, res.resolved.request)); this.channelUrl = res.payload.link_ch; this.channelKey = antelope.PublicKey.from(res.payload.link_key); this.channelName = res.payload.link_name; metadata.name = res.payload.link_name; this.metadata = metadata; } catch (error) { logWarn('Unable to recover link session', error); } } return res; }); } } /** * Link session that sends every request over the transport. * @internal */ class LinkFallbackSession extends LinkSession { constructor(link, data, metadata) { super(); this.type = 'fallback'; this.link = link; this.auth = antelope.PermissionLevel.from(data.auth); this.publicKey = antelope.PublicKey.from(data.publicKey); this.chainId = signingRequest.ChainId.from(data.chainId); this.metadata = metadata || {}; this.identifier = antelope.Name.from(data.identifier); this.serialize = () => ({ type: this.type, data, metadata: this.metadata, }); } onSuccess(request, result) { if (this.link.transport.onSuccess) { this.link.transport.onSuccess(request, result); } } onFailure(request, error) { if (this.link.transport.onFailure) { this.link.transport.onFailure(request, error); } } onRequest(request, cancel) { if (this.link.transport.onSessionRequest) { this.link.transport.onSessionRequest(this, request, cancel); } else { this.link.transport.onRequest(request, cancel); } } prepare(request) { if (this.link.transport.prepare) { return this.link.transport.prepare(request, this); } return Promise.resolve(request); } showLoading() { if (this.link.transport.showLoading) { return this.link.transport.showLoading(); } } makeSignatureProvider() { return this.link.makeSignatureProvider([this.publicKey.toString()], this.chainId, this); } transact(args, options) { return this.link.transact(args, Object.assign(Object.assign({}, options), { chain: this.chainId }), this); } } /** @internal */ class BuoyCallbackService { constructor(address) { this.address = address.trim().replace(/\/$/, ''); } create() { const url = `${this.address}/${uuid.v4()}`; return new BuoyCallback(url); } } /** @internal */ class BuoyCallback { constructor(url) { this.url = url; this.ctx = {}; } wait() { if (this.url.includes('hyperbuoy')) { return pollForCallback(this.url, this.ctx); } else { return waitForCallback(this.url, this.ctx); } } cancel() { if (this.ctx.cancel) { this.ctx.cancel(); } } } /** * Connect to a WebSocket channel and wait for a message. * @internal */ function waitForCallback(url, ctx) { return new Promise((resolve, reject) => { let active = true; let retries = 0; const socketUrl = url.replace(/^http/, 'ws'); const handleResponse = (response) => { try { resolve(JSON.parse(response)); } catch (error) { error.message = 'Unable to parse callback JSON: ' + error.message; reject(error); } }; const connect = () => { const socket = new WebSocket__default["default"](socketUrl); ctx.cancel = () => { active = false; if (socket.readyState === WebSocket__default["default"].OPEN || socket.readyState === WebSocket__default["default"].CONNECTING) { socket.close(); } }; socket.onmessage = (event) => { active = false; if (socket.readyState === WebSocket__default["default"].OPEN) { socket.close(); } if (typeof Blob !== 'undefined' && event.data instanceof Blob) { const reader = new FileReader(); reader.onload = () => { handleResponse(reader.result); }; reader.onerror = (error) => { reject(error); }; reader.readAsText(event.data); } else { if (typeof event.data === 'string') { handleResponse(event.data); } else { handleResponse(event.data.toString()); } } }; socket.onopen = () => { retries = 0; }; socket.onclose = () => { if (active) { setTimeout(connect, backoff(retries++)); } }; }; connect(); }); } /** * Long-poll for message. * @internal */ function pollForCallback(url, ctx) { return tslib.__awaiter(this, void 0, void 0, function* () { let active = true; ctx.cancel = () => { active = false; }; while (active) { try { const res = yield fetch(url); if (res.status === 408) { continue; } else if (res.status === 200) { return yield res.json(); } else { throw new Error(`HTTP ${res.status}: ${res.statusText}`); } } catch (error) { logWarn('Unexpected hyperbuoy error', error); } yield sleep(1000); } return null; }); } /** * Exponential backoff function that caps off at 10s after 10 tries. * https://i.imgur.com/IrUDcJp.png * @internal */ function backoff(tries) { return Math.min(Math.pow(tries * 10, 2), 10 * 1000); } /** * Return promise that resolves after given milliseconds. * @internal */ function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } /** * Class representing a EOSIO chain. */ class LinkChain { /** @internal */ constructor(chainId, clientOrUrl) { this.abiCache = new Map(); this.pendingAbis = new Map(); this.chainId = signingRequest.ChainId.from(chainId); this.client = typeof clientOrUrl === 'string' ? new antelope.APIClient({ url: clientOrUrl }) : clientOrUrl; } /** * Fetch the ABI for given account, cached. * @internal */ getAbi(account) { return tslib.__awaiter(this, void 0, void 0, function* () { const key = String(account); let rv = this.abiCache.get(key); if (!rv) { let getAbi = this.pendingAbis.get(key); if (!getAbi) { getAbi = this.client.v1.chain.get_abi(account); this.pendingAbis.set(key, getAbi); } rv = (yield getAbi).abi; this.pendingAbis.delete(key); if (rv) { this.abiCache.set(key, rv); } } return rv; }); } } /** * Anchor Link main class. * * @example * * ```ts * import AnchorLink from 'anchor-link' * import ConsoleTransport from 'anchor-link-console-transport' * * const link = new AnchorLink({ * transport: new ConsoleTransport(), * chains: [ * { * chainId: 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906', * nodeUrl: 'https://eos.greymass.com', * }, * ], * }) * * const result = await link.transact({actions: myActions}) * ``` */ class Link { /** Create a new link instance. */ constructor(options) { if (typeof options !== 'object') { throw new TypeError('Missing options object'); } if (!options.transport) { throw new TypeError('options.transport is required'); } let chains = options.chains || []; if (options.chainId && options.client) { if (options.chains.length > 0) { throw new TypeError('options.chainId and options.client are deprecated and cannot be used together with options.chains'); } chains = [{ chainId: options.chainId, nodeUrl: options.client }]; } if (chains.length === 0) { throw new TypeError('options.chains is required'); } this.chains = chains.map((chain) => { if (chain instanceof LinkChain) { return chain; } if (!chain.chainId) { throw new TypeError('options.chains[].chainId is required'); } if (!chain.nodeUrl) { throw new TypeError('options.chains[].nodeUrl is required'); } return new LinkChain(chain.chainId, chain.nodeUrl); }); if (options.service === undefined || typeof options.service === 'string') { this.callbackService = new BuoyCallbackService(options.service || LinkOptions.defaults.service); } else { this.callbackService = options.service; } this.transport = options.transport; if (options.storage !== null) { this.storage = options.storage || this.transport.storage; } this.verifyProofs = options.verifyProofs !== undefined ? options.verifyProofs : LinkOptions.defaults.verifyProofs; this.encodeChainIds = options.encodeChainIds !== undefined ? options.encodeChainIds : LinkOptions.defaults.encodeChainIds; } /** * The APIClient instance for communicating with the node. * @note This returns the first APIClient when link is configured with multiple chains. */ get client() { return this.chains[0].client; } /** * Return a [[LinkChain]] object for given chainId or chain reference. * @throws If this link instance has no configured chain for given reference. * @internal */ getChain(chain) { if (chain instanceof LinkChain) { return chain; } if (typeof chain === 'number') { const rv = this.chains[chain]; if (!rv) { throw new Error(`Invalid chain index: ${chain}`); } return rv; } const id = signingRequest.ChainId.from(chain); const rv = this.chains.find((c) => c.chainId.equals(id)); if (!rv) { throw new Error(`Unsupported chain: ${id}`); } return rv; } /** * Create a SigningRequest instance configured for this link. * @internal */ createRequest(args, chain, transport) { return tslib.__awaiter(this, void 0, void 0, function* () { const t = transport || this.transport; let request; if (chain || this.chains.length === 1) { const c = chain || this.chains[0]; request = yield signingRequest.SigningRequest.create(Object.assign(Object.assign({}, args), { chainId: c.chainId, broadcast: false }), { abiProvider: c, zlib: zlib__default["default"] }); } else { // multi-chain request request = yield signingRequest.SigningRequest.create(Object.assign(Object.assign({}, args), { chainId: null, chainIds: this.encodeChainIds ? this.chains.map((c) => c.chainId) : undefined, broadcast: false }), // abi's will be pulled from the first chain and assumed to be identical on all chains { abiProvider: this.chains[0], zlib: zlib__default["default"] }); } if (t.prepare) { request = yield t.prepare(request); } const callback = this.callbackService.create(); request.setCallback(callback.url, true); return { request, callback }; }); } /** * Send a SigningRequest instance using this link. * @internal */ sendRequest(request, callback, chain, transport, broadcast = false) { return tslib.__awaiter(this, void 0, void 0, function* () { const t = transport || this.transport; try { const linkUrl = request.data.callback; if (linkUrl !== callback.url) { throw new Error('Invalid request callback'); } if (request.data.flags.broadcast === true || request.data.flags.background === false) { throw new Error('Invalid request flags'); } // wait for callback or user cancel let done = false; const cancel = new Promise((resolve, reject) => { t.onRequest(request, (reason) => { if (done) { // ignore any cancel calls once callbackResponse below has resolved return; } const error = typeof reason === 'string' ? new CancelError(reason) : reason; if (t.recoverError && t.recoverError(error, request) === true) { // transport was able to recover from the error return; } callback.cancel(); reject(error); }); }); const callbackResponse = yield Promise.race([callback.wait(), cancel]); done = true; if (typeof callbackResponse.rejected === 'string') { throw new CancelError(callbackResponse.rejected); } const payload = callbackResponse; const signer = antelope.PermissionLevel.from({ actor: payload.sa, permission: payload.sp, }); const signatures = Object.keys(payload) .filter((key) => key.startsWith('sig') && key !== 'sig0') .map((key) => antelope.Signature.from(payload[key])); let c; if (!chain && this.chains.length > 1) { if (!payload.cid) { throw new Error('Multi chain response payload must specify resolved chain id (cid)'); } c = this.getChain(payload.cid); } else { c = chain || this.getChain(0); if (payload.cid && !c.chainId.equals(payload.cid)) { throw new Error('Got response for wrong chain id'); } } // recreate transaction from request response const resolved = yield signingRequest.ResolvedSigningRequest.fromPayload(payload, { zlib: zlib__default["default"], abiProvider: c, }); // prepend cosigner signature if present const cosignerSig = resolved.request.getInfoKey('cosig', { type: antelope.Signature, array: true, }); if (cosignerSig) { signatures.unshift(...cosignerSig); } const result = { resolved, chain: c, transaction: resolved.transaction, resolvedTransaction: resolved.resolvedTransaction, signatures, payload, signer, }; if (broadcast) { const signedTx = antelope.SignedTransaction.from(Object.assign(Object.assign({}, resolved.transaction), { signatures })); const res = yield c.client.v1.chain.push_transaction(signedTx); result.processed = res.processed; } if (t.onSuccess) { t.onSuccess(request, result); } return result; } catch (error) { if (t.onFailure) { t.onFailure(request, error); } throw error; } }); } /** * Sign and optionally broadcast a EOSIO transaction, action or actions. * * Example: * * ```ts * let result = await myLink.transact({transaction: myTx}) * ``` * * @param args The action, actions or transaction to use. * @param options Options for this transact call. * @param transport Transport override, for internal use. */ transact(args, options, transport) { return tslib.__awaiter(this, void 0, void 0, function* () { const o = options || {}; const t = transport || this.transport; const c = o.chain !== undefined ? this.getChain(o.chain) : undefined; const broadcast = o.broadcast !== false; const noModify = o.noModify !== undefined ? o.noModify : !broadcast; // Initialize the loading state of the transport if (t && t.showLoading) { t.showLoading(); } // eosjs transact compat: upgrade to transaction if args have any header fields const anyArgs = args; if (args.actions && (anyArgs.expiration || anyArgs.ref_block_num || anyArgs.ref_block_prefix || anyArgs.max_net_usage_words || anyArgs.max_cpu_usage_ms || anyArgs.delay_sec)) { args = { transaction: Object.assign({ expiration: '1970-01-01T00:00:00', ref_block_num: 0, ref_block_prefix: 0, max_net_usage_words: 0, max_cpu_usage_ms: 0, delay_sec: 0 }, anyArgs), }; } const { request, callback } = yield this.createRequest(args, c, t); if (noModify) { request.setInfoKey('no_modify', true, 'bool'); } const result = yield this.sendRequest(request, callback, c, t, broadcast); return result; }); } /** * Send an identity request and verify the identity proof if [[LinkOptions.verifyProofs]] is true. * @param args.scope The scope of the identity request. * @param args.requestPermission Optional request permission if the request is for a specific account or permission. * @param args.info Metadata to add to the request. * @note This is for advanced use-cases, you probably want to use [[Link.login]] instead. */ identify(args) { return tslib.__awaiter(this, void 0, void 0, function* () { const { request, callback } = yield this.createRequest({ identity: { permission: args.requestPermission, scope: args.scope }, info: args.info, }); const res = yield this.sendRequest(request, callback); if (!res.resolved.request.isIdentity()) { throw new IdentityError('Unexpected response'); } let account; const proof = res.resolved.getIdentityProof(res.signatures[0]); if (this.verifyProofs) { account = yield res.chain.client.v1.chain.get_account(res.signer.actor); if (!account) { throw new IdentityError(`Signature from unknown account: ${proof.signer.actor}`); } const accountPermission = account.permissions.find(({ perm_name }) => proof.signer.permission.equals(perm_name)); if (!accountPermission) { throw new IdentityError(`${proof.signer.actor} signed for unknown permission: ${proof.signer.permission}`); } const proofValid = proof.verify(accountPermission.required_auth, account.head_block_time); if (!proofValid) { throw new IdentityError(`Invalid identify proof for: ${proof.signer}`); } } if (args.requestPermission) { const perm = antelope.PermissionLevel.from(args.requestPermission); if ((!perm.actor.equals(signingRequest.PlaceholderName) && !perm.actor.equals(proof.signer.actor)) || (!perm.permission.equals(signingRequest.PlaceholderPermission) && !perm.permission.equals(proof.signer.permission))) { throw new IdentityError(`Identity proof singed by ${proof.signer}, expected: ${formatAuth(perm)} `); } } return Object.assign(Object.assign({}, res), { account, proof }); }); } /** * Login and create a persistent session. * @param identifier The session identifier, an EOSIO name (`[a-z1-5]{1,12}`). * Should be set to the contract account if applicable. */ login(identifier) { return tslib.__awaiter(this, void 0, void 0, function* () { const privateKey = antelope.PrivateKey.generate('K1'); const requestKey = privateKey.toPublic(); const createInfo = LinkCreate.from({ session_name: identifier, request_key: requestKey, user_agent: this.getUserAgent(), }); const res = yield this.identify({ scope: identifier, info: { link: createInfo, scope: identifier, }, }); const metadata = sessionMetadata(res.payload, res.resolved.request); const signerKey = res.proof.recover(); let session; if (res.payload.link_ch && res.payload.link_key && res.payload.link_name) { session = new LinkChannelSession(this, { identifier, chainId: res.chain.chainId, auth: res.signer, publicKey: signerKey, channel: { url: res.payload.link_ch, key: res.payload.link_key, name: res.payload.link_name, }, requestKey: privateKey, }, metadata); } else { session = new LinkFallbackSession(this, { identifier, chainId: res.chain.chainId, auth: res.signer, publicKey: signerKey, }, metadata); } yield this.storeSession(session); return Object.assign(Object.assign({}, res), { session }); }); } /** * Restore previous session, use [[login]] to create a new session. * @param identifier The session identifier, must be same as what was used when creating the session with [[login]]. * @param auth A specific session auth to restore, if omitted the most recently used session will be restored. * @param chainId If given function will only consider that specific chain when restoring session. * @returns A [[LinkSession]] instance or null if no session can be found. * @throws If no [[LinkStorage]] adapter is configured or there was an error retrieving the session data. **/ restoreSession(identifier, auth, chainId) { return tslib.__awaiter(this, void 0, void 0, function* () { if (!this.storage) { throw new Error('Unable to restore session: No storage adapter configured'); } let key; if (auth && chainId) { // both auth and chain id given, we can look up on specific key key = this.sessionKey(identifier, formatAuth(antelope.PermissionLevel.from(auth)), String(signingRequest.ChainId.from(chainId))); } else { // otherwise we use the session list to filter down to most recently used matching given params let list = yield this.listSessions(identifier); if (auth) { list = list.filter((item) => item.auth.equals(auth)); } if (chainId) { const id = signingRequest.ChainId.from(chainId); list = list.filter((item) => item.chainId.equals(id)); } const latest = list[0]; if (!latest) { return null; } key = this.sessionKey(identifier, formatAuth(latest.auth), String(latest.chainId)); } const data = yield this.storage.read(key); if (!data) { return null; } let sessionData; try { sessionData = JSON.parse(data); } catch (error) { throw new Error(`Unable to restore session: Stored JSON invalid (${error.message || String(error)})`); } const session = LinkSession.restore(this, sessionData); if (auth || chainId) { // update latest used yield this.touchSession(identifier, session.auth, session.chainId); } return session; }); } /** * List stored session auths for given identifier. * The most recently used session is at the top (index 0). * @throws If no [[LinkStorage]] adapter is configured or there was an error retrieving the session list. **/ listSessions(identifier) { return tslib.__awaiter(this, void 0, void 0, function* () { if (!this.storage) { throw new Error('Unable to list sessions: No storage adapter configured'); } const key = this.sessionKey(identifier, 'list'); let list; try { list = JSON.parse((yield this.storage.read(key)) || '[]'); } catch (error) { throw new Error(`Unable to list sessions: ${error.message || String(error)}`); } return list.map(({ auth, chainId }) => ({ auth: antelope.PermissionLevel.from(auth), chainId: signingRequest.ChainId.from(chainId), })); }); } /** * Remove stored session for given identifier and auth. * @throws If no [[LinkStorage]] adapter is configured or there was an error removing the session data. */ removeSession(identifier, auth, chainId) { return tslib.__awaiter(this, void 0, void 0, function* () { if (!this.storage) { throw new Error('Unable to remove session: No storage adapter configured'); } const key = this.sessionKey(identifier, formatAuth(auth), String(chainId)); yield this.storage.remove(key); yield this.touchSession(identifier, auth, chainId, true); }); } /** * Remove all stored sessions for given identifier. * @throws If no [[LinkStorage]] adapter is configured or there was an error removing the session data. */ clearSessions(identifier) { return tslib.__awaiter(this, void 0, void 0, function* () { if (!this.storage) { throw new Error('Unable to clear sessions: No storage adapter configured'); } for (const { auth, chainId } of yield this.listSessions(identifier)) { yield this.removeSession(identifier, auth, chainId); } }); } /** * Create an eosjs compatible signature provider using this link. * @param availableKeys Keys the created provider will claim to be able to sign for. * @param chain Chain to use when configured with multiple chains. * @param transport (internal) Transport override for this call. * @note We don't know what keys are available so those have to be provided, * to avoid this use [[LinkSession.makeSignatureProvider]] instead. Sessions can be created with [[Link.login]]. */ makeSignatureProvider(availableKeys, chain, transport) { return { getAvailableKeys: () => tslib.__awaiter(this, void 0, void 0, function* () { return availableKeys; }), sign: (args) => tslib.__awaiter(this, void 0, void 0, function* () { const t = transport || this.transport; const c = chain ? this.getChain(chain) : this.chains[0]; let request = signingRequest.SigningRequest.fromTransaction(args.chainId, args.serializedTransaction, { abiProvider: c, zlib: zlib__default["default"] }); const callback = this.callbackService.create(); request.setCallback(callback.url, true); request.setBroadcast(false); if (t.prepare) { request = yield t.prepare(request); } const { transaction, signatures } = yield this.sendRequest(request, callback, c, t); const serializedTransaction = antelope.Serializer.encode({ object: transaction }); return Object.assign(Object.assign({}, args), { serializedTransaction, signatures }); }), }; } /** Makes sure session is in storage list of sessions and moves it to top (most recently used). */ touchSession(identifier, auth, chainId, remove = false) { return tslib.__awaiter(this, void 0, void 0, function* () { const list = yield this.listSessions(identifier); const existing = list.findIndex((item) => item.auth.equals(auth) && item.chainId.equals(chainId)); if (existing >= 0) { list.splice(existing, 1); } if (remove === false) { list.unshift({ auth, chainId }); } const key = this.sessionKey(identifier, 'list'); yield this.storage.write(key, JSON.stringify(list)); }); } /** * Makes sure session is in storage list of sessions and moves it to top (most recently used). * @internal */ storeSession(session) { return tslib.__awaiter(this, void 0, void 0, function* () { if (this.storage) { const key = this.sessionKey(session.identifier, formatAuth(session.auth), String(session.chainId)); const data = JSON.stringify(session.serialize()); yield this.storage.write(key, data); yield this.touchSession(session.identifier, session.auth, session.chainId); } }); } /** Session storage key for identifier and suffix. */ sessionKey(identifier, ...suffix) { return [String(antelope.Name.from(identifier)), ...suffix].join('-'); } /** * Return user agent of this link. * @internal */ getUserAgent() { let rv = `AnchorLink/${Link.version}`; if (this.transport.userAgent) { rv += ' ' + this.transport.userAgent(); } return rv; } } /** Package version. */ Link.version = '3.6.0'; // eslint-disable-line @typescript-eslint/no-inferrable-types /** * Format a EOSIO permission level in the format `actor@permission` taking placeholders into consideration. * @internal */ function formatAuth(auth) { const a = antelope.PermissionLevel.from(auth); const actor = a.actor.equals(signingRequest.PlaceholderName) ? '<any>' : String(a.actor); let permission; if (a.permission.equals(signingRequest.PlaceholderName) || a.permission.equals(signingRequest.PlaceholderPermission)) { permission = '<any>'; } else { permission = String(a.permission); } return `${actor}@${permission}`; } // export library var pkg = /*#__PURE__*/_mergeNamespaces({ __proto__: null, 'default': Link, IdentityProof: signingRequest.IdentityProof, ChainId: signingRequest.ChainId, ChainName: signingRequest.ChainName, LinkChain: LinkChain, Link: Link, LinkSession: LinkSession, LinkChannelSession: LinkChannelSession, LinkFallbackSession: LinkFallbackSession, CancelError: CancelError, IdentityError: IdentityError, SessionError: SessionError }, [signingRequest__namespace, antelope__namespace]); const AnchorLink = Link; for (const key of Object.keys(pkg)) { if (key === 'default') continue; AnchorLink[key] = pkg[key]; } module.exports = AnchorLink; //# sourceMappingURL=anchor-link.js.map