UNPKG

javalon

Version:

javascript api for the avalon blockchain

459 lines (442 loc) 15.8 kB
const CryptoJS = require('crypto-js') const eccrypto = require('eccrypto') const randomBytes = require('randombytes') const secp256k1 = require('secp256k1') const bs58 = require('bs58') const GrowInt = require('growint') const fetch = require('node-fetch') const bip39 = require('bip39') const bip32 = require('bip32') let avalon = { config: { api: ['https://avalon.d.tube'], bwGrowth: 10000000, vtGrowth: 360000000 }, init: (config) => { avalon.config = Object.assign(avalon.config,config) }, getBlockchainHeight: (cb) => { avalon.get('/count',cb) }, getBlock: (number, cb) => { avalon.get('/block/'+number,cb) }, getAccount: (name, cb) => { avalon.get('/account/'+name,cb) }, getAccountHistory: (name, lastBlock, cb) => { avalon.get('/history/'+name+'/'+lastBlock,cb) }, getVotesByAccount: (name, lastTs, cb) => { avalon.get('/votes/all/'+name+'/'+lastTs,cb) }, getPendingVotesByAccount: (name, lastTs, cb) => { avalon.get('/votes/pending/'+name+'/'+lastTs,cb) }, getClaimableVotesByAccount: (name, lastTs, cb) => { avalon.get('/votes/claimable/'+name+'/'+lastTs,cb) }, getClaimedVotesByAccount: (name, lastTs, cb) => { avalon.get('/votes/claimed/'+name+'/'+lastTs,cb) }, getAccounts: (names, cb) => { avalon.get('/accounts/'+names.join(','),cb) }, getContent: (name, link, cb) => { avalon.get('/content/'+name+'/'+link,cb) }, getFollowing: (name, cb) => { avalon.get('/follows/'+name,cb) }, getFollowers: (name, cb) => { avalon.get('/followers/'+name,cb) }, getPendingRewards: (name, cb) => { avalon.get('/rewards/pending/'+name,cb) }, getClaimedRewards: (name, cb) => { avalon.get('/rewards/claimed/'+name,cb) }, getClaimableRewards: (name, cb) => { avalon.get('/rewards/claimable/'+name,cb) }, generateCommentTree: (root, author, link) => { var replies = [] var content = null if (author === root.author && link === root.link) content = root else content = root.comments[author+'/'+link] if (!content || !content.child || !root.comments) return [] for (var i = 0; i < content.child.length; i++) { var comment = root.comments[content.child[i][0]+'/'+content.child[i][1]] comment.replies = avalon.generateCommentTree(root, comment.author, comment.link) comment.ups = 0 comment.downs = 0 if (comment.votes) for (let i = 0; i < comment.votes.length; i++) { if (comment.votes[i].vt > 0) comment.ups += comment.votes[i].vt if (comment.votes[i].vt < 0) comment.downs -= comment.votes[i].vt } comment.totals = comment.ups - comment.downs replies.push(comment) } replies = replies.sort(function(a,b) { return b.totals-a.totals }) return replies }, getDiscussionsByAuthor: (username, author, link, cb) => { if (!author && !link) avalon.get('/blog/'+username,cb) else avalon.get('/blog/'+username+'/'+author+'/'+link,cb) }, getNewDiscussions: (author, link, cb) => { if (!author && !link) avalon.get('/new',cb) else avalon.get('/new/'+author+'/'+link,cb) }, getHotDiscussions: (author, link, cb) => { if (!author && !link) avalon.get('/hot',cb) else avalon.get('/hot/'+author+'/'+link,cb) }, getTrendingDiscussions: (author, link, cb) => { if (!author && !link) avalon.get('/trending',cb) else avalon.get('/trending/'+author+'/'+link,cb) }, getFeedDiscussions: (username, author, link, cb) => { if (!author && !link) avalon.get('/feed/'+username,cb) else avalon.get('/feed/'+username+'/'+author+'/'+link,cb) }, getNotifications: (username, cb) => { avalon.get('/notifications/'+username,cb) }, getSchedule: (cb) => { avalon.get('/schedule',cb) }, getSupply: (cb) => { avalon.get('/supply',cb) }, getLeaders: (cb) => { avalon.get('/allminers',cb) }, getRewardPool: (cb) => { avalon.get('/rewardpool',cb) }, getRewards: (name, cb) => { avalon.get('/distributed/'+name,cb) }, get: (method,cb) => { fetch(avalon.randomNode()+method, { method: 'get', headers: { 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/json' } }).then(res => res.json()).then(function(res) { cb(null, res) }).catch(function(error) { cb(error) }) }, keypair: () => { let priv, pub do { priv = Buffer.from(randomBytes(32).buffer) pub = secp256k1.publicKeyCreate(priv) } while (!secp256k1.privateKeyVerify(priv)) return { pub: bs58.encode(pub), priv: bs58.encode(priv) } }, generateMnemonic: () => { return bip39.generateMnemonic() }, mnemonicToKeyPair: (mnemonic) => { const seed = bip39.mnemonicToSeedSync(mnemonic) const bip32key = bip32.fromSeed(seed) return { pub: bs58.encode(bip32key.publicKey), priv: bs58.encode(bip32key.privateKey) } }, privToPub: (priv) => { return bs58.encode( secp256k1.publicKeyCreate( bs58.decode(priv))) }, sign: (privKey, sender, tx) => { if (typeof tx !== 'object') try { tx = JSON.parse(tx) } catch(e) { console.log('invalid transaction') return } tx.sender = sender // add timestamp to seed the hash (avoid transactions reuse) tx.ts = new Date().getTime() // hash the transaction tx.hash = CryptoJS.SHA256(JSON.stringify(tx)).toString() // sign the transaction let signature = secp256k1.ecdsaSign(Buffer.from(tx.hash, 'hex'), bs58.decode(privKey)) tx.signature = bs58.encode(signature.signature) return tx }, signMultisig: (privKeys = [], sender, tx) => { if (typeof tx !== 'object') try { tx = JSON.parse(tx) } catch(e) { console.log('invalid transaction') return } if (!tx.sender) tx.sender = sender if (!tx.ts) tx.ts = new Date().getTime() if (!tx.hash) tx.hash = CryptoJS.SHA256(JSON.stringify(tx)).toString() if (!tx.signature || !Array.isArray(tx.signature)) tx.signature = [] for (let k in privKeys) { let sign = secp256k1.ecdsaSign(Buffer.from(tx.hash, 'hex'), bs58.decode(privKeys[k])) tx.signature.push([bs58.encode(sign.signature),sign.recid]) } return tx }, sendTransaction: (tx, cb) => { // sends a transaction to a node // waits for the transaction to be included in a block // 200 with head block number if confirmed // 408 if timeout // 500 with error if transaction is invalid fetch(avalon.randomNode()+'/transactWaitConfirm', { method: 'post', headers: { 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/json' }, body: JSON.stringify(tx) }).then(function(res) { if (res.status === 500 || res.status === 408) res.json().then(function(err) { cb(err) }) else if (res.status === 404) cb({error: 'Avalon API is down'}) else res.text().then(function(headBlock) { cb(null, parseInt(headBlock)) }) }) }, sendRawTransaction: (tx, cb) => { // sends the transaction to a node // 200 with head block number if transaction is valid and node added it to mempool // 500 with error if transaction is invalid fetch(avalon.randomNode()+'/transact', { method: 'post', headers: { 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/json' }, body: JSON.stringify(tx) }).then(function(res) { if (res.status === 500) res.json().then(function(err) { cb(err) }) else res.text().then(function(headBlock) { cb(null, parseInt(headBlock)) }) }) }, sendTransactionDeprecated: (tx, cb) => { // old and bad way of checking if a transaction is confirmed in a block avalon.sendRawTransaction(tx, function(error, headBlock) { if (error) cb(error) else setTimeout(function() { avalon.verifyTransaction(tx, headBlock, 5, function(error, block) { if (error) console.log(error) else cb(null, block) }) }, 1500) }) }, verifyTransaction: (tx, headBlock, retries, cb) => { var nextBlock = headBlock+1 fetch(avalon.randomNode()+'/block/'+nextBlock, { method: 'get', headers: { 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/json' } }).then(res => res.text()).then(function(text) { try { var block = JSON.parse(text) } catch (error) { // block is not yet available, retrying in 1.5 secs if (retries <= 0) return retries-- setTimeout(function(){avalon.verifyTransaction(tx, headBlock, retries, cb)}, 1500) return } var isConfirmed = false for (let i = 0; i < block.txs.length; i++) if (block.txs[i].hash === tx.hash) { isConfirmed = true break } if (isConfirmed) cb(null, block) else if (retries > 0) { retries-- setTimeout(function(){avalon.verifyTransaction(tx, nextBlock, retries, cb)},3000) } else cb('Failed to find transaction up to block #'+nextBlock) }) }, encrypt: (pub, message, ephemPriv, cb) => { // if no ephemPriv is passed, a new random key is generated if (!cb) { cb = ephemPriv ephemPriv = avalon.keypair().priv } try { if (ephemPriv) ephemPriv = bs58.decode(ephemPriv) var pubBuffer = bs58.decode(pub) eccrypto.encrypt(pubBuffer, Buffer.from(message), { ephemPrivateKey: ephemPriv }).then(function(encrypted) { // reducing the encrypted buffers into base 58 encrypted.iv = bs58.encode(encrypted.iv) // compress the sender's public key to compressed format // shortens the encrypted string length encrypted.ephemPublicKey = secp256k1.publicKeyConvert(encrypted.ephemPublicKey, true) encrypted.ephemPublicKey = bs58.encode(encrypted.ephemPublicKey) encrypted.ciphertext = bs58.encode(encrypted.ciphertext) encrypted.mac = bs58.encode(encrypted.mac) encrypted = [ encrypted.iv, encrypted.ephemPublicKey, encrypted.ciphertext, encrypted.mac ] // adding the _ separator character encrypted = encrypted.join('_') cb(null, encrypted) }).catch(function(error) { cb(error) }) } catch (error) { cb(error) } }, decrypt: (priv, encrypted, cb) => { try { // converting the encrypted string to an array of base58 encoded strings encrypted = encrypted.split('_') // then to an object with the correct property names var encObj = {} encObj.iv = bs58.decode(encrypted[0]) encObj.ephemPublicKey = bs58.decode(encrypted[1]) encObj.ephemPublicKey = secp256k1.publicKeyConvert(encObj.ephemPublicKey, false) encObj.ciphertext = bs58.decode(encrypted[2]) encObj.mac = bs58.decode(encrypted[3]) // and we decode it with our private key var privBuffer = bs58.decode(priv) eccrypto.decrypt(privBuffer, encObj).then(function(decrypted) { cb(null, decrypted.toString()) }).catch(function(error) { cb(error) }) } catch (error) { cb(error) } }, randomNode: () => { var nodes = avalon.config.api if (typeof nodes === 'string') return nodes else return nodes[Math.floor(Math.random()*nodes.length)] }, availableBalance: (account) => { if (!account.voteLock) return account.balance let newLock = 0 for (let v in account.proposalVotes) if (account.proposalVotes[v].end > new Date().getTime() && account.proposalVotes[v].amount - account.proposalVotes[v].bonus > newLock) newLock = account.proposalVotes[v].amount - account.proposalVotes[v].bonus return account.balance - newLock }, votingPower: (account) => { return new GrowInt(account.vt, { growth:account.balance/(avalon.config.vtGrowth), max: account.maxVt }).grow(new Date().getTime()).v }, bandwidth: (account) => { return new GrowInt(account.bw, {growth: Math.max(account.baseBwGrowth || 0,account.balance)/(avalon.config.bwGrowth), max:256000}) .grow(new Date().getTime()).v }, TransactionType: { NEW_ACCOUNT: 0, APPROVE_NODE_OWNER: 1, DISAPROVE_NODE_OWNER: 2, TRANSFER: 3, COMMENT: 4, VOTE: 5, USER_JSON: 6, FOLLOW: 7, UNFOLLOW: 8, // RESHARE: 9, // not sure NEW_KEY: 10, REMOVE_KEY: 11, CHANGE_PASSWORD: 12, PROMOTED_COMMENT: 13, TRANSFER_VT: 14, TRANSFER_BW: 15, LIMIT_VT: 16, CLAIM_REWARD: 17, ENABLE_NODE: 18, TIPPED_VOTE: 19, NEW_WEIGHTED_KEY: 20, SET_SIG_THRESHOLD: 21, SET_PASSWORD_WEIGHT: 22, UNSET_SIG_THRESHOLD: 23, NEW_ACCOUNT_WITH_BW: 24, PLAYLIST_JSON: 25, PLAYLIST_PUSH: 26, PLAYLIST_POP: 27, COMMENT_EDIT: 28, ACCOUNT_AUTHORIZE: 29, ACCOUNT_REVOKE: 30, FUND_REQUEST_CREATE: 31, FUND_REQUEST_CONTRIB: 32, FUND_REQUEST_WORK: 33, FUND_REQUEST_WORK_REVIEW: 34, PROPOSAL_VOTE: 35, PROPOSAL_EDIT: 36, CHAIN_UPDATE_CREATE: 37, MD_QUEUE: 38, MD_SIGN: 39 } } if (typeof window != 'undefined') window.javalon = avalon module.exports = avalon