Skip to content

Commit

Permalink
Merge pull request #10 from Adamant-im/feature/add-delegate
Browse files Browse the repository at this point in the history
Feature/add delegate methods
  • Loading branch information
martiliones committed Feb 12, 2022
2 parents 8a1cbcc + 26f48c8 commit 936e7cd
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 11 deletions.
72 changes: 72 additions & 0 deletions groups/newDelegate.js
@@ -0,0 +1,72 @@
const axios = require('axios');
const logger = require('../helpers/logger');
const keys = require('../helpers/keys');
const constants = require('../helpers/constants');
const transactionFormer = require('../helpers/transactionFormer');
const validator = require('../helpers/validator');

const DEFAULT_NEW_DELEGATE_RETRIES = 4; // How much re-tries for send tokens requests by default. Total 4+1 tries

module.exports = (nodeManager) => {
/**
* Registers user account as delegate
* @param {string} passPhrase Senders's passPhrase. Sender's address will be derived from it.
* @param {string} username Delegate name you want to register with.
* It must be unique in ADAMANT blockchain. It should not be similar to ADAMANT address.
* Delegate name can only contain alphanumeric characters and symbols !@$&_.
* @param {number} maxRetries How much times to retry request
* @returns {Promise} Request results
*/
return async (passPhrase, username, maxRetries = DEFAULT_NEW_DELEGATE_RETRIES, retryNo = 0) => {

let transaction;

try {
if (!validator.validatePassPhrase(passPhrase)) {
return validator.badParameter('passPhrase');
}

const keyPair = keys.createKeypairFromPassPhrase(passPhrase);

if (!validator.validateDelegateName(username)) {
return validator.badParameter('username');
}

const type = constants.transactionTypes.DELEGATE;

const data = {
type,
keyPair,
username,
};

transaction = transactionFormer.createTransaction(type, data);

} catch (e) {
return validator.badParameter('#exception_catched#', e);
}

const url = nodeManager.node() + '/api/delegates';

try {
const response = await axios.post(url, transaction);

return validator.formatRequestResults(response, true);
} catch (error) {
const logMessage = `[ADAMANT js-api] New delegate request: Request to ${url} failed with ${error.response ? error.response.status : undefined} status code, ${error.toString()}${error.response && error.response.data ? '. Message: ' + error.response.data.toString().trim() : ''}. Try ${retryNo+1} of ${maxRetries+1}.`;

if (retryNo < maxRetries) {
logger.log(`${logMessage} Retrying…`);

return nodeManager.changeNodes()
.then(() => (
module.exports(nodeManager)(passPhrase, addressOrPublicKey, amount, isAmountInADM, maxRetries, ++retryNo)
));
}

logger.warn(`${logMessage} No more attempts, returning error.`);

return validator.formatRequestResults(error, false);
}
}
};
121 changes: 121 additions & 0 deletions groups/voteForDelegate.js
@@ -0,0 +1,121 @@
const axios = require('axios');
const get = require('./get');
const logger = require('../helpers/logger');
const keys = require('../helpers/keys');
const constants = require('../helpers/constants');
const transactionFormer = require('../helpers/transactionFormer');
const validator = require('../helpers/validator');

const DEFAULT_VOTE_FOR_DELEGATE_RETRIES = 4; // How much re-tries for send tokens requests by default. Total 4+1 tries

const publicKeysCache = { };

module.exports = (nodeManager) => {
/**
* Creates votes for delegate transaction, signs it, and broadcasts to ADAMANT network
* See https://github.com/Adamant-im/adamant/wiki/Transaction-Types#type-3-vote-for-delegate-transaction
* @param {string} passPhrase Senders's passPhrase. Sender's address will be derived from it.
* @param {string[]} votes PublicKeys, ADM addresses and delegate names for upvote and downvote.
* It would be more efficient to pass publicKey, otherwise the api will make additional queries
* @param {number} maxRetries How much times to retry request
* @returns {Promise} Request results
*/
return async (passPhrase, votes, maxRetries = DEFAULT_VOTE_FOR_DELEGATE_RETRIES, retryNo = 0) => {

let transaction;

try {
if (!validator.validatePassPhrase(passPhrase)) {
return validator.badParameter('passPhrase');
}

const keyPair = keys.createKeypairFromPassPhrase(passPhrase);

const uniqueVotes = [];

for (let i = votes.length - 1; i >= 0; i--) {
const vote = votes[i];
const voteName = vote.slice(1);
const voteDirection = vote.charAt(0);

const cachedPublicKey = publicKeysCache[voteName];

if (cachedPublicKey) {
votes[i] = `${voteDirection}${cachedPublicKey}`;
} else {
if (validator.validateAdmVoteForAddress(vote)) {
const res = await get(nodeManager)('/accounts', { address: voteName });

if (res.success) {
const publicKey = res.data.account.publicKey;

votes[i] = `${voteDirection}${publicKey}`;
publicKeysCache[voteName] = publicKey;
} else {
logger.warn(`[ADAMANT js-api] Failed to get public key for ${vote}. ${res.errorMessage}.`);

return validator.badParameter('votes');
}
} else if (validator.validateAdmVoteForDelegateName(vote)) {
const res = await get(nodeManager)('/delegates/get', { username: voteName });

if (res.success) {
const publicKey = res.data.delegate.publicKey;

votes[i] = `${voteDirection}${publicKey}`;
publicKeysCache[voteName] = publicKey;
} else {
logger.warn(`[ADAMANT js-api] Failed to get public key for ${vote}. ${res.errorMessage}.`);

return validator.badParameter('votes');
}
} else if (!validator.validateAdmVoteForPublicKey(vote)) {
return validator.badParameter('votes');
}
}

// Exclude duplicates
const foundCopy = uniqueVotes.find((v) => v.slice(1) === votes[i].slice(1));

if (!foundCopy) {
uniqueVotes.push(votes[i]);
}
}

const type = constants.transactionTypes.VOTE;

const data = {
type,
keyPair,
votes: uniqueVotes,
};

transaction = transactionFormer.createTransaction(type, data);
} catch (error) {
return validator.badParameter('#exception_catched#', error)
}

const url = nodeManager.node() + '/api/accounts/delegates';

try {
const response = await axios.post(url, transaction);

return validator.formatRequestResults(response, true);
} catch(error) {
const logMessage = `[ADAMANT js-api] Vote for delegate request: Request to ${url} failed with ${error.response ? error.response.status : undefined} status code, ${error.toString()}${error.response && error.response.data ? '. Message: ' + error.response.data.toString().trim() : ''}. Try ${retryNo+1} of ${maxRetries+1}.`;

if (retryNo < maxRetries) {
logger.log(`${logMessage} Retrying…`);

return nodeManager.changeNodes()
.then(() => (
module.exports(nodeManager)(passPhrase, addressOrPublicKey, amount, isAmountInADM, maxRetries, ++retryNo)
));
}

logger.warn(`${logMessage} No more attempts, returning error.`);

return validator.formatRequestResults(error, false);
}
}
};
6 changes: 5 additions & 1 deletion helpers/constants.js
Expand Up @@ -31,9 +31,13 @@ module.exports = {
RE_HEX: /^[a-fA-F0-9]+$/,
RE_BASE64: /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/,
RE_ADM_ADDRESS: /^U([0-9]{6,})$/,
RE_ADM_VOTE_FOR_PUBLIC_KEY: /^(\+|-)[a-fA-F0-9]{64}$/,
RE_ADM_VOTE_FOR_ADDRESS: /^(\+|-)U([0-9]{6,})$/,
RE_ADM_VOTE_FOR_DELEGATE_NAME: /^(\+|-)([a-z0-9!@$&_]{1,20})$/,
RE_ADM_DELEGATE_NAME: /^[a-z0-9!@$&_]{1,20}$/,
RE_BTC_ADDRESS: /^(bc1|[13])[a-km-zA-HJ-NP-Z02-9]{25,39}$/,
RE_DASH_ADDRESS: /^[7X][1-9A-HJ-NP-Za-km-z]{33,}$/,
RE_DOGE_ADDRESS: /^[A|D|9][A-Z0-9]([0-9a-zA-Z]{9,})$/,
RE_LSK_ADDRESS: /^[0-9]{2,21}L$/

}
28 changes: 24 additions & 4 deletions helpers/validator.js
Expand Up @@ -52,6 +52,18 @@ module.exports = {
return true
},

validateAdmVoteForPublicKey(publicKey) {
return (publicKey && typeof(publicKey) === 'string' && constants.RE_ADM_VOTE_FOR_PUBLIC_KEY.test(publicKey));
},

validateAdmVoteForAddress(address) {
return (address && typeof(address) === 'string' && constants.RE_ADM_VOTE_FOR_ADDRESS.test(address));
},

validateAdmVoteForDelegateName(delegateName) {
return (delegateName && typeof(delegateName) === 'string' && constants.RE_ADM_VOTE_FOR_DELEGATE_NAME.test(delegateName));
},

validateIntegerAmount(amount) {
if (!amount || typeof(amount) !== 'number' || isNaN(amount) || !Number.isSafeInteger(amount))
return false
Expand Down Expand Up @@ -84,24 +96,24 @@ module.exports = {

let json = this.tryParseJSON(message)

if (!json)
if (!json)
return {
result: false,
error: `For rich and signal messages, 'message' must be a JSON string`
}

if (json.type && json.type.toLowerCase().includes('_transaction'))
if (json.type.toLowerCase() !== json.type)
return {
result: false,
error: `Value '<coin>_transaction' must be in lower case`
}

if (typeof json.amount !== 'string' || !this.validateStringAmount(json.amount))
return {
result: false,
error: `Field 'amount' must be a string, representing a number`
}
}

}
}
Expand All @@ -110,6 +122,14 @@ module.exports = {
}
},

validateDelegateName(name) {
if (typeof name !== 'string') {
return false;
}

return constants.RE_ADM_DELEGATE_NAME.test(name);
},

AdmToSats(amount) {
return BigNumber(String(amount)).multipliedBy(constants.SAT).integerValue().toNumber()
},
Expand Down
6 changes: 5 additions & 1 deletion index.js
Expand Up @@ -2,6 +2,8 @@ const constants = require('./helpers/constants.js');
const get = require('./groups/get');
const getPublicKey = require('./groups/getPublicKey');
const decodeMsg = require('./groups/decodeMsg');
const newDelegate = require('./groups/newDelegate');
const voteForDelegate = require('./groups/voteForDelegate');
const sendTokens = require('./groups/sendTokens');
const sendMessage = require('./groups/sendMessage');
const healthCheck = require('./helpers/healthCheck');
Expand All @@ -19,12 +21,14 @@ module.exports = (params, log) => {
log = log || console;
logger.initLogger(params.logLevel, log);
const nodeManager = healthCheck(params.node);

return {
get: get(nodeManager),
getPublicKey: getPublicKey(nodeManager),
sendTokens: sendTokens(nodeManager),
sendMessage: sendMessage(nodeManager),
newDelegate: newDelegate(nodeManager),
voteForDelegate: voteForDelegate(nodeManager),
decodeMsg,
eth,
dash,
Expand Down
10 changes: 5 additions & 5 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "adamant-api",
"version": "1.3.0",
"version": "1.4.0",
"description": "REST API for ADAMANT Blockchain",
"main": "index.js",
"scripts": {
Expand All @@ -9,14 +9,14 @@
"author": "RomanS, Aleksei Lebedev <devs@adamant.im> (https://adamant.im)",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.23.0",
"bignumber.js": "^9.0.1",
"axios": "^0.25.0",
"bignumber.js": "^9.0.2",
"bitcoinjs-lib": "^5.2.0",
"bitcore-mnemonic": "^8.25.10",
"bitcore-mnemonic": "^8.25.25",
"bytebuffer": "^5.0.1",
"coininfo": "^5.1.0",
"ed2curve": "^0.3.0",
"ethereumjs-util": "^7.1.3",
"ethereumjs-util": "^7.1.4",
"hdkey": "^2.0.1",
"socket.io-client": "^2.4.0",
"sodium-browserify-tweetnacl": "^0.2.6"
Expand Down

0 comments on commit 936e7cd

Please sign in to comment.