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

Query and record exchange rate using notification service #1020

Merged
merged 31 commits into from
Sep 24, 2019
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ec0fd6c
Add some todos
annakaz Sep 12, 2019
091e92e
Merge branch 'master' into annakaz/ex-rate-notification-service
annakaz Sep 16, 2019
b02a819
Update config file, create function signatures
annakaz Sep 16, 2019
86bcbd0
Add makeExhangeQuery and getWeb3Instance
annakaz Sep 16, 2019
c18fd14
Add exchangeRatePair object
annakaz Sep 16, 2019
c21bf0d
Add call to exchangePolling
annakaz Sep 16, 2019
44256d4
Create web3 instance every time polled
annakaz Sep 16, 2019
3923663
Update web3 handling and add exchangeRate to firebase
annakaz Sep 16, 2019
6400495
Write record to firebase
annakaz Sep 16, 2019
d641579
Add to package.json
annakaz Sep 16, 2019
a5bb2f2
Update to integration provider url for testing
annakaz Sep 16, 2019
4c6d22c
Get local test working
annakaz Sep 16, 2019
d757f91
Add web3 provider as a param
annakaz Sep 16, 2019
1c45663
Remove unused interface
annakaz Sep 16, 2019
8bf979a
Cleanup
annakaz Sep 16, 2019
9ef79fd
Add comments, remove unneeded BigNumber type signature
annakaz Sep 17, 2019
65fdb91
Refactor functions to make unit testing possible
annakaz Sep 17, 2019
ddedead
Add exchange tests
annakaz Sep 17, 2019
e7d519f
Make exchange queries in parallel
annakaz Sep 17, 2019
80a339c
Merge branch 'master' into annakaz/ex-rate-notification-service
annakaz Sep 17, 2019
c8eb924
Merge branch 'annakaz/ex-rate-notification-service' of github.com:cel…
annakaz Sep 17, 2019
93eea21
Merge branch 'master' into annakaz/ex-rate-notification-service
annakaz Sep 17, 2019
15c1df0
Make changes based on review
annakaz Sep 17, 2019
70ba895
Merge branch 'annakaz/ex-rate-notification-service' of github.com:cel…
annakaz Sep 17, 2019
a9c62fc
Merge branch 'master' into annakaz/ex-rate-notification-service
annakaz Sep 23, 2019
4bddc02
Update nested ifs and missing web3 provider behavior
annakaz Sep 23, 2019
2ae3fb0
Remove exchange tests
annakaz Sep 23, 2019
ce4bedc
Add comment about contractkit
annakaz Sep 23, 2019
e4eebc7
Merge branch 'master' into annakaz/ex-rate-notification-service
annakaz Sep 24, 2019
211c973
Add web3 provider urls for integration and alfajores
annakaz Sep 24, 2019
702df1b
Merge branch 'annakaz/ex-rate-notification-service' of github.com:cel…
annakaz Sep 24, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/notification-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Next, run this task to build, configure, and start the service:

Deploy your app. The project will be built automatically by Google Cloud Build:

yarn deploy:{ENVIRONMENT}
yarn deploy -n {ENVIRONMENT}

Current supported environments are production, integration, staging-argentina

Expand Down
3 changes: 3 additions & 0 deletions packages/notification-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"deploy": "./deploy.sh"
},
"dependencies": {
"@celo/utils": "^0.0.6-beta5",
jmrossy marked this conversation as resolved.
Show resolved Hide resolved
"@celo/walletkit": "^0.0.14",
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like we shouldn't add a dependency on walletkit here given that we want to deprecate that in favor of contract kit anyway. Can you change this?

Copy link
Contributor Author

@annakaz annakaz Sep 17, 2019

Choose a reason for hiding this comment

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

This is to be consistent with the mobile app, which uses walletkit: https://github.com/celo-org/celo-monorepo/blob/master/packages/mobile/src/exchange/actions.ts#L6
contractkit doesn't have the same getExchangeRate() function, so using contractkit would mean pasting over the code in walletkit.

I created an issue to migrate this and the mobile app away from walletkit after the exchange (or another util where this and the mobile app can share logic) is implemented in contractkit. #1026

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated ContractKit with the needed functionality
#1083

"async-polling": "^0.2.1",
"bignumber.js": "^7.2.0",
"dotenv": "^6.0.0",
Expand All @@ -27,6 +29,7 @@
"i18next": "^12.1.0",
"node-fetch": "^2.2.0",
"utf8": "^3.0.0",
"web3": "1.0.0-beta.37",
"web3-eth-abi": "1.0.0-beta.37",
"web3-utils": "1.0.0-beta.37"
},
Expand Down
4 changes: 4 additions & 0 deletions packages/notification-service/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const DEFAULT_LOCALE = process.env.DEFAULT_LOCALE
export const POLLING_INTERVAL = Number(process.env.POLLING_INTERVAL) || 1000
export const NOTIFICATIONS_TTL_MS = Number(process.env.NOTIFICATION_TTL_MS) || 3600 * 1000 * 24 * 7 // 1 week in milliseconds

export const EXCHANGE_POLLING_INTERVAL =
Number(process.env.EXCHANGE_POLLING_INTERVAL) || 30 * 60 * 1000 // 30 minutes in milliseconds
export const WEB3_PROVIDER_URL = process.env.WEB3_PROVIDER_URL || 'http://34.83.137.48:8545' // Default to integration transaction node
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: this ip seems likely to change as integration churns right? I don't feel strongly but I'd prefer to have it just error if it can find a web3 provider value. Or, even better, have it skip exchange rate polling.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea. Updated to skip exchange polling if it can't find a web3 provider value


export enum NotificationTypes {
PAYMENT_RECEIVED = 'PAYMENT_RECEIVED',
PAYMENT_REQUESTED = 'PAYMENT_REQUESTED',
Expand Down
45 changes: 45 additions & 0 deletions packages/notification-service/src/exchange/exchangeQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { CURRENCY_ENUM } from '@celo/utils'
import { ContractUtils } from '@celo/walletkit'
import BigNumber from 'bignumber.js'
import Web3 from 'web3'
import { WEB3_PROVIDER_URL } from '../config'
import { writeExchangeRatePair } from '../firebase'

// Amounts to estimate the exchange rate, as the rate varies based on transaction size
const SELL_AMOUNTS = {
[CURRENCY_ENUM.DOLLAR]: new BigNumber(10000 * 1000000000000000000), // 100 dollars
[CURRENCY_ENUM.GOLD]: new BigNumber(10 * 1000000000000000000), // 10 gold
}

export async function handleExchangeQuery(web3Instance: Web3) {
const fetchTime = Date.now().toString()
const [dollarMakerRate, goldMakerRate] = await Promise.all([
getExchangeRate(CURRENCY_ENUM.DOLLAR, web3Instance),
getExchangeRate(CURRENCY_ENUM.GOLD, web3Instance),
])

writeExchangeRatePair(CURRENCY_ENUM.DOLLAR, dollarMakerRate.toString(), fetchTime)
writeExchangeRatePair(CURRENCY_ENUM.GOLD, goldMakerRate.toString(), fetchTime)
}

export async function getExchangeRate(makerToken: CURRENCY_ENUM, web3Instance: Web3) {
const rate = await ContractUtils.getExchangeRate(
web3Instance,
makerToken,
SELL_AMOUNTS[makerToken]
)
return rate
}

let web3: Web3
export function getWeb3Instance(): Web3 {
if (web3) {
if (web3.eth.net.isListening()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

And if not?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

// Already connected
return web3
}
}
const httpProvider = new Web3.providers.HttpProvider(WEB3_PROVIDER_URL)
web3 = new Web3(httpProvider)
return web3
}
19 changes: 19 additions & 0 deletions packages/notification-service/src/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CURRENCY_ENUM } from '@celo/utils'
import * as admin from 'firebase-admin'
import i18next from 'i18next'
import { Currencies } from './blockscout/transfers'
Expand All @@ -7,6 +8,7 @@ let database: admin.database.Database
let registrationsRef: admin.database.Reference
let lastBlockRef: admin.database.Reference
let pendingRequestsRef: admin.database.Reference
let exchangeRatesRef: admin.database.Reference

export interface Registrations {
[address: string]:
Expand Down Expand Up @@ -41,6 +43,12 @@ interface PendingRequests {
[uid: string]: PaymentRequest
}

interface ExchangeRateObject {
makerToken: CURRENCY_ENUM
exchangeRate: string
timestamp: string
}

let registrations: Registrations = {}
let lastBlockNotified: number = -1
let pendingRequests: PendingRequests = {}
Expand Down Expand Up @@ -68,6 +76,7 @@ export function initializeDb() {
registrationsRef = database.ref('/registrations')
lastBlockRef = database.ref('/lastBlockNotified')
pendingRequestsRef = database.ref('/pendingRequests')
exchangeRatesRef = database.ref('/exchangeRates')

// Attach to the registration ref to keep local registrations mapping up to date
registrationsRef.on(
Expand Down Expand Up @@ -139,6 +148,16 @@ export function setPaymentRequestNotified(uid: string): Promise<void> {
return database.ref(`/pendingRequests/${uid}`).update({ notified: true })
}

export function writeExchangeRatePair(
makerToken: CURRENCY_ENUM,
exchangeRate: string,
timestamp: string
) {
const exchangeRateRecord: ExchangeRateObject = { makerToken, exchangeRate, timestamp }
exchangeRatesRef.push(exchangeRateRecord)
console.debug('Recorded exchange rate ', exchangeRateRecord)
}

export function setLastBlockNotified(newBlock: number): Promise<void> | undefined {
if (newBlock <= lastBlockNotified) {
console.debug('Block number less than latest, skipping latestBlock update.')
Expand Down
8 changes: 7 additions & 1 deletion packages/notification-service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import express from 'express'
import * as admin from 'firebase-admin'
import { ENVIRONMENT, FIREBASE_DB, getFirebaseAdminCreds, PORT, VERSION } from './config'
import { getLastBlockNotified, initializeDb as initializeFirebaseDb } from './firebase'
import { notificationPolling } from './polling'
import { exchangePolling, notificationPolling } from './polling'

console.info('Service starting with environment, version:', ENVIRONMENT, VERSION)
const START_TIME = Date.now()
Expand Down Expand Up @@ -58,3 +58,9 @@ initializeFirebaseDb()
*/
console.info('Starting Blockscout polling')
notificationPolling.run()

/**
* Start polling the Exchange contract
*/
console.info('Starting Exchange contract polling')
exchangePolling.run()
14 changes: 13 additions & 1 deletion packages/notification-service/src/polling.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import AsyncPolling from 'async-polling'
import { handleTransferNotifications } from './blockscout/transfers'
import { POLLING_INTERVAL } from './config'
import { EXCHANGE_POLLING_INTERVAL, POLLING_INTERVAL } from './config'
import { getWeb3Instance, handleExchangeQuery } from './exchange/exchangeQuery'
import { handlePaymentRequests } from './handlers'

export const notificationPolling = AsyncPolling(async (end) => {
Expand All @@ -13,3 +14,14 @@ export const notificationPolling = AsyncPolling(async (end) => {
end()
}
}, POLLING_INTERVAL)

export const exchangePolling = AsyncPolling(async (end) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm slightly concerned about the impact of having two things polling at the same time given that JS is single threaded, though admittedly this one doesn't run often. If you can confirm that it doesn't seem to interfere with the notification polling then I think we're good

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From running locally, this doesn't seem to interfere, but I'm going to test deploying this to integration to confirm

try {
const web3 = await getWeb3Instance()
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit, any reason to get web3 here given that it's in the same file as handleExchangeQuery?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

await handleExchangeQuery(web3)
} catch (e) {
console.error('Exchange polling failed', e)
} finally {
end()
}
}, EXCHANGE_POLLING_INTERVAL)
26 changes: 26 additions & 0 deletions packages/notification-service/test/exchange.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CURRENCY_ENUM } from '@celo/utils'
import Web3 from 'web3'
import { getExchangeRate, getWeb3Instance } from '../src/exchange/exchangeQuery'

describe('getExchangeRate', () => {
let web3: Web3
let dollarMakerRate: number
let goldMakerRate: number
it('should fetch a working web3 instance', async () => {
web3 = await getWeb3Instance()
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you mocking web3 for this? if not, I recommend we do.
If you need a semi-realistic web3 mock, you can use the one in mobile/__mocks__

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After moving the getExchangeRate function to ContractKit, these tests don't improve coverage, as they are directly testing a function in ContractKit. So I removed this

expect(await web3.eth.net.isListening()).toBeTruthy()
})
it('should fetch a positive dollar maker rate', async () => {
dollarMakerRate = Number(await getExchangeRate(CURRENCY_ENUM.DOLLAR, web3))
expect(dollarMakerRate).toBeGreaterThan(0)
})
it('should fetch a positive gold maker rate', async () => {
goldMakerRate = Number(await getExchangeRate(CURRENCY_ENUM.GOLD, web3))
expect(goldMakerRate).toBeGreaterThan(0)
})
it('should have rates within a 10% spread', async () => {
const spread = dollarMakerRate * goldMakerRate
expect(spread).toBeGreaterThan(0.9)
expect(spread).toBeLessThan(1.1)
})
})