diff --git a/api/api.js b/api/api.js
index 5e4e880b..91dd694a 100644
--- a/api/api.js
+++ b/api/api.js
@@ -31,6 +31,7 @@ const GLOBAL_STATS_BROADCAST_INTERVAL_MS = 2000;
const CLUSTER_INFO_BROADCAST_INTERVAL_MS = 16000;
const CLUSTER_INFO_CACHE_TIME_SECS = 35000;
const CONFIG_PROGRAM_ID = 'Config1111111111111111111111111111111111111';
+const MAX_KEYBASE_USER_LOOKUP = 50;
const app = express();
@@ -499,7 +500,7 @@ async function getClusterInfo() {
let [, feeCalculator] = await connection.getRecentBlockhash();
let supply = await connection.getTotalSupply();
let cluster = await connection.getClusterNodes();
- let validators = await fetchValidatorInfo(cluster.map(c => c.pubkey));
+ let identities = await fetchValidatorIdentities(cluster.map(c => c.pubkey));
let voting = await connection.getEpochVoteAccounts();
let totalStaked = _.reduce(
voting,
@@ -532,7 +533,7 @@ async function getClusterInfo() {
supply,
totalStaked,
cluster,
- validators,
+ identities,
voting,
ts,
};
@@ -565,13 +566,38 @@ app.get('/cluster-info', (req, res) => {
sendClusterResult(req, res);
});
-async function fetchValidatorInfo(keys) {
+async function fetchValidatorAvatars(keybaseUsernames) {
+ const avatarMap = new Map();
+ let batch = keybaseUsernames.splice(0, MAX_KEYBASE_USER_LOOKUP)
+ while (batch.length > 0) {
+ const usernames = batch.join(',');
+ const keybaseApiUrl = `https://keybase.io/_/api/1.0/user/lookup.json?usernames=${usernames}&fields=pictures,basics`;
+ try {
+ const keybaseResponse = await fetch(keybaseApiUrl);
+ const keybaseData = await keybaseResponse.json();
+ if (keybaseData && keybaseData.them) {
+ for (const {basics, pictures} of keybaseData.them) {
+ if (basics && basics.username && pictures && pictures.primary && pictures.primary.url) {
+ avatarMap.set(basics.username, pictures.primary.url);
+ }
+ }
+ }
+ } catch(err) {
+ // Skip failed batch
+ }
+ // Prepare next batch
+ batch = keybaseUsernames.splice(0, MAX_KEYBASE_USER_LOOKUP);
+ }
+ return avatarMap;
+}
+
+async function fetchValidatorIdentities(keys) {
const configKey = new solanaWeb3.PublicKey(CONFIG_PROGRAM_ID);
const connection = new solanaWeb3.Connection(FULLNODE_URL);
const accounts = await connection.getProgramAccounts(configKey);
const keySet = new Set(keys);
- const results = await Promise.all(
+ let identities = await Promise.all(
accounts.map(async account => {
let validatorInfo;
try {
@@ -584,50 +610,36 @@ async function fetchValidatorInfo(keys) {
const validatorKeyStr = validatorInfo.key.toString();
if (keySet.has(validatorKeyStr)) {
keySet.delete(validatorKeyStr);
-
- // build info and verify
- const info = validatorInfo.info;
- const keybaseUsername = info.keybaseUsername;
+ // build identity and verify
+ const identity = validatorInfo.info;
+ const keybaseUsername = identity.keybaseUsername;
if (keybaseUsername) {
const keybaseUrl = `https://keybase.pub/${keybaseUsername}/solana/validator-${validatorKeyStr}`;
const keybaseResponse = await fetch(keybaseUrl, {method: 'HEAD'});
const verified = keybaseResponse.status === 200;
- info.verified = verified;
- info.verifyUrl = keybaseUrl;
+ identity.verified = verified;
+ identity.verifyUrl = keybaseUrl;
}
- info.pubkey = validatorKeyStr;
- return info;
+ identity.pubkey = validatorKeyStr;
+ return identity;
}
}
}),
);
- const infoList = results.filter(r => r);
- const keybaseUsernames = infoList.map(info => info.keybaseUsername).filter(u => u).join(',');
- const avatarMap = new Map();
- if (keybaseUsernames.length > 0) {
- const keybaseApiUrl = `https://keybase.io/_/api/1.0/user/lookup.json?usernames=${keybaseUsernames}&fields=pictures,basics`;
- const keybaseResponse = await fetch(keybaseApiUrl);
- const keybaseData = await keybaseResponse.json();
- if (keybaseData && keybaseData.them) {
- for (const {basics, pictures} of keybaseData.them) {
- if (basics && basics.username && pictures && pictures.primary && pictures.primary.url) {
- avatarMap.set(basics.username, pictures.primary.url);
- }
- }
- }
- }
-
- for (const info of infoList) {
- if (info.keybaseUsername) {
- const avatarUrl = avatarMap.get(info.keybaseUsername);
+ identities = identities.filter(r => r);
+ const keybaseUsernames = identities.map(i => i.keybaseUsername).filter(u => u);
+ const avatarMap = await fetchValidatorAvatars(keybaseUsernames);
+ for (const identity of identities) {
+ if (identity.keybaseUsername) {
+ const avatarUrl = avatarMap.get(identity.keybaseUsername);
if (avatarUrl) {
- info.avatarUrl = avatarUrl;
+ identity.avatarUrl = avatarUrl;
}
}
}
- return infoList;
+ return identities;
}
app.listen(port, () => console.log(`Listening on port ${port}!`));
diff --git a/src/App.js b/src/App.js
index f6efa5f9..1f802293 100755
--- a/src/App.js
+++ b/src/App.js
@@ -141,14 +141,14 @@ class App extends Component {
parseClusterInfo(data) {
let voting = data.voting;
let gossip = data.cluster;
- let validators = data.validators;
+ let identities = data.identities;
let nodes = _.map(gossip, g => {
let newG = {...g};
let vote = voting.find(x => x.nodePubkey === newG.pubkey);
newG.voteAccount = vote;
- let info = validators.find(v => v.pubkey === newG.pubkey);
- newG.info = info;
+ let identity = identities.find(v => v.pubkey === newG.pubkey);
+ newG.identity = identity;
return newG;
});
diff --git a/src/v2/Bx2PanelTourDeSolLeaderboard.jsx b/src/v2/Bx2PanelTourDeSolLeaderboard.jsx
index 10cb8f01..02bd0e13 100644
--- a/src/v2/Bx2PanelTourDeSolLeaderboard.jsx
+++ b/src/v2/Bx2PanelTourDeSolLeaderboard.jsx
@@ -287,7 +287,7 @@ class Bx2PanelTourDeSolLeaderboard extends Component {
{(row.voteAccount && row.voteAccount.stake) || 0} Lamports
-
+
TODO
diff --git a/src/v2/Bx2PanelValidatorDetail.jsx b/src/v2/Bx2PanelValidatorDetail.jsx
index 95b96a74..998f9bd7 100644
--- a/src/v2/Bx2PanelValidatorDetail.jsx
+++ b/src/v2/Bx2PanelValidatorDetail.jsx
@@ -194,7 +194,7 @@ class Bx2PanelValidatorDetail extends Component {
{(row.voteAccount && row.voteAccount.stake) || 0} Lamports
-
+
TODO
TODO
diff --git a/src/v2/Bx2PanelValidators.jsx b/src/v2/Bx2PanelValidators.jsx
index ae5f98e6..d7715c23 100644
--- a/src/v2/Bx2PanelValidators.jsx
+++ b/src/v2/Bx2PanelValidators.jsx
@@ -73,7 +73,7 @@ class Bx2PanelValidators extends React.Component {
{(row.voteAccount && row.voteAccount.stake) || 0} Lamports
-
+
TODO
TODO
diff --git a/src/v2/Bx2PanelValidatorsOverview.jsx b/src/v2/Bx2PanelValidatorsOverview.jsx
index 9e119553..be31c579 100644
--- a/src/v2/Bx2PanelValidatorsOverview.jsx
+++ b/src/v2/Bx2PanelValidatorsOverview.jsx
@@ -288,7 +288,7 @@ class Bx2PanelValidatorsOverview extends Component {
{(row.voteAccount && row.voteAccount.stake) || 0} Lamports
-
+
TODO
TODO
diff --git a/src/v2/Bx2ValidatorIdentity.jsx b/src/v2/Bx2ValidatorIdentity.jsx
index 9b493f1c..d9d8beb1 100644
--- a/src/v2/Bx2ValidatorIdentity.jsx
+++ b/src/v2/Bx2ValidatorIdentity.jsx
@@ -10,7 +10,7 @@ import Link from '@material-ui/core/Link';
class Bx2ValidatorIdentity extends Component {
renderAvatar() {
- let {avatarUrl, name} = this.props.info;
+ let {avatarUrl, name} = this.props.identity;
const avatarStyle = {
marginRight: 10,
@@ -39,7 +39,7 @@ class Bx2ValidatorIdentity extends Component {
}
renderVerified() {
- const {verified, verifyUrl} = this.props.info;
+ const {verified, verifyUrl} = this.props.identity;
let verifiedIcon;
if (verified && verifyUrl) {
@@ -70,7 +70,7 @@ class Bx2ValidatorIdentity extends Component {
}
renderName() {
- const {name, verified, website} = this.props.info;
+ const {name, verified, website} = this.props.identity;
let color = 'secondary';
if (!verified) {
@@ -121,7 +121,7 @@ class Bx2ValidatorIdentity extends Component {
}
render() {
- if (!this.props.info) {
+ if (!this.props.identity) {
return this.renderMissingInfo();
}
@@ -136,7 +136,7 @@ class Bx2ValidatorIdentity extends Component {
}
Bx2ValidatorIdentity.propTypes = {
- info: PropTypes.object,
+ identity: PropTypes.object,
};
export default Bx2ValidatorIdentity;