From 8c14aa4e25f0acc9578b7b44cacce8c28be1df5a Mon Sep 17 00:00:00 2001 From: Sunny Gleason Date: Tue, 30 Apr 2019 11:33:45 -0400 Subject: [PATCH] WIP: account balance multi-get API (#77) * feat: account balance multi-get API * feat: remove console logs * feat: remove console log straggler * chore: lint * feat: implement multi program ids * chore: lint * Finish v0.14 upgrade (#1) * Revert "fix: adapt to insruction.key field changes" This reverts commit a62f016017d9e7bf872e8e8dbec5f4d4581b515c. * fix: Restore instruction length check * fix: temporary fix, adapt to insruction.key field changes --- api/api.js | 52 ++++++++++++++++++++++++++++++++++++++++++- api/inbound-stream.js | 13 ++++++----- src/App.js | 32 +++++++++++++------------- src/BxDataTable.jsx | 28 ++++++++++++++++++----- 4 files changed, 96 insertions(+), 29 deletions(-) diff --git a/api/api.js b/api/api.js index b826566c..0103abce 100644 --- a/api/api.js +++ b/api/api.js @@ -6,19 +6,25 @@ import express from 'express'; import nocache from 'nocache'; import cors from 'cors'; +import expressWs from 'express-ws'; import {promisify} from 'util'; import redis from 'redis'; import WebSocket from 'ws'; import _ from 'lodash'; import './inbound-stream'; -import expressWs from 'express-ws'; import geoip from 'geoip-lite'; import YAML from 'yaml'; import fs from 'fs'; import assert from 'assert'; +import * as solanaWeb3 from '@solana/web3.js'; import config from './config'; +// +// FIXME: make configurable +// +let FULLNODE_URL = 'http://localhost:8899'; + const app = express(); const port = 3001; @@ -369,4 +375,48 @@ app.get('/search/:id', (req, res) => { sendSearchResults(req, res); }); +function sendAccountResult(req, res) { + if (!req.params.ids) { + // give up + res.status(404).send('{"error":"not_found"}\n'); + return; + } + + try { + let idsStr = req.params.ids; + let ids = idsStr.split(','); + + let thePromises = _.map(ids, id => { + return new Promise(resolve => { + const connection = new solanaWeb3.Connection(FULLNODE_URL); + return connection + .getBalance(new solanaWeb3.PublicKey(id)) + .then(balance => { + return resolve({id: id, balance: balance}); + }); + }); + }); + + return Promise.all(thePromises).then(values => { + let consolidated = _.reduce( + values, + (a, v) => { + a[v.id] = v.balance; + return a; + }, + {}, + ); + + res.send(JSON.stringify(consolidated) + '\n'); + }); + } catch (err) { + res.status(500).send(`{"error":"server_error","err":"${err}"}\n`); + return; + } +} + +app.get('/accts_bal/:ids', (req, res) => { + sendAccountResult(req, res); +}); + app.listen(port, () => console.log(`Listening on port ${port}!`)); diff --git a/api/inbound-stream.js b/api/inbound-stream.js index cb6a0ef1..bd593d63 100644 --- a/api/inbound-stream.js +++ b/api/inbound-stream.js @@ -50,7 +50,7 @@ class BridgeFn { let inst = {}; inst.keys = _.map(y.keys, z => { - return `${z.pubkey.toBase58()} (is_signer=${z.isSigner})`; + return z.pubkey.toBase58(); }); inst.program_id = y.programId.toBase58(); inst.data = b58e(y.data); @@ -219,29 +219,30 @@ class RedisHandler { }, ]); - // append block hexHeight:dt:id to timeline let txnMsg = [ message.h, message.l, message.s, message.dt, message.hash, + tx.id, ]; if (tx.instructions.length > 0) { - txnMsg.push(tx.instructions[0].program_id); - txnMsg.push(tx.instructions[0].keys.join(',')); + let txInst = _.map(tx.instructions, i => { + return [i.program_id, i.keys.join(','), i.data].join('@'); + }).join('|'); + txnMsg.push(txInst); } else { // Transactions should always have at least one instruction. But if // the Transaction was not deserialized correctly we could end up // here. txnMsg.push(''); - txnMsg.push(''); } - txnMsg.push(tx.id); txnMsg = txnMsg.join('#'); commands.push(['sadd', `!ent-txn:${message.hash}`, tx.id]); commands.push(['lpush', '!txn-timeline', txnMsg]); + if (tx.instructions.length > 0) { commands.push([ 'lpush', diff --git a/src/App.js b/src/App.js index 375304e0..3f50d6dc 100755 --- a/src/App.js +++ b/src/App.js @@ -307,21 +307,10 @@ class App extends Component { return; } - let txnFun = v => { - let newObj = {}; - let fields = v.split('#'); - - newObj.t = 'txn'; - newObj.h = fields[0]; - newObj.l = fields[1]; - newObj.s = fields[2]; - newObj.dt = fields[3]; - newObj.entry_id = fields[4]; - newObj.program_id = fields[5]; - newObj.keys = fields[6].split(','); - newObj.id = fields[7]; + let self = this; - return newObj; + let txnFun = v => { + return self.parseTransactionMessage(v); }; this.getRemoteState( @@ -439,6 +428,16 @@ class App extends Component { parseTransactionMessage(message) { let fields = message.split('#'); + let instructions = _.map(fields[6].split('|'), i => { + let instParts = i.split('@'); + + return { + program_id: instParts[0], + keys: instParts[1].split(','), + data: instParts[2], + }; + }); + return { t: 'txn', h: parseInt(fields[0]), @@ -446,9 +445,8 @@ class App extends Component { s: parseInt(fields[2]), dt: fields[3], entry_id: fields[4], - program_id: fields[5], - keys: fields[6].split(','), - id: fields[7], + id: fields[5], + instructions, }; } diff --git a/src/BxDataTable.jsx b/src/BxDataTable.jsx index 73f98e34..beb4030b 100644 --- a/src/BxDataTable.jsx +++ b/src/BxDataTable.jsx @@ -60,6 +60,21 @@ class BxDataTable extends React.Component { renderTransactions() { const {dataItems, noTitle} = this.props; + let collectProgramIds = tx => { + return _.chain(tx.instructions) + .map(x => x.program_id) + .uniq() + .value(); + }; + + let collectKeys = tx => { + return _.chain(tx.instructions) + .map(x => x.keys) + .flatten() + .uniq() + .value(); + }; + return ( {!noTitle && ( @@ -86,7 +101,7 @@ class BxDataTable extends React.Component { - Program ID + Program ID(s) @@ -102,7 +117,7 @@ class BxDataTable extends React.Component {
- {_.map(row.keys, key => ( + {_.map(collectKeys(row), key => ( @@ -110,9 +125,12 @@ class BxDataTable extends React.Component { ))}
- -
-   + {_.map(collectProgramIds(row), program_id => ( + + +
+
+ ))}
{row.s}