Skip to content

Commit 936e7cd

Browse files
authored
Merge pull request #10 from Adamant-im/feature/add-delegate
Feature/add delegate methods
2 parents 8a1cbcc + 26f48c8 commit 936e7cd

File tree

6 files changed

+232
-11
lines changed

6 files changed

+232
-11
lines changed

groups/newDelegate.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
const axios = require('axios');
2+
const logger = require('../helpers/logger');
3+
const keys = require('../helpers/keys');
4+
const constants = require('../helpers/constants');
5+
const transactionFormer = require('../helpers/transactionFormer');
6+
const validator = require('../helpers/validator');
7+
8+
const DEFAULT_NEW_DELEGATE_RETRIES = 4; // How much re-tries for send tokens requests by default. Total 4+1 tries
9+
10+
module.exports = (nodeManager) => {
11+
/**
12+
* Registers user account as delegate
13+
* @param {string} passPhrase Senders's passPhrase. Sender's address will be derived from it.
14+
* @param {string} username Delegate name you want to register with.
15+
* It must be unique in ADAMANT blockchain. It should not be similar to ADAMANT address.
16+
* Delegate name can only contain alphanumeric characters and symbols !@$&_.
17+
* @param {number} maxRetries How much times to retry request
18+
* @returns {Promise} Request results
19+
*/
20+
return async (passPhrase, username, maxRetries = DEFAULT_NEW_DELEGATE_RETRIES, retryNo = 0) => {
21+
22+
let transaction;
23+
24+
try {
25+
if (!validator.validatePassPhrase(passPhrase)) {
26+
return validator.badParameter('passPhrase');
27+
}
28+
29+
const keyPair = keys.createKeypairFromPassPhrase(passPhrase);
30+
31+
if (!validator.validateDelegateName(username)) {
32+
return validator.badParameter('username');
33+
}
34+
35+
const type = constants.transactionTypes.DELEGATE;
36+
37+
const data = {
38+
type,
39+
keyPair,
40+
username,
41+
};
42+
43+
transaction = transactionFormer.createTransaction(type, data);
44+
45+
} catch (e) {
46+
return validator.badParameter('#exception_catched#', e);
47+
}
48+
49+
const url = nodeManager.node() + '/api/delegates';
50+
51+
try {
52+
const response = await axios.post(url, transaction);
53+
54+
return validator.formatRequestResults(response, true);
55+
} catch (error) {
56+
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}.`;
57+
58+
if (retryNo < maxRetries) {
59+
logger.log(`${logMessage} Retrying…`);
60+
61+
return nodeManager.changeNodes()
62+
.then(() => (
63+
module.exports(nodeManager)(passPhrase, addressOrPublicKey, amount, isAmountInADM, maxRetries, ++retryNo)
64+
));
65+
}
66+
67+
logger.warn(`${logMessage} No more attempts, returning error.`);
68+
69+
return validator.formatRequestResults(error, false);
70+
}
71+
}
72+
};

groups/voteForDelegate.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
const axios = require('axios');
2+
const get = require('./get');
3+
const logger = require('../helpers/logger');
4+
const keys = require('../helpers/keys');
5+
const constants = require('../helpers/constants');
6+
const transactionFormer = require('../helpers/transactionFormer');
7+
const validator = require('../helpers/validator');
8+
9+
const DEFAULT_VOTE_FOR_DELEGATE_RETRIES = 4; // How much re-tries for send tokens requests by default. Total 4+1 tries
10+
11+
const publicKeysCache = { };
12+
13+
module.exports = (nodeManager) => {
14+
/**
15+
* Creates votes for delegate transaction, signs it, and broadcasts to ADAMANT network
16+
* See https://github.com/Adamant-im/adamant/wiki/Transaction-Types#type-3-vote-for-delegate-transaction
17+
* @param {string} passPhrase Senders's passPhrase. Sender's address will be derived from it.
18+
* @param {string[]} votes PublicKeys, ADM addresses and delegate names for upvote and downvote.
19+
* It would be more efficient to pass publicKey, otherwise the api will make additional queries
20+
* @param {number} maxRetries How much times to retry request
21+
* @returns {Promise} Request results
22+
*/
23+
return async (passPhrase, votes, maxRetries = DEFAULT_VOTE_FOR_DELEGATE_RETRIES, retryNo = 0) => {
24+
25+
let transaction;
26+
27+
try {
28+
if (!validator.validatePassPhrase(passPhrase)) {
29+
return validator.badParameter('passPhrase');
30+
}
31+
32+
const keyPair = keys.createKeypairFromPassPhrase(passPhrase);
33+
34+
const uniqueVotes = [];
35+
36+
for (let i = votes.length - 1; i >= 0; i--) {
37+
const vote = votes[i];
38+
const voteName = vote.slice(1);
39+
const voteDirection = vote.charAt(0);
40+
41+
const cachedPublicKey = publicKeysCache[voteName];
42+
43+
if (cachedPublicKey) {
44+
votes[i] = `${voteDirection}${cachedPublicKey}`;
45+
} else {
46+
if (validator.validateAdmVoteForAddress(vote)) {
47+
const res = await get(nodeManager)('/accounts', { address: voteName });
48+
49+
if (res.success) {
50+
const publicKey = res.data.account.publicKey;
51+
52+
votes[i] = `${voteDirection}${publicKey}`;
53+
publicKeysCache[voteName] = publicKey;
54+
} else {
55+
logger.warn(`[ADAMANT js-api] Failed to get public key for ${vote}. ${res.errorMessage}.`);
56+
57+
return validator.badParameter('votes');
58+
}
59+
} else if (validator.validateAdmVoteForDelegateName(vote)) {
60+
const res = await get(nodeManager)('/delegates/get', { username: voteName });
61+
62+
if (res.success) {
63+
const publicKey = res.data.delegate.publicKey;
64+
65+
votes[i] = `${voteDirection}${publicKey}`;
66+
publicKeysCache[voteName] = publicKey;
67+
} else {
68+
logger.warn(`[ADAMANT js-api] Failed to get public key for ${vote}. ${res.errorMessage}.`);
69+
70+
return validator.badParameter('votes');
71+
}
72+
} else if (!validator.validateAdmVoteForPublicKey(vote)) {
73+
return validator.badParameter('votes');
74+
}
75+
}
76+
77+
// Exclude duplicates
78+
const foundCopy = uniqueVotes.find((v) => v.slice(1) === votes[i].slice(1));
79+
80+
if (!foundCopy) {
81+
uniqueVotes.push(votes[i]);
82+
}
83+
}
84+
85+
const type = constants.transactionTypes.VOTE;
86+
87+
const data = {
88+
type,
89+
keyPair,
90+
votes: uniqueVotes,
91+
};
92+
93+
transaction = transactionFormer.createTransaction(type, data);
94+
} catch (error) {
95+
return validator.badParameter('#exception_catched#', error)
96+
}
97+
98+
const url = nodeManager.node() + '/api/accounts/delegates';
99+
100+
try {
101+
const response = await axios.post(url, transaction);
102+
103+
return validator.formatRequestResults(response, true);
104+
} catch(error) {
105+
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}.`;
106+
107+
if (retryNo < maxRetries) {
108+
logger.log(`${logMessage} Retrying…`);
109+
110+
return nodeManager.changeNodes()
111+
.then(() => (
112+
module.exports(nodeManager)(passPhrase, addressOrPublicKey, amount, isAmountInADM, maxRetries, ++retryNo)
113+
));
114+
}
115+
116+
logger.warn(`${logMessage} No more attempts, returning error.`);
117+
118+
return validator.formatRequestResults(error, false);
119+
}
120+
}
121+
};

helpers/constants.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ module.exports = {
3131
RE_HEX: /^[a-fA-F0-9]+$/,
3232
RE_BASE64: /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/,
3333
RE_ADM_ADDRESS: /^U([0-9]{6,})$/,
34+
RE_ADM_VOTE_FOR_PUBLIC_KEY: /^(\+|-)[a-fA-F0-9]{64}$/,
35+
RE_ADM_VOTE_FOR_ADDRESS: /^(\+|-)U([0-9]{6,})$/,
36+
RE_ADM_VOTE_FOR_DELEGATE_NAME: /^(\+|-)([a-z0-9!@$&_]{1,20})$/,
37+
RE_ADM_DELEGATE_NAME: /^[a-z0-9!@$&_]{1,20}$/,
3438
RE_BTC_ADDRESS: /^(bc1|[13])[a-km-zA-HJ-NP-Z02-9]{25,39}$/,
3539
RE_DASH_ADDRESS: /^[7X][1-9A-HJ-NP-Za-km-z]{33,}$/,
3640
RE_DOGE_ADDRESS: /^[A|D|9][A-Z0-9]([0-9a-zA-Z]{9,})$/,
3741
RE_LSK_ADDRESS: /^[0-9]{2,21}L$/
38-
42+
3943
}

helpers/validator.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ module.exports = {
5252
return true
5353
},
5454

55+
validateAdmVoteForPublicKey(publicKey) {
56+
return (publicKey && typeof(publicKey) === 'string' && constants.RE_ADM_VOTE_FOR_PUBLIC_KEY.test(publicKey));
57+
},
58+
59+
validateAdmVoteForAddress(address) {
60+
return (address && typeof(address) === 'string' && constants.RE_ADM_VOTE_FOR_ADDRESS.test(address));
61+
},
62+
63+
validateAdmVoteForDelegateName(delegateName) {
64+
return (delegateName && typeof(delegateName) === 'string' && constants.RE_ADM_VOTE_FOR_DELEGATE_NAME.test(delegateName));
65+
},
66+
5567
validateIntegerAmount(amount) {
5668
if (!amount || typeof(amount) !== 'number' || isNaN(amount) || !Number.isSafeInteger(amount))
5769
return false
@@ -84,24 +96,24 @@ module.exports = {
8496

8597
let json = this.tryParseJSON(message)
8698

87-
if (!json)
99+
if (!json)
88100
return {
89101
result: false,
90102
error: `For rich and signal messages, 'message' must be a JSON string`
91103
}
92-
104+
93105
if (json.type && json.type.toLowerCase().includes('_transaction'))
94106
if (json.type.toLowerCase() !== json.type)
95107
return {
96108
result: false,
97109
error: `Value '<coin>_transaction' must be in lower case`
98110
}
99-
111+
100112
if (typeof json.amount !== 'string' || !this.validateStringAmount(json.amount))
101113
return {
102114
result: false,
103115
error: `Field 'amount' must be a string, representing a number`
104-
}
116+
}
105117

106118
}
107119
}
@@ -110,6 +122,14 @@ module.exports = {
110122
}
111123
},
112124

125+
validateDelegateName(name) {
126+
if (typeof name !== 'string') {
127+
return false;
128+
}
129+
130+
return constants.RE_ADM_DELEGATE_NAME.test(name);
131+
},
132+
113133
AdmToSats(amount) {
114134
return BigNumber(String(amount)).multipliedBy(constants.SAT).integerValue().toNumber()
115135
},

index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const constants = require('./helpers/constants.js');
22
const get = require('./groups/get');
33
const getPublicKey = require('./groups/getPublicKey');
44
const decodeMsg = require('./groups/decodeMsg');
5+
const newDelegate = require('./groups/newDelegate');
6+
const voteForDelegate = require('./groups/voteForDelegate');
57
const sendTokens = require('./groups/sendTokens');
68
const sendMessage = require('./groups/sendMessage');
79
const healthCheck = require('./helpers/healthCheck');
@@ -19,12 +21,14 @@ module.exports = (params, log) => {
1921
log = log || console;
2022
logger.initLogger(params.logLevel, log);
2123
const nodeManager = healthCheck(params.node);
22-
24+
2325
return {
2426
get: get(nodeManager),
2527
getPublicKey: getPublicKey(nodeManager),
2628
sendTokens: sendTokens(nodeManager),
2729
sendMessage: sendMessage(nodeManager),
30+
newDelegate: newDelegate(nodeManager),
31+
voteForDelegate: voteForDelegate(nodeManager),
2832
decodeMsg,
2933
eth,
3034
dash,

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "adamant-api",
3-
"version": "1.3.0",
3+
"version": "1.4.0",
44
"description": "REST API for ADAMANT Blockchain",
55
"main": "index.js",
66
"scripts": {
@@ -9,14 +9,14 @@
99
"author": "RomanS, Aleksei Lebedev <devs@adamant.im> (https://adamant.im)",
1010
"license": "GPL-3.0",
1111
"dependencies": {
12-
"axios": "^0.23.0",
13-
"bignumber.js": "^9.0.1",
12+
"axios": "^0.25.0",
13+
"bignumber.js": "^9.0.2",
1414
"bitcoinjs-lib": "^5.2.0",
15-
"bitcore-mnemonic": "^8.25.10",
15+
"bitcore-mnemonic": "^8.25.25",
1616
"bytebuffer": "^5.0.1",
1717
"coininfo": "^5.1.0",
1818
"ed2curve": "^0.3.0",
19-
"ethereumjs-util": "^7.1.3",
19+
"ethereumjs-util": "^7.1.4",
2020
"hdkey": "^2.0.1",
2121
"socket.io-client": "^2.4.0",
2222
"sodium-browserify-tweetnacl": "^0.2.6"

0 commit comments

Comments
 (0)