Skip to content

Commit

Permalink
Collect fees handler (#491)
Browse files Browse the repository at this point in the history
* Create CollectFeesHandler for admin. Added tests.

* Print balance.

* Add debug log.

* Added regex validation for ethereum addresses.

* Added checks for zero address. Refactor transfer call.

* Debug logs.

* Tweak for addresses length in regex.

* fix.

* Added debug logs for handle function and refactored test for fee amount.

* Fix call.

* Fix provider wallet.

* Fix breaking change for provider wallet.

* Print error.

* Replace ReadableString with Readable.

* Added more debug logs.

* More debug.

* Use readStream instead of streamToString.

* Move test position.

* increase timeout.

* remove message from handler response.

* store tx receipt in a separate var.

* Fix unit for parseUnit.

* Fix wallet.

* Change token address.

* Print addresses.

* Changed wallet private key.

* Added logs for expected failure cases.

* Fix log for balance.

* cleanup.

* Fix review part 1.

* Fix review part 2.

* Fix review part 3.

* tweak.

* Resolve conditions. Refactor code.

* Fix review.

* Updated provider wallet.

* Make tokenAmount optional.
  • Loading branch information
mariacarmina authored Aug 13, 2024
1 parent a91157e commit f0ec9f4
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 10 deletions.
12 changes: 12 additions & 0 deletions src/@types/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export interface AdminCommand extends Command {
signature: string
}

export interface AdminCollectFeesHandlerResponse {
tx: string
message: string
}

export interface DownloadURLCommand extends Command {
fileObject: any
aes_encrypted_key?: string // if not present it means download without encryption
Expand Down Expand Up @@ -116,6 +121,13 @@ export interface AdminReindexTxCommand extends AdminCommand {
txId: string
}

export interface AdminCollectFeesCommand extends AdminCommand {
tokenAddress: string
chainId: number
tokenAmount?: number
destinationAddress: string
}

export interface AdminReindexChainCommand extends AdminCommand {
chainId: number
}
Expand Down
163 changes: 163 additions & 0 deletions src/components/core/admin/collectFeesHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { AdminHandler } from './adminHandler.js'
import {
AdminCollectFeesCommand,
AdminCollectFeesHandlerResponse
} from '../../../@types/commands.js'
import { P2PCommandResponse } from '../../../@types/OceanNode.js'
import {
ValidateParams,
buildInvalidParametersResponse,
buildErrorResponse,
buildInvalidRequestMessage,
validateCommandParameters
} from '../../httpRoutes/validateCommands.js'
import {
getConfiguration,
checkSupportedChainId,
Blockchain
} from '../../../utils/index.js'
import { parseUnits, Contract, ZeroAddress, isAddress, Wallet } from 'ethers'
import ERC20Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json' assert { type: 'json' }
import { CORE_LOGGER } from '../../../utils/logging/common.js'
import { Readable } from 'stream'

export class CollectFeesHandler extends AdminHandler {
validate(command: AdminCollectFeesCommand): ValidateParams {
if (
!validateCommandParameters(command, [
'chainId',
'tokenAddress',
'tokenAmount',
'destinationAddress'
])
) {
return buildInvalidRequestMessage(
`Missing chainId field for command: "${command}".`
)
}
if (!isAddress(command.tokenAddress) || !isAddress(command.destinationAddress)) {
const msg: string = `Invalid format for token address or destination address.`
CORE_LOGGER.error(msg)
return buildInvalidRequestMessage(msg)
}
return super.validate(command)
}

async handle(task: AdminCollectFeesCommand): Promise<P2PCommandResponse> {
const validation = this.validate(task)
if (!validation.valid) {
return buildInvalidParametersResponse(validation)
}
const config = await getConfiguration()
if (task.node && task.node !== config.keys.peerId.toString()) {
const msg: string = `Cannot run this command ${JSON.stringify(
task
)} on a different node.`
CORE_LOGGER.error(msg)
return buildErrorResponse(msg)
}
const checkChainId = await checkSupportedChainId(task.chainId)
if (!checkChainId.validation) {
return buildErrorResponse(
`Chain ID ${task.chainId} is not supported in the node's config`
)
}

try {
const { rpc, network, chainId, fallbackRPCs } =
config.supportedNetworks[task.chainId]
const blockchain = new Blockchain(rpc, network, chainId, fallbackRPCs)
const provider = blockchain.getProvider()
const providerWallet = blockchain.getSigner() as Wallet
const providerWalletAddress = await providerWallet.getAddress()
const ammountInEther = task.tokenAmount
? parseUnits(task.tokenAmount.toString(), 'ether')
: await provider.getBalance(providerWalletAddress)

let receipt
if (task.tokenAddress.toLowerCase() === ZeroAddress) {
if (
(await provider.getBalance(providerWalletAddress)) <
(await blockchain.calculateGasCost(
task.destinationAddress.toLowerCase(),
ammountInEther
))
) {
const msg: string = `Amount too high to transfer native token! Balance: ${await provider.getBalance(
providerWalletAddress
)} vs. amount provided: ${ammountInEther}`
CORE_LOGGER.error(msg)
return buildErrorResponse(msg)
}

receipt = await blockchain.sendTransaction(
providerWallet,
task.destinationAddress.toLowerCase(),
ammountInEther
)
} else {
const token = new Contract(
task.tokenAddress.toLowerCase(),
ERC20Template.abi,
providerWallet
)
const tokenAmount = task.tokenAmount
? parseUnits(task.tokenAmount.toString(), 'ether')
: await token.balanceOf(providerWalletAddress)

if ((await token.balanceOf(providerWalletAddress)) < tokenAmount) {
const msg: string = `Amount too high to transfer! Balance: ${await token.balanceOf(
providerWalletAddress
)} vs. amount provided: ${tokenAmount}`
CORE_LOGGER.error(msg)
return buildErrorResponse(msg)
}
const tx = await token.transfer(
task.destinationAddress.toLowerCase(),
tokenAmount
)
receipt = await tx.wait()
}
if (!receipt) {
const msg: string = `Receipt does not exist`
CORE_LOGGER.error(msg)
return {
stream: null,
status: {
httpStatus: 404,
error: msg
}
}
}
if (receipt.status !== 1) {
const msg: string = `Reverted transaction: ${JSON.stringify(receipt.logs)}`
CORE_LOGGER.error(msg)
return {
stream: null,
status: {
httpStatus: 404,
error: msg
}
}
}
const response: AdminCollectFeesHandlerResponse = {
tx: receipt.hash,
message: 'Fees successfully transfered to admin!'
}
return {
status: { httpStatus: 200 },
stream: Readable.from(JSON.stringify(response))
}
} catch (e) {
const msg: string = `Error in collecting provider fees: ${e}`
CORE_LOGGER.error(msg)
return {
stream: null,
status: {
httpStatus: 500,
error: msg
}
}
}
}
}
2 changes: 2 additions & 0 deletions src/components/core/handler/coreHandlersRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { StopNodeHandler } from '../admin/stopNodeHandler.js'
import { ReindexTxHandler } from '../admin/reindexTxHandler.js'
import { ReindexChainHandler } from '../admin/reindexChainHandler.js'
import { IndexingThreadHandler } from '../admin/IndexingThreadHandler.js'
import { CollectFeesHandler } from '../admin/collectFeesHandler.js'

export type HandlerRegistry = {
handlerName: string // name of the handler
Expand Down Expand Up @@ -117,6 +118,7 @@ export class CoreHandlersRegistry {
PROTOCOL_COMMANDS.HANDLE_INDEXING_THREAD,
new IndexingThreadHandler(node)
)
this.registerCoreHandler(PROTOCOL_COMMANDS.COLLECT_FEES, new CollectFeesHandler(node))
}

public static getInstance(node: OceanNode): CoreHandlersRegistry {
Expand Down
3 changes: 1 addition & 2 deletions src/components/core/utils/feesHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,10 +471,9 @@ export async function checkFee(
* @returns the wallet
*/
export async function getProviderWallet(chainId?: string): Promise<ethers.Wallet> {
const wallet: ethers.Wallet = new ethers.Wallet(
return new ethers.Wallet(
Buffer.from((await getConfiguration()).keys.privateKey).toString('hex')
)
return wallet
}
export async function getProviderWalletAddress(): Promise<string> {
return (await getProviderWallet()).address
Expand Down
87 changes: 83 additions & 4 deletions src/test/integration/operationsDashboard.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { assert, expect } from 'chai'
import { Readable } from 'stream'
import { Signer, JsonRpcProvider, ethers } from 'ethers'
import { Signer, JsonRpcProvider, ethers, Contract, parseUnits } from 'ethers'
import { Database } from '../../components/database/index.js'
import { OceanNode } from '../../OceanNode.js'
import { RPCS } from '../../@types/blockchain.js'
Expand All @@ -24,15 +24,16 @@ import {
INDEXER_CRAWLING_EVENTS
} from '../../utils/index.js'
import { OceanNodeConfig } from '../../@types/OceanNode.js'

import { DEVELOPMENT_CHAIN_ID } from '../../utils/address.js'
import ERC20Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json' assert { type: 'json' }
import { DEVELOPMENT_CHAIN_ID, getOceanArtifactsAdresses } from '../../utils/address.js'
import {
AdminReindexChainCommand,
AdminReindexTxCommand,
AdminStopNodeCommand,
JobStatus,
IndexingCommand,
StartStopIndexingCommand
StartStopIndexingCommand,
AdminCollectFeesCommand
} from '../../@types/commands.js'
import { StopNodeHandler } from '../../components/core/admin/stopNodeHandler.js'
import { ReindexTxHandler } from '../../components/core/admin/reindexTxHandler.js'
Expand All @@ -49,6 +50,8 @@ import {
import { getCrawlingInterval } from '../../components/Indexer/utils.js'
import { ReindexTask } from '../../components/Indexer/crawlerThread.js'
import { create256Hash } from '../../utils/crypt.js'
import { CollectFeesHandler } from '../../components/core/admin/collectFeesHandler.js'
import { getProviderFeeToken } from '../../components/core/utils/feesHandler.js'

describe('Should test admin operations', () => {
let config: OceanNodeConfig
Expand All @@ -67,6 +70,10 @@ describe('Should test admin operations', () => {
'0xc594c6e5def4bab63ac29eed19a134c130388f74f019bc74b8f4389df2837a58',
provider
)
const destinationWallet = new ethers.Wallet(
'0xef4b441145c1d0f3b4bc6d61d29f5c6e502359481152f869247c7a4244d45209',
provider
)

const mockSupportedNetworks: RPCS = getMockSupportedNetworks()

Expand Down Expand Up @@ -121,6 +128,78 @@ describe('Should test admin operations', () => {
assert(validationResponse.valid === true, 'validation for stop node command failed')
})

it('should test command for collect fees', async function () {
this.timeout(DEFAULT_TEST_TIMEOUT * 2)
// -----------------------------------------
// CollectFeesHandler
const collectFeesHandler: CollectFeesHandler = CoreHandlersRegistry.getInstance(
oceanNode
).getHandler(PROTOCOL_COMMANDS.COLLECT_FEES)

const signature = await getSignature(expiryTimestamp.toString())
const collectFeesCommand: AdminCollectFeesCommand = {
command: PROTOCOL_COMMANDS.COLLECT_FEES,
tokenAddress: await getProviderFeeToken(DEVELOPMENT_CHAIN_ID),
chainId: DEVELOPMENT_CHAIN_ID,
tokenAmount: 0.01,
destinationAddress: await destinationWallet.getAddress(),
expiryTimestamp,
signature
}
const validationResponse = collectFeesHandler.validate(collectFeesCommand)
assert(validationResponse, 'invalid collect fees validation response')
assert(
validationResponse.valid === true,
'validation for collect fees command failed'
)
const providerWallet = wallet
const token = new Contract(
collectFeesCommand.tokenAddress.toLowerCase(),
ERC20Template.abi,
providerWallet
)
const balanceBefore = await token.balanceOf(await destinationWallet.getAddress())
expect(collectFeesHandler.validate(collectFeesCommand).valid).to.be.equal(true) // OK
const result = await collectFeesHandler.handle(collectFeesCommand)
expect(result.status.httpStatus).to.be.equal(200) // OK

const obj = await streamToObject(result.stream as Readable)

expect(obj.tx).to.be.not.equal(null) // OK
expect(obj.message).to.be.equal('Fees successfully transfered to admin!') // OK
expect(await token.balanceOf(await destinationWallet.getAddress())).to.be.equal(
balanceBefore + parseUnits(collectFeesCommand.tokenAmount.toString(), 'ether')
)

// Test incorrect values for command: node ID and big amount
const collectFeesCommandWrongNode: AdminCollectFeesCommand = {
command: PROTOCOL_COMMANDS.COLLECT_FEES,
node: 'My peerID', // dummy peer ID
tokenAddress: getOceanArtifactsAdresses().development.Ocean,
chainId: DEVELOPMENT_CHAIN_ID,
tokenAmount: 0.01,
destinationAddress: await wallet.getAddress(),
expiryTimestamp,
signature
}
expect(
(await collectFeesHandler.handle(collectFeesCommandWrongNode)).status.httpStatus
).to.be.equal(400) // NOK

const collectFeesCommandWrongAmount: AdminCollectFeesCommand = {
command: PROTOCOL_COMMANDS.COLLECT_FEES,
tokenAddress: getOceanArtifactsAdresses().development.Ocean,
chainId: DEVELOPMENT_CHAIN_ID,
tokenAmount: 366666666666, // big amount
destinationAddress: await wallet.getAddress(),
expiryTimestamp,
signature
}
expect(
(await collectFeesHandler.handle(collectFeesCommandWrongAmount)).status.httpStatus
).to.be.equal(400) // NOK
})

it('should publish dataset', async function () {
this.timeout(DEFAULT_TEST_TIMEOUT * 2)
publishedDataset = await publishAsset(downloadAsset, wallet as Signer)
Expand Down
7 changes: 7 additions & 0 deletions src/test/unit/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { ComputeInitializeHandler } from '../../components/core/compute/initiali
import { StopNodeHandler } from '../../components/core/admin/stopNodeHandler.js'
import { ReindexTxHandler } from '../../components/core/admin/reindexTxHandler.js'
import { ReindexChainHandler } from '../../components/core/admin/reindexChainHandler.js'
import { CollectFeesHandler } from '../../components/core/admin/collectFeesHandler.js'

describe('Commands and handlers', () => {
it('Check that all supported commands have registered handlers', () => {
Expand Down Expand Up @@ -228,6 +229,12 @@ describe('Commands and handlers', () => {
).getHandler(PROTOCOL_COMMANDS.REINDEX_CHAIN)
expect(reindexChainHandler).to.be.not.equal(null)
// -----------------------------------------
// CollectFeesHandler
const collectFeesHandler: CollectFeesHandler = CoreHandlersRegistry.getInstance(
node
).getHandler(PROTOCOL_COMMANDS.COLLECT_FEES)
expect(collectFeesHandler).to.be.not.equal(null)
// -----------------------------------------
// FileInfoHandler
const fileInfoHandler: FileInfoHandler = CoreHandlersRegistry.getInstance(
node
Expand Down
Loading

0 comments on commit f0ec9f4

Please sign in to comment.