cosmic-lib
Version:
A JavaScript implementation of the CosmicLink protocol for Stellar
355 lines (299 loc) • 10.3 kB
JavaScript
"use strict";
/**
* Contains functions that probe the blockchain or federation servers to collect
* datas.
*
* @exports resolve
*/
const resolve = exports;
const misc = require("@cosmic-plus/jsutils/es5/misc");
const StellarSdk = require("@cosmic-plus/base/es5/stellar-sdk");
const specs = require("./specs");
const status = require("./status");
/**
* Returns the
* [Server]{@link https://stellar.github.io/js-stellar-sdk/Server.html} object
* for **horizon**, or for **network**, or for the current network.
*
* @param {string} [network] 'public', 'test' or a network passphrase
* @param {string} [horizon] A horizon URL
* @returns {Server} A StellarSdk Server object
*/
resolve.server = function (conf, network = conf.network, horizon = conf.horizon) {
if (!horizon) horizon = resolve.horizon(conf, network);
if (!horizon) throw new Error("No horizon node defined for selected network.");
if (!conf.current.server[horizon]) {
conf.current.server[horizon] = new StellarSdk.Server(horizon);
}
return conf.current.server[horizon];
};
/**
* Switch to the current network, or to **network** if provided.
*
* @deprecated StellarSdk global `Network` setting is deprecated.
* @param {string} [network] 'public', 'test' or a network passphrase
* @returns {Server} A StellarSdk Server object
*/
resolve.useNetwork = function (conf, network = conf.network) {
// DEPRECATED - to be removed when in sync with stellar-sdk 3.x.
console.warn("`.selectNetwork()`, `.useNetwork()`, as well as StellarSdk global `Network` setting are deprecated. Please use `cosmicLib.config.network` or pass parameter explicitely.");
const passphrase = resolve.networkPassphrase(conf, network);
const currentPassphrase = resolve.networkPassphrase();
if (passphrase !== currentPassphrase) {
// eslint-disable-next-line no-console
console.log("Switch to network: " + network);
StellarSdk.Network.use(new StellarSdk.Network(passphrase));
}
};
/**
* Returns the curent Horizon node URL, or the Horizon node URL for **network**
* if provided.
*
* @param {string} [network] A network name or passphrase.
*/
resolve.horizon = function (conf, network = conf.network) {
if (conf.horizon) {
return conf.horizon;
} else {
const passphrase = resolve.networkPassphrase(conf, network);
if (conf.current && conf.current.horizon[passphrase]) {
return conf.current.horizon[passphrase];
}
}
};
/**
* Returns the current network passphrase, or the passphrase for **network** is
* provided.
*/
resolve.networkPassphrase = function (conf = {}, network = conf.network) {
if (network === undefined) {
// DEPRECATED: To be removed in sync with stellar-sdk 3.x.
const currentNetwork = StellarSdk.Network.current();
if (currentNetwork) return currentNetwork.networkPassphrase();
} else {
return conf.current.passphrase[network] || network;
}
};
/**
* Returns the network name for **network passphrase**, or `undefined`.
*
* @param {string} networkPassphrase
* @return {string}
*/
resolve.networkName = function (conf = {}, networkPassphrase) {
const index = Object.values(conf.current.passphrase).indexOf(networkPassphrase);
if (index === -1) return networkPassphrase;else return Object.keys(conf.current.passphrase)[index];
};
/**
* Returns the federation server
* [Account]{@link https://stellar.github.io/js-stellar-sdk/Account.html}
* for **address**.
*
* @async
* @param {string} address A Stellar public key or a federated address
* @return {} Resolve to federation server response
*/
resolve.address = function (conf, address) {
const cache = conf.cache;
if (cache && cache.destination[address]) return cache.destination[address];
const promise = addressResolver(conf, address);
if (cache) cache.destination[address] = promise;
return promise;
};
async function addressResolver(conf, address) {
try {
const account = await StellarSdk.FederationServer.resolve(address);
const accountId = account.account_id;
if (!accountId) throw new Error("Unknow address");
if (!account.memo_type) delete account.memo;
if (address !== accountId) account.address = address;
if (conf.aliases && conf.aliases[accountId]) {
account.alias = conf.aliases[accountId];
}
return account;
} catch (error) {
console.error(error);
status.error(conf, "Can't resolve: " + misc.shorter(address));
status.fail(conf, "Unresolved address", "throw");
}
}
/**
* Returns the
* [AccountResponse]{@link https://stellar.github.io/js-stellar-sdk/AccountResponse.html}
* object for **address**.
*
* @param {string} address A public key or a federated address
* @return {Object} The AccountResponse
*/
resolve.account = async function (conf, address, quietFlag) {
const account = await resolve.address(conf, address);
const accountId = account.account_id;
const cache = conf.cache;
if (cache && cache.account[accountId]) return cache.account[accountId];
const promise = accountResolver(conf, accountId, quietFlag);
if (cache) cache.account[accountId] = promise;
return promise;
};
async function accountResolver(conf, accountId, quietFlag) {
const server = resolve.server(conf);
try {
const accountResponse = await server.loadAccount(accountId);
return accountResponse;
} catch (error) {
if (quietFlag) {
throw error;
} else {
if (error.response) {
status.error(conf, "Empty account: " + misc.shorter(accountId), "throw");
} else {
status.error(conf, "Invalid horizon node: " + resolve.horizon(conf), "throw");
}
}
}
}
/**
* Returns `true` if **address** account is empty, `false` otherwise.
*
* @async
* @param {string} address Public key or federated address
* @return {boolean}
*/
resolve.isAccountEmpty = function (conf, address) {
return resolve.account(conf, address, true).then(() => false).catch(() => true);
};
/**
* Returns the account object for transaction source **address`** with sequence
* set at **sequence** if provided. If **address** is not provided, returns the
* neutral account object instead (as in SEP-0007 specifications).
*
* @param {string} [address]
* @param {string|numbre} [sequence]
* @return {AccountResponse}
*/
resolve.txSourceAccount = async function (conf, address, sequence) {
if (!address) {
return makeAccountResponse(conf, specs.neutralAccountId, "-1");
} else {
const destination = await resolve.address(conf, address);
if (destination.memo) status.error(conf, "Invalid transaction source address (requires a memo)", "throw");
const account = await resolve.account(conf, destination.account_id);
if (sequence) {
const baseAccount = new StellarSdk.Account(account.id, sequence);
baseAccount.sequence = baseAccount.sequence.sub(1);
account._baseAccount = baseAccount;
}
return account;
}
};
/**
* Creates an AccountResponse object with signers set for an empty account.
*
* @param {string} publicKey
* @param {string} sequence [description]
* @return {AccountResponse}
*/
function makeAccountResponse(conf, publicKey, sequence) {
const account = new StellarSdk.Account(publicKey, sequence);
if (conf.cache) conf.cache.account[publicKey] = account;
account.id = publicKey;
account.signers = [{
public_key: publicKey,
weight: 1,
key: publicKey,
type: "ed25519_public_key"
}];
return account;
}
/**
* Returns the array of all source accounts ID involved in **transaction**.
*
* @param {Transaction} transaction
* @return {Array}
*/
resolve.txSources = function (conf, transaction) {
if (!transaction.source) throw new Error("No source for transaction");
const extra = resolve.extra(conf, transaction);
if (extra.cache.txSources) return extra.cache.txSources;
const array = extra.cache.txSources = [transaction.source];
for (let index in transaction.operations) {
const source = transaction.operations[index].source;
if (source && !array.find(a => a === source)) array.push(source);
}
return array;
};
/**
* Returns an object such as:
*
* ```js
* {
* $accountId: $accountSigners
* ...
* }
* ```
*
* @param {Transaction} transaction
* @return {Object}
*/
resolve.txSigners = async function (conf, transaction) {
const extra = resolve.extra(conf, transaction);
if (extra.cache.txSigners) return extra.cache.txSigners;
const txSources = resolve.txSources(extra, transaction);
const signers = extra.cache.txSigners = {};
for (let index in txSources) {
const source = txSources[index];
const account = await resolveTxSource(extra, source);
if (!signers[account.id]) {
signers[account.id] = account.signers.filter(signer => {
return signer.type !== "preauthTx";
});
}
}
return signers;
};
async function resolveTxSource(conf, address) {
try {
return await resolve.account(conf, address, "quiet");
} catch (error) {
return makeAccountResponse(conf, address, "0");
}
}
/**
* Returns an Array containing the keys for all legit signers of **transaction**.
*
* @param {Transaction} transaction
* @return {Array}
*/
resolve.txSignersList = async function (conf, transaction) {
const extra = resolve.extra(conf, transaction);
if (!extra.cache.txSignersList) {
const txSigners = await resolve.txSigners(extra, transaction);
extra.cache.txSignersList = signersTableToSignersList(txSigners);
}
return extra.cache.txSignersList;
};
function signersTableToSignersList(signersTable) {
const array = [];
for (let accountId in signersTable) {
signersTable[accountId].forEach(signer => {
if (!array.find(key => key === signer.key)) array.push(signer.key);
});
}
return array;
}
/**
* Add an extra field to **object** that embed cache and local configuration.
*
* @private
*/
resolve.extra = function (conf, object) {
if (!object._cosmicplus) {
misc.setHiddenProperty(object, "_cosmicplus", {});
if (conf.cache) object._cosmicplus.cache = conf.cache;else object._cosmicplus.cache = {
destination: {},
account: {}
};
object._cosmicplus.network = conf.network;
object._cosmicplus.current = conf.current;
}
return object._cosmicplus;
};