anchor-link
Version:
Library for authenticating and signing transactions using the Anchor Link protocol
1,243 lines (1,226 loc) • 47.9 kB
JavaScript
/**
* 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