Skip to content
This repository has been archived by the owner on May 19, 2023. It is now read-only.

Commit

Permalink
feat: add multi currency support (#303)
Browse files Browse the repository at this point in the history
* feat: add `token` column to billing-price table
adjust offer and agreement event handlers for multi currency

* feat: update agreement-reference computation
adjust tests

* feat: update topics for storage sc

* fix: event log

* feat: implement nested query for aggregation of avg billing price
converted to specific currency using `rates`
remove old billing price avg calculation
adjust `avg-price` filter in offer hook
add tests

* chore: few improvements

* chore: remove avg-price from offer model

* chore: adjust tests

* chore: fix casting

* chore: make query prettier

* feat: add `token` column to agreement model
adjust event handler

* feat: implement service for avg price min max value retrieving

* feat: fix offer hooks

* feat: add additional field `accepted-currency` to offers after hook

* feat: change avg-billing-price method from find to get

* feat: fix pr comments

* feat: sort the billing plans for offer in offer hook

* feat: fix rebase conflicts

* feat: add rate fk to billing price models
adjust billing price creation in event handler
simplify avg-billing-price query

* feat: add fk to rates on staking models
simplify stake aggregation query
add virtual field accepted-currency to offer

* feat: simplify min/max avg billing price query

* feat: remove unused import

* feat: fix to usd conversion for avg billing price and staking
create storage utils

* fix: to wei conversion by casting to real

* feat: improve test for avg billing price calculation

* feat: adjust readme
small refactor

* fix: avg price query string

* chore: fix sonar cloud

* feat: fix overwriting between sequelize scope and conditions in before hook

* chore: add todos

* chore: remove unused var

* feat: try to resolve sonar cloud error

* chore: remove todo

* chore: add escape for offer custome queries

* feat: fix pr comments

* docs: adjust readme

* chore; remove unused var

* chore: fix linter
  • Loading branch information
nduchak authored Sep 30, 2020
1 parent 51de8e6 commit 4223106
Show file tree
Hide file tree
Showing 17 changed files with 355 additions and 111 deletions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,39 @@ It has following schema:
]
```

```
GET: /storage/v0/stakes'
```

Returns JSON that represent all stakes for each account.

It has following schema:

```json5
[
{
"total": "number",
"symbol": "string", // token symbol
"token": "string", // token address
"account": "string",
}
]
```

```
GET: /storage/v0/avgBillingPrice
```

Return min/max average billing price converted to USD.


```json5
{
min: 'number',
max: 'number'
}
```

### Rates

API that caches conversion rates currently for RBTC and RIF Token.
Expand Down
1 change: 0 additions & 1 deletion config/default.json5
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@
polling: true
}
}

},

// Settings for RNS service related function
Expand Down
2 changes: 1 addition & 1 deletion src/blockchain/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ export class PollingEventsEmitter extends BaseEventsEmitter {
return
}

this.logger.info(`Checking new events between blocks ${lastFetchedBlockNumber}-${currentBlock}`)
this.logger.info(`Checking new events between blocks ${lastFetchedBlockNumber}-${currentBlock.number}`)
const events = await this.contract.getPastEvents('allEvents', {
fromBlock: (lastFetchedBlockNumber as number) + 1, // +1 because both fromBlock and toBlock is "or equal"
toBlock: currentBlock.number,
Expand Down
11 changes: 7 additions & 4 deletions src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as Parser from '@oclif/parser'
import { EventData } from 'web3-eth-contract'
import { Eth } from 'web3-eth'

import type { AgreementService, OfferService, StakeService } from './services/storage'
import type { AvgBillingPriceService, AgreementService, OfferService, StakeService } from './services/storage'
import type { RatesService } from './services/rates'
import type { RnsBaseService } from './services/rns'
import { ConfirmatorService } from './blockchain/confirmator'
Expand All @@ -28,6 +28,7 @@ export enum ServiceAddresses {
RNS_SOLD = '/rns/v0/sold',
RNS_OFFERS = '/rns/v0/offers',
STORAGE_OFFERS = '/storage/v0/offers',
AVG_BILLING_PRICE = '/storage/v0/avgBillingPrice',
STORAGE_AGREEMENTS = '/storage/v0/agreements',
STORAGE_STAKES = '/storage/v0/stakes',
XR = '/rates/v0/',
Expand All @@ -39,6 +40,7 @@ export enum ServiceAddresses {
// A mapping of service names to types. Will be extended in service files.
interface ServiceTypes {
[ServiceAddresses.STORAGE_OFFERS]: OfferService & ServiceAddons<any>
[ServiceAddresses.AVG_BILLING_PRICE]: AvgBillingPriceService & ServiceAddons<any>
[ServiceAddresses.STORAGE_AGREEMENTS]: AgreementService & ServiceAddons<any>
[ServiceAddresses.STORAGE_STAKES]: StakeService & ServiceAddons<any>
[ServiceAddresses.XR]: RatesService & ServiceAddons<any>
Expand Down Expand Up @@ -166,14 +168,15 @@ export interface Config {

// Settings for Storage service related function
storage?: {

// Sets if Storage service should be enabled
enabled?: boolean

// Supported tokens and their addresses
tokens?: {
[key: string]: SupportedTokens
}

// Sets if Storage service should be enabled
enabled?: boolean

// Staking contract options
staking?: BlockchainServiceOptions

Expand Down
9 changes: 5 additions & 4 deletions src/services/storage/handlers/agreement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ const logger = loggingFactory('storage:handler:request')

const handlers = {
async NewAgreement (event: EventData, { agreementService }: StorageServices, eth: Eth): Promise<void> {
const { provider: offerId, billingPeriod: period } = event.returnValues
const id = soliditySha3(event.returnValues.agreementCreator, ...event.returnValues.dataReference)
const { provider: offerId, billingPeriod: period, token: tokenAddress } = event.returnValues
const id = soliditySha3(event.returnValues.agreementCreator, ...event.returnValues.dataReference, tokenAddress)
const dataReference = decodeByteArray(event.returnValues.dataReference)

const plan = await BillingPlan.findOne({ where: { offerId, period } })
const plan = await BillingPlan.findOne({ where: { offerId, period, tokenAddress } })

if (!plan) {
throw new EventError(`Price for period ${period} and offer ${offerId} not found when creating new request ${id}`, 'RequestMade')
throw new EventError(`Price for period ${period}, token ${tokenAddress} and offer ${offerId} not found when creating new request ${id}`, 'RequestMade')
}

const data = {
Expand All @@ -34,6 +34,7 @@ const handlers = {
size: event.returnValues.size,
billingPeriod: event.returnValues.billingPeriod,
billingPrice: plan.price,
tokenAddress,
availableFunds: event.returnValues.availableFunds,
lastPayout: await getBlockDate(eth, event.blockNumber)
}
Expand Down
33 changes: 13 additions & 20 deletions src/services/storage/handlers/offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,35 @@ import { Handler } from '../../../definitions'
import { OfferService, StorageServices } from '../index'
import { decodeByteArray, wrapEvent } from '../../../utils'
import { EventError } from '../../../errors'
import { getTokenSymbol } from '../utils'

const logger = loggingFactory('storage:handler:offer')

function updatePrices (offer: Offer, period: BigNumber, price: BigNumber): Promise<BillingPlan> {
function updatePrices (offer: Offer, period: BigNumber, price: BigNumber, tokenAddress: string): Promise<BillingPlan> {
const {
plans,
provider
} = offer

const billingPlan = plans && plans.find(value => new BigNumber(value.period).eq(period))
const billingPlan = plans && plans.find(value => new BigNumber(value.period).eq(period) && tokenAddress === value.tokenAddress)
logger.info(`Updating period ${period} to price ${price} (ID: ${provider})`)

if (billingPlan) {
billingPlan.price = price
return billingPlan.save()
} else {
const newBillingPlanEntity = new BillingPlan({ period, price, offerId: provider })
const tokenSymbol = getTokenSymbol(tokenAddress).toLowerCase()
const newBillingPlanEntity = new BillingPlan({
period,
price,
offerId: provider,
tokenAddress,
rateId: tokenSymbol
})
return newBillingPlanEntity.save()
}
}

export function calculateAverage (plans: BillingPlan[]): number {
return plans.map(({ price, period }: BillingPlan) => {
const priceMBPPeriod = price
const priceGBPPeriod = priceMBPPeriod.times(1024)
const priceGBPSec = priceGBPPeriod.div(period)
return priceGBPSec.times(3600 * 24)
}).reduce((sum, x) => sum.plus(x)).toNumber() / plans.length
}

const handlers: { [key: string]: Function } = {
async TotalCapacitySet (event: EventData, offer: Offer, offerService: OfferService): Promise<void> {
offer.totalCapacity = event.returnValues.capacity
Expand Down Expand Up @@ -71,14 +70,8 @@ const handlers: { [key: string]: Function } = {
throw new EventError(`Unknown message flag ${flag}!`, event.event)
}
},
async BillingPlanSet ({ returnValues: { period, price } }: EventData, offer: Offer, offerService: OfferService): Promise<void> {
const plan = await updatePrices(offer, period, price)

const updatedPlans = [...offer.plans || [], plan]

const newAvgPrice = updatedPlans?.length && calculateAverage(updatedPlans)
offer.averagePrice = newAvgPrice || 0
offer.save()
async BillingPlanSet ({ returnValues: { period, price, token } }: EventData, offer: Offer, offerService: OfferService): Promise<void> {
await updatePrices(offer, period, price, token)

if (offerService.emit) {
const freshOffer = await Offer.findByPk(offer.provider) as Offer
Expand Down
20 changes: 3 additions & 17 deletions src/services/storage/handlers/stake.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
import { EventData } from 'web3-eth-contract'
import BigNumber from 'bignumber.js'
import config from 'config'

import { loggingFactory } from '../../../logger'
import { Handler, SupportedTokens } from '../../../definitions'
import { Handler } from '../../../definitions'
import { StorageServices } from '../index'
import StakeModel from '../models/stake.model'
import { getTokenSymbol } from '../utils'

const logger = loggingFactory('storage:handler:stake')

/**
* Make a call to ERC20 token SC and return token symbol
* Return `rbtc` for ZERO_ADDRESS
* @param tokenContractAddress
* @returns {SupportedTokens} token symbol
*/
function getTokenSymbol (tokenContractAddress: string): SupportedTokens {
if (!config.has(`storage.tokens.${tokenContractAddress}`)) {
throw new Error(`Token at ${tokenContractAddress} not supported`)
}

return config.get<SupportedTokens>(`storage.tokens.${tokenContractAddress}`)
}

/**
* Find or create stake
* @param account
Expand All @@ -35,7 +21,7 @@ async function findOrCreateStake (account: string, token: string): Promise<Stake
if (stake) {
return stake
}
const symbol = getTokenSymbol(token)
const symbol = getTokenSymbol(token).toLowerCase()
return StakeModel.create({ account, token, symbol, total: 0 })
}

Expand Down
84 changes: 69 additions & 15 deletions src/services/storage/hooks/offers.hooks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,67 @@
import { Op, literal, Sequelize } from 'sequelize'
import { HookContext } from '@feathersjs/feathers'
import { disallow, discard } from 'feathers-hooks-common'
import { hooks } from 'feathers-sequelize'
import { Op, literal, Sequelize } from 'sequelize'

import { getStakesAggregateQuery } from '../models/offer.model'
import BillingPlan from '../models/billing-plan.model'
import Agreement from '../models/agreement.model'
import Offer, { getBillingPriceAvgQuery, getStakesAggregateQuery } from '../models/offer.model'
import dehydrate = hooks.dehydrate

/**
* This hook will sort billing plans array by period ASC
* @param context
*/
function sortBillingPlansHook (context: HookContext): HookContext {
context.result.forEach((offer: Offer & { acceptedCurrencies: string[] }) => {
offer.plans = offer.plans
.sort((planA, planB) => planA.period.minus(planB.period).toNumber())
})
return context
}

/**
* average price filter query
* @param sequelize
* @param context
* @param averagePrice
*/
function averagePriceFilter (
sequelize: Sequelize,
context: HookContext,
averagePrice: { min: number | string, max: number | string }
): void {
const minPrice = sequelize.escape(averagePrice.min)
const maxPrice = sequelize.escape(averagePrice.max)
const rawQ = `avgBillingPrice BETWEEN ${minPrice} AND ${maxPrice}`
// We should use Op.and to prevent overwriting the scope values
context.params.sequelize.where[Op.and] = [
...context.params.sequelize.where[Op.and] || [],
literal(rawQ)
]
}

/**
* total capacity filter query
* @param sequelize
* @param context
* @param totalCapacity
*/
function totalCapacityFilter (
sequelize: Sequelize,
context: HookContext,
totalCapacity: { min: number | string, max: number | string }
): void {
const minCap = sequelize.escape(totalCapacity.min)
const maxCap = sequelize.escape(totalCapacity.max)
const rawQ = `cast(totalCapacity as integer) BETWEEN ${minCap} AND ${maxCap}`
// We should use Op.and to prevent overwriting the scope values
context.params.sequelize.where[Op.and] = [
...context.params.sequelize.where[Op.and] || [],
literal(rawQ)
]
}

export default {
before: {
all: [
Expand All @@ -26,13 +80,13 @@ export default {
}
],
find: [
async (context: HookContext) => {
(context: HookContext) => {
if (context.params.query && !context.params.query.$limit) {
const { averagePrice, totalCapacity, periods, provider } = context.params.query
const sequelize = context.app.get('sequelize') as Sequelize
const sequelize = context.app.get('sequelize')

const aggregateLiteral = await getStakesAggregateQuery(sequelize, 'usd')
context.params.sequelize = {
...context.params.sequelize,
raw: false,
nest: true,
include: [
Expand All @@ -44,8 +98,13 @@ export default {
model: Agreement
}
],
attributes: [[aggregateLiteral, 'totalStakedUSD']],
order: [literal('totalStakedUSD DESC'), ['plans', 'period', 'ASC']],
attributes: {
include: [
[getBillingPriceAvgQuery(sequelize, 'usd'), 'avgBillingPrice'],
[getStakesAggregateQuery(sequelize, 'usd'), 'totalStakedUSD']
]
},
order: [literal('totalStakedUSD DESC')],
where: {}
}

Expand All @@ -56,16 +115,11 @@ export default {
}

if (averagePrice) {
context.params.sequelize.where.averagePrice = {
[Op.between]: [averagePrice.min, averagePrice.max]
}
averagePriceFilter(sequelize, context, averagePrice)
}

if (totalCapacity) {
const minCap = sequelize.escape(totalCapacity.min)
const maxCap = sequelize.escape(totalCapacity.max)
const rawQ = `cast(totalCapacity as integer) BETWEEN ${minCap} AND ${maxCap}`
context.params.sequelize.where.totalCapacity = literal(rawQ)
totalCapacityFilter(sequelize, context, totalCapacity)
}

if (periods?.length) {
Expand All @@ -83,7 +137,7 @@ export default {

after: {
all: [dehydrate(), discard('agreements')],
find: [],
find: [sortBillingPlansHook],
get: [],
create: [],
update: [],
Expand Down
Loading

0 comments on commit 4223106

Please sign in to comment.