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

Commit

Permalink
feat(storage): staking support (#286)
Browse files Browse the repository at this point in the history
  • Loading branch information
nduchak authored Sep 25, 2020
1 parent fd1c33a commit 7be56f7
Show file tree
Hide file tree
Showing 23 changed files with 599 additions and 132 deletions.
10 changes: 8 additions & 2 deletions config/custom-environment-variables.json5
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@
provider: "RIFM_PROVIDER",
},
storage: {
contractAddress: "RIFM_STORAGE_CONTRACT_ADDR",
startingBlock: "RIFM_STORAGE_STARTING_BLOCK"
storageManager: {
contractAddress: "RIFM_STORAGE_CONTRACT_ADDR",
startingBlock: "RIFM_STORAGE_STARTING_BLOCK"
},
staking: {
contractAddress: "RIFM_STAKING_CONTRACT_ADDR",
startingBlock: "RIFM_STAKING_STARTING_BLOCK"
}
},
rns: {
owner: {
Expand Down
92 changes: 66 additions & 26 deletions config/default.json5
Original file line number Diff line number Diff line change
Expand Up @@ -47,38 +47,78 @@
// Sets if Storage service should be enabled
enabled: true,

// Topics that will be listened to
topics: [
'TotalCapacitySet(address,uint64)',
'BillingPlanSet(address,uint64,uint128)',
'MessageEmitted(address,bytes32[])',
'NewAgreement(bytes32,bytes32[],address,address,uint128,uint64,uint128,uint256)',
'AgreementFundsDeposited(bytes32,uint256)',
'AgreementFundsWithdrawn(bytes32,uint256)',
'AgreementFundsPayout(bytes32,uint256)',
'AgreementStopped(bytes32)',
],

// Specify behavior of EventsEmitter, that retrieves events from blockchain and pass them onwards for further processing.
eventsEmitter: {
// If to use polling strategy, if false then listening is used.
polling: true,
// supported tokens
tokens: {
// Native token use zero address
'0x0000000000000000000000000000000000000000': 'rbtc',
},

// Storage Manager Contract
storageManager: {
// Topics that will be listened to
topics: [
'TotalCapacitySet(address,uint64)',
'BillingPlanSet(address,address,uint64,uint128)',
'MessageEmitted(address,bytes32[])',
'NewAgreement(bytes32[],address,address,uint128,uint64,uint128,address,uint256)',
'AgreementFundsDeposited(bytes32,uint256,address)',
'AgreementFundsWithdrawn(bytes32,uint256,address)',
'AgreementFundsPayout(bytes32,uint256,address)',
'AgreementStopped(bytes32)'
],

// Specify behavior of EventsEmitter, that retrieves events from blockchain and pass them onwards for further processing.
eventsEmitter: {
// If to use polling strategy, if false then listening is used.
polling: true,

// Interval in milliseconds, how often is blockchain checked.
pollingInterval: 5000,

// Interval in milliseconds, how often is blockchain checked.
pollingInterval: 5000,
// Starting block that upon first start of the service, will the blockchain be crawled for the past events.
startingBlock: "genesis",

// Starting block that upon first start of the service, will the blockchain be crawled for the past events.
startingBlock: "genesis",
// Number of blocks that will be waited before passing an event for further processing.
confirmations: 6
},

// Number of blocks that will be waited before passing an event for further processing.
confirmations: 6
// Specify behavior of NewBlockEmitter, that detects new blocks on blockchain.
newBlockEmitter: {
// If to use polling strategy, if false then listening is used.
polling: true
}
},

// Specify behavior of NewBlockEmitter, that detects new blocks on blockchain.
newBlockEmitter: {
// If to use polling strategy, if false then listening is used.
polling: true
// Staking Contract
staking: {
// Topics that will be listened to
topics: [
'Staked(address,uint256,uint256,address,bytes)',
'Unstaked(address,uint256,uint256,address,bytes)',
],

// Specify behavior of EventsEmitter, that retrieves events from blockchain and pass them onwards for further processing.
eventsEmitter: {
// If to use polling strategy, if false then listening is used.
polling: true,

// Interval in milliseconds, how often is blockchain checked.
pollingInterval: 5000,

// Starting block that upon first start of the service, will the blockchain be crawled for the past events.
startingBlock: "genesis",

// Number of blocks that will be waited before passing an event for further processing.
confirmations: 6
},

// Specify behavior of NewBlockEmitter, that detects new blocks on blockchain.
newBlockEmitter: {
// If to use polling strategy, if false then listening is used.
polling: true
}
}

},

// Settings for RNS service related function
Expand Down
11 changes: 9 additions & 2 deletions config/development.json5
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@
provider: "ws://localhost:8545",
},
storage: {
eventsEmitter: {
confirmations: 1
storageManager: {
eventsEmitter: {
confirmations: 1
}
},
staking: {
eventsEmitter: {
confirmations: 1
}
}
},
rates: {
Expand Down
10 changes: 6 additions & 4 deletions config/ganache.json5
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
},
storage: {
enabled: true,
contractAddress: "0x47a2Db5D68751EeAdFBC44851E84AcDB4F7299Cc",
eventsEmitter: {
confirmations: 2
}
storageManager: {
contractAddress: "0x47a2Db5D68751EeAdFBC44851E84AcDB4F7299Cc",
eventsEmitter: {
confirmations: 2
}
},
},
rns: {
enabled: true,
Expand Down
5 changes: 5 additions & 0 deletions config/test.json5
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
provider: 'ws://localhost:8545',
waitBlockCountBeforeConfirmationRemoved: 10
},
storage: {
tokens: {
'0x12345': 'rif'
}
},
rns: {
enabled: true,
batchContractAddress: "0xc0b3b62dd0400e4baa721ddec9b8a384147b23ff", // encoded address used in tests
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"scripts": {
"prepack": "tasegir run --watch node_modules/.bin/oclif-dev -- manifest && sed -i '' 's#\"./src/cli\"#\"./lib/cli\"#g' package.json",
"postpack": "sed -i '' 's#\"./lib/cli\"#\"./src/cli\"#g' package.json",
"bin": "tasegir run --watch ./bin/run -- ",
"bin": "tasegir run ./bin/run -- ",
"compile": "tasegir compile",
"docs": "typedoc --mode modules --excludeNotExported --readme none --excludePrivate --tsconfig ./node_modules/tasegir/src/config/tsconfig.json --exclude 'src/providers/*,test/**/*' --out docs src",
"types-check": "tasegir types-check",
Expand All @@ -60,7 +60,7 @@
"@oclif/parser": "^3.8.5",
"@oclif/plugin-help": "^3.2.0",
"@rsksmart/rif-marketplace-nfts": "~0.1.4",
"@rsksmart/rif-marketplace-storage": "^0.1.0-dev.1",
"@rsksmart/rif-marketplace-storage": "^0.1.0-dev.2",
"@rsksmart/rns-auction-registrar": "1.0.2",
"@rsksmart/rns-reverse": "^1.0.2",
"@rsksmart/rns-rskregistrar": "^1.2.1",
Expand Down
4 changes: 4 additions & 0 deletions src/db-backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { BlockHeader, Eth } from 'web3-eth'
import { AutoStartStopEventEmitter, NEW_BLOCK_EVENT_NAME } from './blockchain/new-block-emitters'
import { DbBackUpConfig } from './definitions'
import { getNewBlockEmitter } from './blockchain/utils'
import { loggingFactory } from './logger'

const logger = loggingFactory('db:backups')

export type BackUpEntry = { name: string, block: { hash: string, number: BigNumber } }

Expand Down Expand Up @@ -74,6 +77,7 @@ export class DbBackUpJob {
if (previousBackUp) {
await fs.promises.unlink(path.resolve(this.backUpConfig.path, previousBackUp.name))
}
logger.info(`Make DB backup on block ${block.number}`)
}
}

Expand Down
19 changes: 17 additions & 2 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 } from './services/storage'
import type { 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 @@ -17,6 +17,8 @@ export enum SupportedServices {
RNS = 'rns'
}

export type SupportedTokens = 'rif' | 'rbtc'

export function isSupportedServices (value: any): value is SupportedServices {
return Object.values(SupportedServices).includes(value)
}
Expand All @@ -27,6 +29,7 @@ export enum ServiceAddresses {
RNS_OFFERS = '/rns/v0/offers',
STORAGE_OFFERS = '/storage/v0/offers',
STORAGE_AGREEMENTS = '/storage/v0/agreements',
STORAGE_STAKES = '/storage/v0/stakes',
XR = '/rates/v0/',
CONFIRMATIONS = '/confirmations',
NEW_BLOCK_EMITTER = '/new-block',
Expand All @@ -37,6 +40,7 @@ export enum ServiceAddresses {
interface ServiceTypes {
[ServiceAddresses.STORAGE_OFFERS]: OfferService & ServiceAddons<any>
[ServiceAddresses.STORAGE_AGREEMENTS]: AgreementService & ServiceAddons<any>
[ServiceAddresses.STORAGE_STAKES]: StakeService & ServiceAddons<any>
[ServiceAddresses.XR]: RatesService & ServiceAddons<any>
[ServiceAddresses.RNS_DOMAINS]: RnsBaseService & ServiceAddons<any>
[ServiceAddresses.RNS_SOLD]: RnsBaseService & ServiceAddons<any>
Expand Down Expand Up @@ -161,9 +165,20 @@ export interface Config {
}

// Settings for Storage service related function
storage?: BlockchainServiceOptions & {
storage?: {
// Supported tokens and their addresses
tokens?: {
[key: string]: SupportedTokens
}

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

// Staking contract options
staking?: BlockchainServiceOptions

// Storage Manager contract options
storageManager?: BlockchainServiceOptions
}

// Settings for RNS service related function
Expand Down
7 changes: 7 additions & 0 deletions src/sequelize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export function sequelizeFactory (): Sequelize {
return new Sequelize(`sqlite:${config.get('db')}`, dbSettings)
}

/**
* consider that the field will be stored as string in the data base,
* so you not be able to use number comparision when querying
* BigNumberStringType for sequelize models
* @param propName
* @constructor
*/
export function BigNumberStringType (propName: string): Partial<ModelAttributeColumnOptions> {
return {
type: DataType.STRING(),
Expand Down
91 changes: 91 additions & 0 deletions src/services/storage/handlers/stake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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 { StorageServices } from '../index'
import StakeModel from '../models/stake.model'

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
* @param token
* @returns {Promise<StakeModel>} stake
*/
async function findOrCreateStake (account: string, token: string): Promise<StakeModel> {
const stake = await StakeModel.findOne({ where: { account, token } })

if (stake) {
return stake
}
const symbol = getTokenSymbol(token)
return StakeModel.create({ account, token, symbol, total: 0 })
}

const handlers = {
async Staked (event: EventData, { stakeService }: StorageServices): Promise<void> {
const { user: account, total, token, amount } = event.returnValues

const stake = await findOrCreateStake(account, token)

stake.total = new BigNumber(stake.total).plus(amount)
await stake.save()
logger.info(`Account ${account} stake amount ${amount}, final balance ${total}`)

if (stakeService.emit) {
stakeService.emit('updated', stake.toJSON())
}
},

async Unstaked (event: EventData, { stakeService }: StorageServices): Promise<void> {
const { user: account, total, token, amount } = event.returnValues

const stake = await StakeModel.findOne({ where: { token, account } })

if (!stake) {
throw new Error(`Stake for account ${account}, token ${token} not exist`)
}

stake.total = new BigNumber(stake.total).minus(amount)
await stake.save()
logger.info(`Account ${account} un-stake amount ${amount}, final balance ${total}`)

if (stakeService.emit) {
stakeService.emit('updated', stake.toJSON())
}
}
}

function isValidEvent (value: string): value is keyof typeof handlers {
return value in handlers
}

const handler: Handler<StorageServices> = {
events: ['Staked', 'Unstaked'],
process (event: EventData, services: StorageServices): Promise<void> {
if (!isValidEvent(event.event)) {
return Promise.reject(new Error(`Unknown event ${event.event}`))
}

return handlers[event.event](event, services)
}
}

export default handler
Loading

0 comments on commit 7be56f7

Please sign in to comment.