Skip to content

Commit

Permalink
First steps to integrate Polkadot (#376)
Browse files Browse the repository at this point in the history
* Add polkadot-testnet in networks.js

* Install polkadot-api, polkadot-node-sub. draft

* lint

* lint

* lint

* add polkadotV0 source

* add address creator

* Update data/networks.js

Co-Authored-By: Jordan Bibla <[email protected]>

* Update data/networks.js

Co-Authored-By: Jordan Bibla <[email protected]>

* blocks, events

* Cleanup

* polling instead subscription

* husky

* updateDBValidatorProfiles

* no wait for block data fetching, kind of stable

* Add reducers file

* block subscription works!

* Cleanup, fix memory leak

* lint, node

* comment

* Handle polkadot chain reorgs

* Cleanup

* Optimization, cleanup

* Optimization

* fixes, validators query working, cleanup

* comment

* lint

* fix block time

* handle polkadot chain hangups

* cleanup

* validator reducer

* lint

* wip

* calculate and include a bunch of validator fields

* add bech32_prefix address_prefix to networks.js

* add 1 space so we dont break tests

* remove chain reorg handling, more stable

Co-authored-by: Ana G. <[email protected]>
Co-authored-by: Fabian <[email protected]>
Co-authored-by: Jordan Bibla <[email protected]>
  • Loading branch information
4 people authored Mar 3, 2020
1 parent cded999 commit 4cc08d9
Show file tree
Hide file tree
Showing 7 changed files with 10,403 additions and 1 deletion.
34 changes: 34 additions & 0 deletions data/networks.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,40 @@ module.exports = [
icon: 'https://app.lunie.io/img/networks/emoney-testnet.png',
slug: 'emoney-testnet'
},
{
id: 'polkadot-testnet',
title: 'Kusama',
chain_id: 'kusama-cc3',
api_url: 'https://host-01.polkascan.io/kusama/api/v1/',
rpc_url: 'wss://kusama-rpc.polkadot.io/',
bech32_prefix: ' ',
address_prefix: ' ',
ledger_app: 'polkadot',
address_creator: 'polkadot',
source_class_name: 'source/polkadotV0-source',
block_listener_class_name: 'block-listeners/polkadot-node-subscription',
testnet: true,
feature_session: true,
feature_explore: true,
feature_portfolio: true,
feature_validators: true,
feature_proposals: false,
feature_activity: false,
feature_explorer: false,
action_send: true,
action_claim_rewards: false,
action_delegate: true,
action_redelegate: true,
action_undelegate: true,
action_deposit: false,
action_vote: false,
action_proposal: false,
default: false,
stakingDenom: 'KSM',
enabled: true,
icon: 'https://app.lunie.io/img/networks/polkadot-testnet.png',
slug: 'kusama'
},
{
id: 'livepeer-mainnet',
title: 'Livepeer',
Expand Down
2 changes: 1 addition & 1 deletion lib/block-listeners/cosmos-node-subscription.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class CosmosNodeSubscription {
try {
block = await this.cosmosAPI.getBlockByHeight()
} catch (error) {
console.error('Failed to fetch block for subscription', error)
console.error('Failed to fetch block', error)
Sentry.captureException(error)
}
if (block && this.height !== block.height) {
Expand Down
155 changes: 155 additions & 0 deletions lib/block-listeners/polkadot-node-subscription.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
const _ = require('lodash')
const {
publishBlockAdded
// publishUserTransactionAdded,
// publishEvent: publishEvent
} = require('../subscriptions')
const Sentry = require('@sentry/node')
const database = require('../database')
const config = require('../../config.js')

// This class polls for new blocks
// Used for listening to events, such as new blocks.
class PolkadotNodeSubscription {
constructor(network, PolkadotApiClass, store) {
this.network = network
this.polkadotAPI = new PolkadotApiClass(network)
this.store = store
this.validators = []
this.sessionValidators = []
const networkSchemaName = this.network.id.replace(/-/g, '_')
this.db = new database(config)(networkSchemaName)
this.height = 0
this.currentSessionIndex = 0
this.blockQueue = []
this.subscribeForNewBlock()
}

async subscribeForNewBlock() {
const api = await this.polkadotAPI.getAPIPromise()

// Subscribe to new block headers
await api.rpc.chain.subscribeNewHeads(async blockHeader => {
const blockHeight = blockHeader.number.toNumber()
if (this.height < blockHeight) {
this.height = blockHeight
console.log(`\x1b[36mNew kusama block #${blockHeight}\x1b[0m`)
this.newBlockHandler(blockHeight)
}
})
}

// Sometimes blocks get published unordered so we need to enqueue
// them before publish to ensure correct order. This adds 3 blocks delay.
enqueueAndPublishBlockAdded(newBlock) {
this.blockQueue.push(newBlock)
if (this.blockQueue.length > 2) {
this.blockQueue.sort((a, b) =>
a.height > b.height ? 1 : b.height > a.height ? -1 : 0
)
console.log(
`\x1b[36mPublishing new kusama block #${newBlock.height}\x1b[0m`
)
publishBlockAdded(this.network.id, this.blockQueue.shift())
}
}

// For each block event, we fetch the block information and publish a message.
// A GraphQL resolver is listening for these messages and sends the block to
// each subscribed user.
async newBlockHandler(blockHeight) {
try {
Sentry.configureScope(function(scope) {
scope.setExtra('height', blockHeight)
})

const block = await this.polkadotAPI.getBlockByHeight(blockHeight)
// publishBlockAdded(this.network.id, block)
this.enqueueAndPublishBlockAdded(block)

// We dont need to fetch validators on every new block.
// Validator list only changes on new sessions
if (
this.currentSessionIndex < block.sessionIndex ||
this.currentSessionIndex === 0
) {
console.log(
`\x1b[36mCurrent session index is ${block.sessionIndex}, fetching validators!\x1b[0m`
)
this.currentSessionIndex = block.sessionIndex
this.sessionValidators = await this.polkadotAPI.getAllValidators()
}

this.updateDBValidatorProfiles(this.sessionValidators)
this.store.update({
height: blockHeight,
block,
validators: this.sessionValidators
})

// For each transaction listed in a block we extract the relevant addresses. This is published to the network.
// A GraphQL resolver is listening for these messages and sends the
// transaction to each subscribed user.

// let addresses = []
// addresses = this.polkadotAPI.extractInvolvedAddresses(block.transactions)
// addresses = _.uniq(addresses)

// if (addresses.length > 0) {
// console.log(
// `\x1b[36mAddresses included in tx for block #${blockHeight}: ${addresses}\x1b[0m`
// )
// }

// addresses.forEach(address => {
// publishUserTransactionAdded(this.network.id, address, tx)
// publishEvent(this.network.id, 'transaction', address, tx)
// })
} catch (error) {
console.error(`newBlockHandler failed: ${error}`)
Sentry.captureException(error)
}
}

async getValidatorMap(validators) {
const validatorMap = _.keyBy(validators, 'operatorAddress')
return validatorMap
}

// this adds all the validator addresses to the database so we can easily check in the database which ones have an image and which ones don't
async updateDBValidatorProfiles(validators) {
// filter only new validators
let newValidators = validators.filter(
validator =>
!this.validators.find(
v =>
v.address == validator.operatorAddress && v.name == validator.name // in case if validator name was changed
)
)
// save all new validators to an array
this.validators = [
...this.validators.filter(
({ operatorAddress }) =>
!newValidators.find(
({ operatorAddress: newValidatorOperatorAddress }) =>
newValidatorOperatorAddress === operatorAddress
)
),
...newValidators.map(v => ({
address: v.operatorAddress,
name: v.name
}))
]
// update only new onces
const validatorRows = newValidators.map(
({ operatorAddress, name, chainId }) => ({
operator_address: operatorAddress,
name,
chain_id: chainId
})
)
return this.db.upsert('validatorprofiles', validatorRows)
}
}

module.exports = PolkadotNodeSubscription
55 changes: 55 additions & 0 deletions lib/reducers/polkadotV0-reducers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
function blockReducer(
networkId,
blockHeight,
blockHash,
sessionIndex,
blockAuthor,
blockEvents
) {
return {
networkId,
height: blockHeight,
chainId: `kusama-cc3`,
hash: blockHash,
sessionIndex,
time: new Date().toISOString(), // TODO: Get from blockchain state
transactions: blockEvents, // TODO: IMPROVE!
proposer_address: blockAuthor
}
}

function validatorReducer(network, validator) {
return {
networkId: network.id,
chainId: network.chain_id,
operatorAddress: validator.accountId,
website:
validator.identity.web && validator.identity.web !== ``
? validator.identity.web
: ``,
identity:
validator.identity.display && validator.identity.display !== ``
? validator.identity.display
: validator.accountId,
name:
validator.identity.display && validator.identity.display !== ``
? validator.identity.display
: validator.accountId,
votingPower: validator.votingPower.toFixed(9),
startHeight: undefined,
uptimePercentage: undefined,
tokens: validator.tokens,
commissionUpdateTime: undefined,
commission: validator.validatorPrefs.commission / 100000000,
maxCommission: undefined,
maxChangeCommission: undefined,
status: `ACTIVE`, // We are fetching current session active validators only (not intentions)
statusDetailed: ``, // TODO: Include validator heartbeat messages
delegatorShares: undefined
}
}

module.exports = {
blockReducer,
validatorReducer
}
Loading

0 comments on commit 4cc08d9

Please sign in to comment.