Skip to content

Commit

Permalink
colw/Transaction Service (#120)
Browse files Browse the repository at this point in the history
* Obtain transaction payload, mirror back to client

* Respond with request, or error if none present

* Load node friendly versions of cosmos-api for dev

* Send response as JSON

* Successfully retrieve a gas estimate

- Use node-fetch for cosmosAPIs
-

* Add two sub routes: /estimate and /broadcast

* Add account number and sequence to overview query

* Disable cors options

* Add support to broadcast messages

* Ignore fetch undefined

* Delete package-lock.json
  • Loading branch information
colw authored and faboweb committed Nov 26, 2019
1 parent 6f3a6df commit bc493ce
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 5 deletions.
2 changes: 2 additions & 0 deletions cosmos-api-v0-0.1.2/index.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions cosmos-api-v0-0.2.2/index.js

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions lib/controller/transaction/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const { getMessage } = require('./messageConstructor')
const { networks } = require('../../networks')

global.fetch = require('node-fetch')

async function estimate(tx) {
const context = {
userAddress: tx.address,
networkId: tx.networkId,
url: networks[tx.networkId].api_url,
chainId: networks[tx.networkId].chain_id
}

const message = getMessage(tx.messageType, tx.txProperties, context)

try {
const gasEstimate = await message.simulate({ memo: tx.memo })
return {
gasEstimate,
success: true
}
} catch (e) {
return {
error: e,
success: false
}
}
}

async function broadcast(tx) {
console.log(`Received broadcast: ${JSON.stringify(tx)}`)
try {
const hash = await broadcastTransaction(
networks[tx.networkId].api_url,
tx.signedMessage
)
return {
hash: hash,
success: true
}
} catch (e) {
return {
error: e,
success: false
}
}
}

module.exports = {
estimate,
broadcast
}

async function broadcastTransaction(url, signedTx) {
// broadcast transaction with signatures included
const body = createBroadcastBody(signedTx, `sync`)
console.log('broadcast to:', url)
// eslint-disable-next-line no-undef
const res = await fetch(`${url}/txs`, {
method: `POST`,
headers: {
'Content-Type': 'application/json'
},
body
})
.then(res => res.json())
.then(assertOk)

return res.txhash
}

function createBroadcastBody(signedTx, returnType = `sync`) {
return JSON.stringify({
tx: signedTx,
mode: returnType
})
}

function assertOk(res) {
if (Array.isArray(res)) {
if (res.length === 0) throw new Error(`Error sending transaction`)

res.forEach(assertOk)
}

if (res.error) {
throw new Error(res.error)
}

// Sometimes we get back failed transactions, which shows only by them having a `code` property
if (res.code) {
const message = JSON.parse(res.raw_log).message
throw new Error(message)
}

if (!res.txhash) {
const message = res.message
throw new Error(message)
}

return res
}
48 changes: 48 additions & 0 deletions lib/controller/transaction/messageConstructor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const cosmosapiV0 = require('../../../cosmos-api-v0-0.1.2/').default
const cosmosapiV2 = require('../../../cosmos-api-v0-0.2.2/').default

function getMessage(messageType, transactionProperties, context) {
const messageConstructor = getMessageConstructor(context)
const message = messageConstructor(
messageType,
context.userAddress,
transactionProperties
)
return message
}

function getMessageConstructor({ networkId, url, chainId }) {
switch (networkId) {
case `local-cosmos-hub-testnet`:
case `cosmos-hub-mainnet`: {
const cosmos = new cosmosapiV0(url || '', chainId || '')
return (messageType, userAddress, transactionProperties) =>
cosmos[messageType](userAddress, transactionProperties)
}
case `cosmos-hub-testnet`: {
const cosmos = new cosmosapiV2(url || '', chainId || '')
return (messageType, userAddress, transactionProperties) =>
cosmos[messageType](userAddress, transactionProperties)
}
}
throw Error('Network is not supported for signing transactions.')
}

function getMultiMessage({ userAddress, networkId, url, chainId }, messages) {
switch (networkId) {
case `local-cosmos-hub-testnet`:
case `cosmos-hub-mainnet`: {
const cosmos = new cosmosapiV0(url || '', chainId || '')
return cosmos.MultiMessage(userAddress, messages)
}
case `cosmos-hub-testnet`: {
const cosmos = new cosmosapiV2(url || '', chainId || '')
return cosmos.MultiMessage(userAddress, messages)
}
}
}

module.exports = {
getMessage,
getMultiMessage
}
6 changes: 6 additions & 0 deletions lib/cosmosV0-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ class CosmosV0API extends PerBlockCacheDataSource {
return balances.map(this.reducers.coinReducer)
}

async getAccountInfo(address) {
const response = await this.query(`auth/accounts/${address}`)
const accountValue = response && response.value
return this.reducers.accountInfoReducer(accountValue)
}

async getDelegationsForDelegatorAddress(address, validatorsDictionary) {
let delegations =
(await this.query(`staking/delegators/${address}/delegations`)) || []
Expand Down
12 changes: 12 additions & 0 deletions lib/networks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const config = require('../config')
const networksMain = require('../data/networks.json')
const networksLocal = require('../data/networks-local.js')

let networks = networksMain
if (config.enableTestnet) {
networks = { ...networks, ...networksLocal }
}

module.exports = {
networks
}
9 changes: 9 additions & 0 deletions lib/reducers/cosmosV0-reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ function proposalFinalized(proposal) {
return ['Passed', 'Rejected'].indexOf(proposal.proposal_status) !== -1
}

function accountInfoReducer(accountValue) {
return {
address: accountValue.address,
accountNumber: accountValue.account_number,
sequence: accountValue.sequence
}
}

function atoms(nanoAtoms) {
return BigNumber(nanoAtoms)
.div(1000000)
Expand Down Expand Up @@ -390,6 +398,7 @@ module.exports = {
undelegationReducer,
rewardReducer,
overviewReducer,
accountInfoReducer,

atoms,
proposalBeginTime,
Expand Down
11 changes: 9 additions & 2 deletions lib/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ async function undelegations(
}

const resolvers = {
Overview: {
accountInformation: (account, _, { dataSources }) =>
selectFrom(dataSources, account.networkId).getAccountInfo(account.address)
},
Proposal: {
validator: (proposal, _, { dataSources }) => {
//
Expand Down Expand Up @@ -260,12 +264,15 @@ const resolvers = {
}
return rewards
},
overview: (_, { networkId, address }, { dataSources }) => {
overview: async (_, { networkId, address }, { dataSources }) => {
const validatorsDictionary = dataSources.store[networkId].validators
return selectFrom(dataSources, networkId).getOverview(
const overview = await selectFrom(dataSources, networkId).getOverview(
address,
validatorsDictionary
)
overview.networkId = networkId
overview.address = address
return overview
},
transactions: (_, { networkId, address }, { dataSources }) =>
selectFrom(dataSources, networkId).getTransactions(address)
Expand Down
18 changes: 15 additions & 3 deletions lib/routes/transaction.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
var express = require('express')
var router = express.Router()
var { estimate, broadcast } = require('./../controller/transaction')

router.use(function timeLog(req, res, next) {
console.log('Transaction Received', Date.now())
req.txRequest = req.body && req.body.payload
if (req.txRequest) {
console.log(`Transaction ${Date.now()} ${req.txRequest.messageType}`)
} else {
res.json({ error: 'No Request Found' })
}
next()
})

router.use('/', function(req, res) {
res.send('Transaction API Request received')
router.use('/estimate', async function(req, res) {
const response = await estimate(req.txRequest)
res.json(response)
})

router.use('/broadcast', async function(req, res) {
const response = await broadcast(req.txRequest)
res.json(response)
})

module.exports = router
8 changes: 8 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,18 @@ const typeDefs = gql`
option: String
}
type AccountInformation {
accountNumber: String
sequence: String
}
type Overview {
networkId: String!
address: String!
totalStake: String!
liquidStake: String!
totalRewards: String!
accountInformation: AccountInformation
}
type Subscription {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"eslint-plugin-prettier": "^3.1.1",
"husky": "^3.0.9",
"jest": "^24.9.0",
"node-fetch": "^2.6.0",
"nodemon": "^1.19.4",
"prettier": "^1.19.1"
}
Expand Down

0 comments on commit bc493ce

Please sign in to comment.