Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Live balances, transactions and prices #9

Merged
merged 73 commits into from
Jan 27, 2022
Merged
Show file tree
Hide file tree
Changes from 65 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
d9468ba
Adding CoinMarketCap types
agustin-v Dec 8, 2021
2fe5713
CoinMarketCap class to access the quotas(prices) of each token or coin
agustin-v Dec 8, 2021
a8eaa96
Added type for query params for the prices endpoint
agustin-v Dec 8, 2021
307ab2e
Changing return type
agustin-v Dec 8, 2021
0579d74
Changing return type
agustin-v Dec 8, 2021
3127777
Adding price endpoint
agustin-v Dec 9, 2021
25ec8e0
Function to convert a new Date object into UTC epoch
agustin-v Dec 9, 2021
ef96c6a
Adding loki js to cache the amount of requests we have performed to t…
agustin-v Dec 9, 2021
79228b5
Implementing lokijs to cache the request count and data from coinmark…
agustin-v Dec 9, 2021
739e7b8
Removing cache system and lokijs
agustin-v Dec 10, 2021
e49e91e
Adding types for metadata
agustin-v Dec 10, 2021
3b490ee
Removing error handling from the coinmarketcap lib
agustin-v Dec 13, 2021
78dcd26
Types for metadata response
agustin-v Dec 13, 2021
e93c731
Adding sanitization functions for metadata and quote results
agustin-v Dec 13, 2021
e5b1485
Running linter
agustin-v Dec 13, 2021
7682b84
Endpoint for prices.
agustin-v Dec 13, 2021
6670340
Reunning linter
agustin-v Dec 13, 2021
d0a1786
Merging develop into feature branch to remove conflicts
agustin-v Dec 13, 2021
e368bdf
socket setup
Dec 14, 2021
f720de8
something proposal
Dec 14, 2021
b8e0dac
better emit event name
Dec 14, 2021
c1a5136
Fixing types syntax
agustin-v Dec 14, 2021
2b36027
push new balances working
Dec 15, 2021
b268bfd
minor changes and handle disconnect
Dec 16, 2021
b219ab9
Move parsing logic to coinmarketcap.ts
ilanolkies Dec 17, 2021
c7e0de9
Move files
ilanolkies Dec 17, 2021
714f13f
Refactor api
ilanolkies Dec 17, 2021
600950a
Lint
ilanolkies Dec 17, 2021
c487e6e
Refactor json result
ilanolkies Dec 17, 2021
fd133fb
Add tests
ilanolkies Dec 19, 2021
696483c
Add error handling tests - move validatoins to api
ilanolkies Dec 19, 2021
f8f4fdc
review changes
Dec 20, 2021
aa84621
Fixing merging issues
Dec 20, 2021
7963e73
Getting new transactions from WS explorer api (#11)
sleyter93 Dec 20, 2021
25f2c00
Merge branch 'develop' into feature/live-balances
ilanolkies Dec 20, 2021
eee6a11
Lint
ilanolkies Dec 20, 2021
c84f755
Merging updated-live-balances
Dec 20, 2021
d5ceeb2
Polling function for coinmarketcap prices
Dec 21, 2021
6a1c294
Implemented pushNewPrices socket based on wallet address
Dec 21, 2021
5890dbf
Implementing tokens addresses by chainId, 30 for mainnet and 31 for t…
Dec 21, 2021
6b46b74
Fixing tests
Dec 21, 2021
b25753a
Adding chainId parameter for the coinmarketcap class
Dec 21, 2021
9a59761
Lint
Dec 21, 2021
7109f3c
Fixing merge conflicts
Dec 21, 2021
fe840f1
Removing trailing comma
Dec 21, 2021
f68710e
Extends map between contract address and coin market cap
sleyter93 Dec 23, 2021
00a154a
Update list of supported tokens by coinmarketcap
Dec 27, 2021
31b362e
Filtering out unsupported tokens
Dec 28, 2021
7f6fe29
Commenting out the validatePricesRequest function which validates if …
Dec 28, 2021
94bb4c4
Commenting out the validatePricesRequest function which validates if …
Dec 28, 2021
33a379e
coinmarketcap api will fail if no ids passed therefore we added an if…
Dec 29, 2021
8f36bc7
adding an empty json as default value for prices, in case the wallet …
Dec 29, 2021
04cb38e
fixing merging issues
Dec 29, 2021
8eec61b
Lint
Dec 29, 2021
1f11902
Adding only supported tokens
Jan 3, 2022
4d5b7a6
exporting isConvertSupported function
Jan 3, 2022
a62c9b2
Lint
Jan 3, 2022
67d4bba
Adding conditions to handle unsupported convert or addresses
Jan 3, 2022
e98cd36
Modifying tests to handle new responses
Jan 3, 2022
c1104ac
Adding chainId as parameter to the subscriptions
Jan 3, 2022
b9c23f4
Adding RBTC as default in the subscriptions
Jan 3, 2022
dde2abe
Merge pull request #14 from rsksmart/feature/live-prices
chescalante Jan 4, 2022
2efd605
Add caching strategy to coin market cap
sleyter93 Jan 11, 2022
120978c
Merging changes
sleyter93 Jan 11, 2022
91a5811
Fixing test for caching
sleyter93 Jan 11, 2022
55cdd12
Migrate store in cache and filtering missing address
Jan 19, 2022
71ec7e4
Fixing name in test and add type for channel response
sleyter93 Jan 20, 2022
85dbcdd
Adding tests for caching
sleyter93 Jan 20, 2022
7f0e823
Merge pull request #16 from rsksmart/feature/caching
ilanolkies Jan 20, 2022
d754ef2
Polling to get contract call transactions (#19)
sleyter93 Jan 26, 2022
a4d4ac7
Enhancement/error handling (#17)
agustin-v Jan 26, 2022
8f9b981
Push transactions that have higher block number than the last sent
ilanolkies Jan 27, 2022
2a774a9
Merge pull request #20 from rsksmart/fix-tx-condition
sleyter93 Jan 27, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
723 changes: 714 additions & 9 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
"@rsksmart/rsk-utils": "^1.1.0",
"axios": "^0.22.0",
"dotenv": "^10.0.0",
"express": "^4.17.1"
"express": "^4.17.1",
"node-cache": "^5.1.2",
"socket.io": "^4.4.0",
"socket.io-client": "^2.4.0"
}
}
31 changes: 26 additions & 5 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { RSKExplorerAPI } from '../rskExplorerApi'
import { CoinMarketCapAPI } from '../coinmatketcap'
import { registeredDapps as _registeredDapps } from '../registered_dapps'
import { PricesQueryParams } from './types'
import { validatePricesRequest } from '../coinmatketcap/validations'
import { isConvertSupported, isTokenSupported } from '../coinmatketcap/validations'
import NodeCache from 'node-cache'
import { findInCache } from '../coinmatketcap/cache'

const responseJsonOk = (res: Response) => res.status(200).json.bind(res)

Expand All @@ -22,11 +24,13 @@ type APIOptions = {
rskExplorerApi: RSKExplorerAPI
coinMarketCapApi: CoinMarketCapAPI
registeredDapps: typeof _registeredDapps
cache: NodeCache
logger?: any
chainId: number
}

export const setupApi = (app: Application, {
rskExplorerApi, coinMarketCapApi, registeredDapps, logger = { log: () => {}, error: () => {} }
rskExplorerApi, coinMarketCapApi, registeredDapps, cache, logger = { log: () => {}, error: () => {} }, chainId
}: APIOptions) => {
const makeRequest = makeRequestFactory(logger)

Expand Down Expand Up @@ -57,10 +61,27 @@ export const setupApi = (app: Application, {
'/price',
async (req: Request<{}, {}, {}, PricesQueryParams>, res: Response) => makeRequest(
req, res, () => {
const addresses = req.query.addresses.split(',')
let addresses = req.query.addresses.split(',').filter((address) => isTokenSupported(address, chainId))
const convert = req.query.convert
validatePricesRequest(addresses, convert)
return coinMarketCapApi.getQuotesLatest({ addresses, convert })

const isAddressesEmpty = addresses.length === 0
if (isAddressesEmpty) return ({})

if (!isConvertSupported(convert)) throw new Error('Convert not supported')

const pricesInCache = findInCache(addresses, cache)
addresses = addresses.filter(address => !Object.keys(pricesInCache).includes(address))
ilanolkies marked this conversation as resolved.
Show resolved Hide resolved
let prices = {}
if (addresses.length) {
prices = coinMarketCapApi.getQuotesLatest({ addresses, convert })
}
ilanolkies marked this conversation as resolved.
Show resolved Hide resolved
return Promise.all([pricesInCache, prices]).then(values => {
ilanolkies marked this conversation as resolved.
Show resolved Hide resolved
Object.keys(values[1]).forEach(address => cache.set(address, { [address]: values[1][address] }, 60))
return {
...values[0],
...values[1]
}
})
}
)
)
Expand Down
15 changes: 15 additions & 0 deletions src/coinmatketcap/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import NodeCache from 'node-cache'
import { Prices } from '../api/types'

export const findInCache = (addresses: string[], cache: NodeCache): Prices => {
ilanolkies marked this conversation as resolved.
Show resolved Hide resolved
let response = {}
addresses.forEach(address => {
if (cache.get(address)) {
ilanolkies marked this conversation as resolved.
Show resolved Hide resolved
response = {
...response,
...cache.get(address)
}
}
})
return response
}
24 changes: 15 additions & 9 deletions src/coinmatketcap/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import _axios, { AxiosResponse } from 'axios'
import { ICoinMarketCapQuoteParams, ICoinMarketCapQuoteResponse } from './types'
import { addressToCoinmarketcapId } from './support'
import { isTokenSupported } from '../coinmatketcap/validations'
import { Prices } from '../api/types'

type PricesQueryParams = { addresses: string[], convert: string }

const coinmarketcapIdToAddress = Object.keys(addressToCoinmarketcapId)
.reduce((p, c) => ({ ...p, [addressToCoinmarketcapId[c]]: c }), {})

const fromQueryParamsToRequestParams = (params: PricesQueryParams): ICoinMarketCapQuoteParams => ({
id: params.addresses.map(address => addressToCoinmarketcapId[address]).join(','),
const fromQueryParamsToRequestParams = (params: PricesQueryParams, chaindId: number): ICoinMarketCapQuoteParams => ({
id: params.addresses
.filter((address) => isTokenSupported(address, chaindId))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this again? We should do decide where to do it and do it only once

.map(address => addressToCoinmarketcapId[chaindId][address]).join(','),
convert: params.convert
})

const fromQuotesResponseToPrices =
(convert: string) =>
(convert: string, coinmarketcapIdToAddress: Record<string, string>) =>
(response: AxiosResponse<ICoinMarketCapQuoteResponse>) =>
Object.keys(response.data.data).reduce<Prices>((p, c) => ({
...p,
Expand All @@ -28,23 +28,29 @@ export class CoinMarketCapAPI {
headers: { 'X-CMC_PRO_API_KEY': string }
baseURL: string
axios: typeof _axios
chainId: number
coinmarketcapIdToAddress: Record<string, string>

constructor (
url: string,
version: string,
apiKey: string,
axios: typeof _axios
axios: typeof _axios,
chainId: number
) {
this.baseURL = `${url}/${version}`
this.headers = {
'X-CMC_PRO_API_KEY': apiKey
}
this.axios = axios
this.chainId = chainId
this.coinmarketcapIdToAddress = Object.keys(addressToCoinmarketcapId[chainId])
.reduce((p, c) => ({ ...p, [addressToCoinmarketcapId[chainId][c]]: c }), {})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we removed the name coinmarketcapIdToAddress?

}

getQuotesLatest = (queryParams: PricesQueryParams): Promise<Prices> => this.axios.get<ICoinMarketCapQuoteResponse>(
`${this.baseURL}/cryptocurrency/quotes/latest`, {
headers: this.headers,
params: fromQueryParamsToRequestParams(queryParams)
}).then(fromQuotesResponseToPrices(queryParams.convert!))
params: fromQueryParamsToRequestParams(queryParams, this.chainId)
}).then(fromQuotesResponseToPrices(queryParams.convert!, this.coinmarketcapIdToAddress))
}
15 changes: 13 additions & 2 deletions src/coinmatketcap/support.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
export const addressToCoinmarketcapId = {
'0x0000000000000000000000000000000000000000': '3626', // RBTC
'0x2acc95758f8b5f583470ba265eb685a8f45fc9d5': '3701'
30: {
'0x0000000000000000000000000000000000000000': '3626', // RBTC
'0x2acc95758f8b5f583470ba265eb685a8f45fc9d5': '3701', // RIF
'0xef213441a85df4d7acbdae0cf78004e1e486bb96': '825', // rUSDT
'0x4991516df6053121121274397a8c1dad608bc95b': '7785', // rBUND
'0xefc78fc7d48b64958315949279ba181c2114abbd': '8669' // SOV
},
31: {
'0x0000000000000000000000000000000000000000': '3626', // RBTC
'0x19f64674d8a5b4e652319f5e239efd3bc969a1fe': '3701', // tRIF
'0x4cfe225ce54c6609a525768b13f7d87432358c57': '825', // rKovUSDT
'0xe95afdfec031f7b9cd942eb7e60f053fb605dfcd': '7785' // rKovBUND
}
ilanolkies marked this conversation as resolved.
Show resolved Hide resolved
}

export const supportedFiat = ['USD']
8 changes: 4 additions & 4 deletions src/coinmatketcap/validations.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { addressToCoinmarketcapId, supportedFiat } from './support'

const isTokenSupported = (address: string) => addressToCoinmarketcapId[address] !== undefined
const isConvertSupported = (convert: string) => supportedFiat.includes(convert)
export const isTokenSupported = (address: string, chainId: number) => addressToCoinmarketcapId[chainId][address] !== undefined
export const isConvertSupported = (convert: string) => supportedFiat.includes(convert)

export const validatePricesRequest = (addresses: string[], convert: string) => {
addresses.forEach(address => { if (!isTokenSupported(address)) throw new Error('Token address not supported') })
export const validatePricesRequest = (addresses: string[], convert: string, chainId: number) => {
addresses.forEach(address => { if (!isTokenSupported(address, chainId)) throw new Error('Token address not supported') })
if (!isConvertSupported(convert)) throw new Error('Convert not supported')
}
60 changes: 54 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { CoinMarketCapAPI } from './coinmatketcap'
import { RSKExplorerAPI } from './rskExplorerApi'
import { registeredDapps } from './registered_dapps'
import { setupApi } from './api'
import { Server } from 'socket.io'
import NodeCache from 'node-cache'
import http from 'http'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import http from 'http'
import https from 'https'

Lets make this safe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll need a ssl certificate for that. (https://nodejs.org/en/knowledge/HTTP/servers/how-to-create-a-HTTPS-server/)
Let's do it in another PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: #12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, definitely.
For dev you can use a self signed (self generated) for prod, you should cordinate that with devops

import pushNewBalances from './subscriptions/pushNewBalances'
import pushNewPrices from './subscriptions/pushNewPrices'
import pushNewTransactions from './subscriptions/pushNewTransactions'

const environment = {
// TODO: remove these defaults
Expand All @@ -16,21 +22,63 @@ const environment = {
CHAIN_ID: parseInt(process.env.CHAIN_ID as string) || 31,
COIN_MARKET_CAP_URL: process.env.COIN_MARKET_CAP_URL as string || 'https://pro-api.coinmarketcap.com',
COIN_MARKET_CAP_VERSION: process.env.COIN_MARKET_CAP_VERSION as string || 'v1',
COIN_MARKET_CAP_KEY: process.env.COIN_MARKET_CAP_KEY! as string
COIN_MARKET_CAP_KEY: process.env.COIN_MARKET_CAP_KEY! as string,
DEFAULT_CONVERT_FIAT: process.env.DEFAULT_CONVERT_FIAT! as string
}

const app = express()

const rskExplorerApi = new RSKExplorerAPI(environment.API_URL, environment.CHAIN_ID, axios)
const coinMarketCapApi = new CoinMarketCapAPI(environment.COIN_MARKET_CAP_URL, environment.COIN_MARKET_CAP_VERSION, environment.COIN_MARKET_CAP_KEY, axios)
const coinMarketCapApi = new CoinMarketCapAPI(
environment.COIN_MARKET_CAP_URL,
environment.COIN_MARKET_CAP_VERSION,
environment.COIN_MARKET_CAP_KEY,
axios,
environment.CHAIN_ID
)

const app = express()
const cache = new NodeCache()
setupApi(app, {
rskExplorerApi,
coinMarketCapApi,
registeredDapps,
logger: console
cache,
logger: console,
chainId: environment.CHAIN_ID
})

const server = http.createServer(app)
const io = new Server(server, {
// cors: {
// origin: 'https://amritb.github.io'
// },
path: '/ws'
})

io.on('connection', (socket) => {
console.log('new user connected')

socket.on('subscribe', ({ address }: { address: string }) => {
console.log('new subscription with address: ', address)

const stopPushingNewBalances = pushNewBalances(socket, rskExplorerApi, address)
const stopPushingNewTransactions = pushNewTransactions(socket, address)
const stopPushingNewPrices = pushNewPrices(
socket,
rskExplorerApi,
coinMarketCapApi,
address,
environment.DEFAULT_CONVERT_FIAT,
environment.CHAIN_ID
)

socket.on('disconnect', () => {
stopPushingNewBalances()
stopPushingNewTransactions()
stopPushingNewPrices()
})
})
})

app.listen(environment.PORT, () => {
server.listen(environment.PORT, () => {
console.log(`RIF Wallet services running on port ${environment.PORT}.`)
})
51 changes: 51 additions & 0 deletions src/subscriptions/pushNewBalances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Socket } from 'socket.io'
import { DefaultEventsMap } from 'socket.io/dist/typed-events'
import { RSKExplorerAPI } from '../rskExplorerApi/index'

interface ISentBalances {
[address: string]: {
[tokenAddress: string]: string
}
}

const EXECUTION_INTERVAL = 60000

const sentBalances: ISentBalances = {}

const pushNewBalances = (
socket: Socket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>,
api: RSKExplorerAPI,
address: string
) => {
const execute = executeFactory(socket, api, address)

execute()

const timer = setInterval(execute, EXECUTION_INTERVAL)

return () => {
clearInterval(timer)
sentBalances[address] = {}
}
}

const executeFactory = (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we wan to declare the functions before we use them. Should we reorder this code?

socket: Socket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>,
api: RSKExplorerAPI,
address: string
) => async () => {
if (!sentBalances[address]) {
sentBalances[address] = {}
}

const tokens = await api.getTokensByAddress(address.toLowerCase())

for (const token of tokens) {
if (sentBalances[address][token.contractAddress] !== token.balance) {
sentBalances[address][token.contractAddress] = token.balance
socket.emit('change', { type: 'newBalance', payload: token })
}
}
}

export default pushNewBalances
49 changes: 49 additions & 0 deletions src/subscriptions/pushNewPrices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { CoinMarketCapAPI } from '../coinmatketcap'
import { DefaultEventsMap } from 'socket.io/dist/typed-events'
import { RSKExplorerAPI } from '../rskExplorerApi/index'
import { Socket } from 'socket.io'
import { isTokenSupported } from '../coinmatketcap/validations'

const EXECUTION_INTERVAL = 60000

const pushNewPrices = (socket: Socket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>,
api: RSKExplorerAPI,
cmc: CoinMarketCapAPI,
address: string,
convert: string,
chainId: number
) => {
const execute = getPricesByToken(socket, api, cmc, address, convert, chainId)

execute()

const timer = setInterval(execute, EXECUTION_INTERVAL)

return () => {
clearInterval(timer)
}
}

const getPricesByToken = (
socket: Socket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>,
api: RSKExplorerAPI,
cmc: CoinMarketCapAPI,
address: string,
convert: string,
chainId: number) => async () => {
const RBTC = '0x0000000000000000000000000000000000000000'
let prices = {}
const addresses = [RBTC, ...(await api.getTokensByAddress(address.toLowerCase()))
.map(token => token.contractAddress.toLocaleLowerCase())
.filter(token => isTokenSupported(token, chainId))]

const isAddressesEmpty = addresses.length === 0

if (!isAddressesEmpty) {
prices = await cmc.getQuotesLatest({ addresses, convert })
ilanolkies marked this conversation as resolved.
Show resolved Hide resolved
}

socket.emit('change', { type: 'newPrice', payload: prices })
}

export default pushNewPrices