cosmos-delegation-js
Version:
Node API for Cosmos Delegation using Ledger Nano S/X
407 lines (341 loc) • 13.3 kB
JavaScript
/** ******************************************************************************
* (c) 2019 ZondaX GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************* */
import CosmosApp from 'ledger-cosmos-js';
import { getBech32FromPK } from 'ledger-cosmos-js';
import axios from 'axios';
import Big from 'big.js';
import secp256k1 from 'secp256k1';
import txs from './txs';
const defaultHrp = 'cosmos';
const CosmosDelegateTool = function (transport) {
// eslint-disable-next-line camelcase
this.connected = false;
this.lastError = 'No error';
this.checkAppInfo = false;
this.transportDebug = false;
this.transport = transport;
this.resturl = null;
this.requiredVersionMajor = 1;
this.requiredVersionMinor = 1;
};
// eslint-disable-next-line no-unused-vars
CosmosDelegateTool.prototype.setNodeURL = function (resturl) {
this.resturl = resturl;
};
function wrapError(cdt, e) {
try {
// eslint-disable-next-line no-param-reassign
let errMessage = '';
if (typeof e.response === 'undefined') {
errMessage = e.message;
} else {
errMessage = e.response.data.error;
}
// eslint-disable-next-line no-param-reassign
cdt.lastError = errMessage;
return {
error: errMessage,
};
} catch (e2) {
// eslint-disable-next-line no-param-reassign
cdt.lastError = `${e.message} ${e2.message}`;
return {
error: `${e.message} ${e2.message}`,
};
}
}
function nodeURL(cdt) {
if (typeof cdt.resturl === 'undefined' || cdt.resturl === null) {
throw new Error('Node URL has not been defined');
}
return cdt.resturl;
}
// Detect when a ledger device is connected and verify the cosmos app is running.
CosmosDelegateTool.prototype.connect = async function () {
this.connected = false;
this.lastError = null;
this.app = new CosmosApp(this.transport);
if (this.checkAppInfo) {
const appInfo = await this.app.appInfo();
if (appInfo.return_code !== 0x9000) {
this.lastError = appInfo.error_message;
return false;
}
appInfo.appName = appInfo.appName || '?';
console.log(`Detected app ${appInfo.appName} ${appInfo.appVersion}`);
if (appInfo.appName.toLowerCase() !== 'cosmos') {
this.lastError = `Incorrect app detected ${appInfo.appName.toString()}`;
return false;
}
}
const version = await this.app.getVersion();
if (version.return_code !== 0x9000) {
this.lastError = version.error_message;
throw new Error(version.error_message);
}
const major = version.major || 0;
const minor = version.minor || 0;
if (major < this.requiredVersionMajor || minor < this.requiredVersionMinor) {
this.lastError = 'Version not supported';
return false;
}
// Mark as connected
this.connected = true;
return this.connected;
};
function connectedOrThrow(cdt) {
if (!cdt.connected) {
this.lastError = 'Device is not connected';
throw new Error('Device is not connected');
}
}
// Returns a signed transaction ready to be relayed
CosmosDelegateTool.prototype.sign = async function (unsignedTx, txContext) {
connectedOrThrow(this);
if (typeof txContext.path === 'undefined') {
this.lastError = 'context should include the account path';
throw new Error('context should include the account path');
}
const bytesToSign = txs.getBytesToSign(unsignedTx, txContext);
console.log(bytesToSign);
const response = await this.app.sign(txContext.path, bytesToSign);
if (response.return_code !== 0x9000) {
this.lastError = response.error_message;
throw new Error(response.error_message);
}
const sig = secp256k1.signatureImport(response.signature);
return txs.applySignature(unsignedTx, txContext, sig);
};
// Retrieve public key and bech32 address
CosmosDelegateTool.prototype.retrieveAddress = async function (account, index) {
connectedOrThrow(this);
const path = [44, 118, account, 0, index];
const pk = await this.app.publicKey(path);
if (pk.return_code !== 0x9000) {
this.lastError = pk.error_message;
throw new Error(pk.error_message);
}
return {
pk: pk.compressed_pk.toString('hex'),
path,
bech32: getBech32FromPK(defaultHrp, pk.compressed_pk),
};
};
// Scan multiple address in a derivation path range (44’/118’/X/0/Y)
// eslint-disable-next-line max-len
CosmosDelegateTool.prototype.scanAddresses = async function (minAccount, maxAccount, minIndex, maxIndex) {
const answer = [];
for (let account = minAccount; account < maxAccount + 1; account += 1) {
for (let index = minIndex; index < maxIndex + 1; index += 1) {
// retrieve address cannot be called in parallel
// eslint-disable-next-line no-await-in-loop
const tmp = await this.retrieveAddress(account, index);
answer.push(tmp);
}
}
return answer;
};
CosmosDelegateTool.prototype.retrieveValidators = async function () {
const url = `${nodeURL(this)}/staking/validators`;
return axios.get(url).then((r) => {
const validators = {};
for (let i = 0; i < r.data.length; i += 1) {
const validatorData = {};
const t = r.data[i];
validatorData.tokens = Big(t.tokens);
validatorData.totalShares = Big(t.delegator_shares);
validators[t.operator_address] = validatorData;
}
return validators;
}, e => wrapError(this, e));
};
CosmosDelegateTool.prototype.getAccountInfo = async function (addr) {
const url = `${nodeURL(this)}/auth/accounts/${addr.bech32}`;
const txContext = {
sequence: '0',
accountNumber: '0',
balanceuAtom: '0',
};
return axios.get(url).then((r) => {
try {
if (typeof r.data !== 'undefined' && typeof r.data.value !== 'undefined') {
txContext.sequence = Number(r.data.value.sequence).toString();
txContext.accountNumber = Number(r.data.value.account_number).toString();
if (r.data.value.coins !== null) {
const tmp = r.data.value.coins.filter(x => x.denom === txs.DEFAULT_DENOM);
if (tmp.length > 0) {
txContext.balanceuAtom = Big(tmp[0].amount).toString();
}
}
}
} catch (e) {
console.log('Error ', e, ' returning defaults');
}
return txContext;
}, e => wrapError(this, e));
};
CosmosDelegateTool.prototype.getAccountDelegations = async function (validators, addr) {
const url = `${nodeURL(this)}/staking/delegators/${addr.bech32}/delegations`;
return axios.get(url).then((r) => {
const txContext = {
delegations: {},
delegationsTotaluAtoms: '0',
};
const delegations = {};
let totalDelegation = Big(0);
try {
if (typeof r.data !== 'undefined' && r.data !== null) {
for (let i = 0; i < r.data.length; i += 1) {
const t = r.data[i];
const valAddr = t.validator_address;
if (valAddr in validators) {
const shares = Big(t.shares);
const valData = validators[valAddr];
const valTokens = valData.tokens;
const valTotalShares = valData.totalShares;
const tokens = shares.times(valTokens).div(valTotalShares);
delegations[valAddr] = {
uatoms: tokens.toString(),
shares: shares.toString(),
};
totalDelegation = totalDelegation.add(tokens);
}
}
}
} catch (e) {
console.log('Error ', e, ' returning defaults');
}
txContext.delegations = delegations;
txContext.delegationsTotaluAtoms = totalDelegation.toString();
return txContext;
}, e => wrapError(this, e));
};
// Retrieve atom balances from the network for a list of account
// Retrieve delegated/not-delegated balances for each account
CosmosDelegateTool.prototype.retrieveBalances = async function (addressList) {
const validators = await this.retrieveValidators();
// Get all balances
const requestsBalance = addressList.map(async (addr, index) => {
const txContext = await this.getAccountInfo(addr);
return Object.assign({}, addressList[index], txContext);
});
// eslint-disable-next-line max-len,no-unused-vars
const requestsDelegations = addressList.map((addr, index) => this.getAccountDelegations(validators, addr));
// eslint-disable-next-line no-unused-vars,max-len
const balances = await Promise.all(requestsBalance);
const delegations = await Promise.all(requestsDelegations);
const reply = [];
for (let i = 0; i < addressList.length; i += 1) {
reply.push(Object.assign({}, delegations[i], balances[i]));
}
return reply;
};
// Creates a new delegation tx based on the input parameters
// this function expect that retrieve balances has been called before
CosmosDelegateTool.prototype.txCreateDelegate = async function (
txContext,
validatorBech32,
uatomAmount,
memo,
) {
if (typeof txContext === 'undefined') {
throw new Error('undefined txContext');
}
if (typeof txContext.bech32 === 'undefined') {
throw new Error('txContext does not contain the source address (bech32)');
}
const accountInfo = await this.getAccountInfo(txContext);
// eslint-disable-next-line no-param-reassign
txContext.accountNumber = accountInfo.accountNumber;
// eslint-disable-next-line no-param-reassign
txContext.sequence = accountInfo.sequence;
return txs.createDelegate(
txContext,
validatorBech32,
uatomAmount,
memo,
);
}
// Creates a new staking tx based on the input parameters
// this function expect that retrieve balances has been called before
CosmosDelegateTool.prototype.txCreateRedelegate = async function (
txContext,
validatorSourceBech32,
validatorDestBech32,
uatomAmount,
memo,
) {
if (typeof txContext === 'undefined') {
throw new Error('undefined txContext');
}
if (typeof txContext.bech32 === 'undefined') {
throw new Error('txContext does not contain the source address (bech32)');
}
const accountInfo = await this.getAccountInfo(txContext);
// eslint-disable-next-line no-param-reassign
txContext.accountNumber = accountInfo.accountNumber;
// eslint-disable-next-line no-param-reassign
txContext.sequence = accountInfo.sequence;
// Convert from uatoms to shares
return txs.createRedelegate(
txContext,
validatorSourceBech32,
validatorDestBech32,
uatomAmount,
memo,
);
};
// Creates a new undelegation tx based on the input parameters
// this function expect that retrieve balances has been called before
CosmosDelegateTool.prototype.txCreateUndelegate = async function (
txContext,
validatorBech32,
uatomAmount,
memo,
) {
if (typeof txContext === 'undefined') {
throw new Error('undefined txContext');
}
if (typeof txContext.bech32 === 'undefined') {
throw new Error('txContext does not contain the source address (bech32)');
}
const accountInfo = await this.getAccountInfo(txContext);
// eslint-disable-next-line no-param-reassign
txContext.accountNumber = accountInfo.accountNumber;
// eslint-disable-next-line no-param-reassign
txContext.sequence = accountInfo.sequence;
return txs.createUndelegate(
txContext,
validatorBech32,
uatomAmount,
memo,
);
};
// Relays a signed transaction and returns a transaction hash
CosmosDelegateTool.prototype.txSubmit = async function (signedTx) {
const txBody = {
tx: signedTx.value,
mode: 'async',
};
const url = `${nodeURL(this)}/txs`;
return axios.post(url, JSON.stringify(txBody)).then(r => r, e => wrapError(this, e));
};
// Retrieve the status of a transaction hash
CosmosDelegateTool.prototype.txStatus = async function (txHash) {
const url = `${nodeURL(this)}/txs/${txHash}`;
return axios.get(url).then(r => r.data, e => wrapError(this, e));
};
module.exports = CosmosDelegateTool;