diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 403e144..c0f118d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,7 +52,7 @@ jobs: run: | bash -x start_ocean.sh --with-thegraph --skip-subgraph-deploy --no-dashboard 2>&1 > start_ocean.log & env: - CONTRACTS_VERSION: v1.1.3 + CONTRACTS_VERSION: v1.1.4 - run: npm ci diff --git a/package-lock.json b/package-lock.json index c6da12c..26b9805 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "2.0.4", "license": "Apache-2.0", "dependencies": { - "@oceanprotocol/contracts": "^1.1.3", - "@oceanprotocol/lib": "^2.0.0", + "@oceanprotocol/contracts": "^1.1.4", + "@oceanprotocol/lib": "^2.0.2", "cross-fetch": "^3.1.4" }, "devDependencies": { @@ -860,16 +860,16 @@ } }, "node_modules/@oceanprotocol/contracts": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-1.1.3.tgz", - "integrity": "sha512-pn0rm4QKF8sVfDeJVlt18TV4Qj5oGgR/qQNO7O0GO2DQ3q8KHXRS15uRjmLTr5wW1kGcCHcTqEndXEEC7Elzkw==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-1.1.4.tgz", + "integrity": "sha512-fIJjtyj1fxF3GNaITUDaUJbQ2FBCLqB6Hlg72k5SzBK2//yuSPfdZVAqomul0qQjgiKl0jlJRmWVpfer/a5z2g==" }, "node_modules/@oceanprotocol/lib": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.0.0.tgz", - "integrity": "sha512-8MpMAkUG4LbalyN4UCcR+kZmo+Nmk/l22aTG3vQKjUfrF7mq3hvW5ThVlYll/sNarVTI/S086NvuxGX/ypcJuw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.0.2.tgz", + "integrity": "sha512-Vso3lRTqLkuCYvH8tKVZ2NrsVJALYzmO4Pp72IPdhJYevloalJmG5vOEzC/Z6cp4DAMq/Oqlb/zviYkqubsv/Q==", "dependencies": { - "@oceanprotocol/contracts": "^1.1.3", + "@oceanprotocol/contracts": "^1.1.4", "bignumber.js": "^9.0.2", "cross-fetch": "^3.1.5", "crypto-js": "^4.1.1", @@ -15274,16 +15274,16 @@ } }, "@oceanprotocol/contracts": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-1.1.3.tgz", - "integrity": "sha512-pn0rm4QKF8sVfDeJVlt18TV4Qj5oGgR/qQNO7O0GO2DQ3q8KHXRS15uRjmLTr5wW1kGcCHcTqEndXEEC7Elzkw==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-1.1.4.tgz", + "integrity": "sha512-fIJjtyj1fxF3GNaITUDaUJbQ2FBCLqB6Hlg72k5SzBK2//yuSPfdZVAqomul0qQjgiKl0jlJRmWVpfer/a5z2g==" }, "@oceanprotocol/lib": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.0.0.tgz", - "integrity": "sha512-8MpMAkUG4LbalyN4UCcR+kZmo+Nmk/l22aTG3vQKjUfrF7mq3hvW5ThVlYll/sNarVTI/S086NvuxGX/ypcJuw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.0.2.tgz", + "integrity": "sha512-Vso3lRTqLkuCYvH8tKVZ2NrsVJALYzmO4Pp72IPdhJYevloalJmG5vOEzC/Z6cp4DAMq/Oqlb/zviYkqubsv/Q==", "requires": { - "@oceanprotocol/contracts": "^1.1.3", + "@oceanprotocol/contracts": "^1.1.4", "bignumber.js": "^9.0.2", "cross-fetch": "^3.1.5", "crypto-js": "^4.1.1", diff --git a/package.json b/package.json index 369c24e..60d3a5d 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "test-fixed": "TS_NODE_PROJECT='test/integration/tsconfig.json' mocha --config=test/integration/.mocharc.json --node-env=test --exit 'test/integration/FixedRateExchange.test.ts'", "test-users": "TS_NODE_PROJECT='test/integration/tsconfig.json' mocha --config=test/integration/.mocharc.json --node-env=test --exit 'test/integration/users.test.ts'", "test-ve": "TS_NODE_PROJECT='test/integration/tsconfig.json' mocha --config=test/integration/.mocharc.json --node-env=test --exit 'test/integration/VeOcean.test.ts'", + "test-df": "TS_NODE_PROJECT='test/integration/tsconfig.json' mocha --config=test/integration/.mocharc.json --node-env=test --exit 'test/integration/DFRewards.test.ts'", "lint": "eslint --ignore-path .gitignore --ext .js --ext .ts --ext .tsx .", "lint:fix": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx . --fix", "format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json,yaml}' --write", @@ -65,8 +66,8 @@ "typescript": "^4.8.3" }, "dependencies": { - "@oceanprotocol/contracts": "^1.1.3", - "@oceanprotocol/lib": "^2.0.0", + "@oceanprotocol/contracts": "^1.1.4", + "@oceanprotocol/lib": "^2.0.2", "cross-fetch": "^3.1.4" }, "repository": { diff --git a/schema.graphql b/schema.graphql index 6a801b5..5174a5d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -466,4 +466,40 @@ type VeDeposit @entity { timestamp: BigInt! block: Int! tx: String! +} + + +enum DFHistoryType { + Allocated, + Claimed +} + +type DFAvailableClaim @entity { + "id = {userId}-{tokenId}" + id: ID! + receiver: DFReward! + amount: BigDecimal! + token: Token! +} + + +type DFHistory @entity { + "id = {user-id}-{txId}-{eventId}" + id: ID! + receiver: DFReward! + amount: BigDecimal! + token: Token! + type: DFHistoryType! + timestamp: BigInt! + block: Int! + tx: String! +} + + +type DFReward @entity { + "id = {user address}" + id: ID! + receiver: User! + availableClaims: [DFAvailableClaim!] @derivedFrom(field: "receiver") + history: [DFHistory!] @derivedFrom(field: "receiver") } \ No newline at end of file diff --git a/scripts/generatenetworkssubgraphs.js b/scripts/generatenetworkssubgraphs.js index a4aa7d9..72f56ce 100644 --- a/scripts/generatenetworkssubgraphs.js +++ b/scripts/generatenetworkssubgraphs.js @@ -22,6 +22,7 @@ async function replaceContractAddresses() { let subgraph = fs.readFileSync('./subgraph.template.yaml', 'utf8') const subgraphVe = fs.readFileSync('./subgraph_ve.template.yaml', 'utf8') if (addresses[network].veOCEAN) { + console.log('\t Adding veOCEAN') // fix identation , due to vs auto format (subgraph_ve.template is moved to left) const lines = subgraphVe.split('\n') for (let line = 0; line < lines.length; line++) { diff --git a/src/mappings/dfRewards.ts b/src/mappings/dfRewards.ts new file mode 100644 index 0000000..83d4059 --- /dev/null +++ b/src/mappings/dfRewards.ts @@ -0,0 +1,65 @@ +import { BigInt } from '@graphprotocol/graph-ts' +import { Allocated, Claimed } from '../@types/DFRewards/DFRewards' +import { DFHistory } from '../@types/schema' +import { weiToDecimal } from './utils/generic' +import { getToken } from './utils/tokenUtils' +import { getDFReward, getDFAvailableClaim } from './utils/dfUtils' + +export function handleAllocated(event: Allocated): void { + // loop all allocations + const token = getToken(event.params.tokenAddress, false) + for (let i = 0; i < event.params.tos.length; i++) { + const reward = getDFReward(event.params.tos[i]) + const history = new DFHistory( + event.params.tos[i].toHexString() + + '-' + + event.transaction.hash.toHex() + + '-' + + event.logIndex.toString() + ) + history.amount = weiToDecimal( + event.params.values[i].toBigDecimal(), + BigInt.fromI32(token.decimals).toI32() + ) + history.receiver = reward.id + history.token = token.id + history.type = 'Allocated' + history.timestamp = event.block.timestamp + history.tx = event.transaction.hash.toHex() + history.block = event.block.number.toI32() + history.save() + + // update available claims + const claim = getDFAvailableClaim( + event.params.tos[i], + event.params.tokenAddress + ) + claim.amount = claim.amount.plus(history.amount) + claim.save() + } +} + +export function handleClaimed(event: Claimed): void { + // loop all allocations + const token = getToken(event.params.tokenAddress, false) + const reward = getDFReward(event.params.to) + const history = new DFHistory( + event.transaction.hash.toHex() + '-' + event.logIndex.toString() + ) + history.amount = weiToDecimal( + event.params.value.toBigDecimal(), + BigInt.fromI32(token.decimals).toI32() + ) + history.receiver = reward.id + history.token = token.id + history.type = 'Claimed' + history.timestamp = event.block.timestamp + history.tx = event.transaction.hash.toHex() + history.block = event.block.number.toI32() + history.save() + + // update available claims + const claim = getDFAvailableClaim(event.params.to, event.params.tokenAddress) + claim.amount = claim.amount.minus(history.amount) + claim.save() +} diff --git a/src/mappings/utils/dfUtils.ts b/src/mappings/utils/dfUtils.ts new file mode 100644 index 0000000..f827e94 --- /dev/null +++ b/src/mappings/utils/dfUtils.ts @@ -0,0 +1,34 @@ +import { Address, BigDecimal } from '@graphprotocol/graph-ts' +import { DFAvailableClaim, DFReward } from '../../@types/schema' +import { getUser } from './userUtils' + +export function createDFReward(address: Address): DFReward { + const dfRewards = new DFReward(address.toHexString()) + const user = getUser(address.toHexString()) + dfRewards.receiver = user.id + dfRewards.save() + return dfRewards +} + +export function getDFReward(address: Address): DFReward { + let dfRewards = DFReward.load(address.toHexString()) + if (dfRewards === null) { + dfRewards = createDFReward(address) + } + return dfRewards +} + +export function getDFAvailableClaim( + user: Address, + token: Address +): DFAvailableClaim { + const id = user.toHexString() + '-' + token.toHexString() + let dfClaim = DFAvailableClaim.load(id) + if (dfClaim == null) { + dfClaim = new DFAvailableClaim(id) + dfClaim.receiver = user.toHexString() + dfClaim.amount = BigDecimal.zero() + dfClaim.token = token.toHexString() + } + return dfClaim +} diff --git a/subgraph_ve.template.yaml b/subgraph_ve.template.yaml index a86c48a..ad95f0b 100644 --- a/subgraph_ve.template.yaml +++ b/subgraph_ve.template.yaml @@ -66,3 +66,26 @@ eventHandlers: - event: DelegateBoost(indexed address,indexed address,indexed uint256,uint256,uint256,uint256) handler: handleDelegation + +- name: DFRewards + kind: ethereum/contract + network: __NETWORK__ + source: + abi: DFRewards + address: __DFREWARDSADDRESS__ + startBlock: __STARTBLOCK__ + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + file: ./src/mappings/dfRewards.ts + entities: + - DFRewards + abis: + - name: DFRewards + file: ./node_modules/@oceanprotocol/contracts/artifacts/contracts/df/DFRewards.sol/DFRewards.json + eventHandlers: + - event: Allocated(address[],uint256[],address) + handler: handleAllocated + - event: Claimed(address,uint256,address) + handler: handleClaimed diff --git a/test/integration/DFRewards.test.ts b/test/integration/DFRewards.test.ts new file mode 100644 index 0000000..2c9e97f --- /dev/null +++ b/test/integration/DFRewards.test.ts @@ -0,0 +1,239 @@ +import { NftFactory, sleep, Datatoken, DfRewards } from '@oceanprotocol/lib' +import { assert } from 'chai' +import Web3 from 'web3' +import { homedir } from 'os' +import fs from 'fs' +import { fetch } from 'cross-fetch' + +const data = JSON.parse( + fs.readFileSync( + process.env.ADDRESS_FILE || + `${homedir}/.ocean/ocean-contracts/artifacts/address.json`, + 'utf8' + ) +) + +const addresses = data.development +// const aquarius = new Aquarius('http://127.0.0.1:5000') +const web3 = new Web3('http://127.0.0.1:8545') + +const subgraphUrl = + 'http://127.0.0.1:9000/subgraphs/name/oceanprotocol/ocean-subgraph' + +describe('DFRewards tests', async () => { + const nftName = 'testNFT' + const nftSymbol = 'TST' + const marketPlaceFeeAddress = '0x1230000000000000000000000000000000000000' + const feeToken = '0x3210000000000000000000000000000000000000' + const publishMarketFeeAmount = '0.1' + const cap = '10000' + const templateIndex = 1 + let datatokenAddress1: string + let datatokenAddress2: string + let dataToken: Datatoken + let Factory: NftFactory + let factoryAddress: string + let accounts: string[] + let publisher: string + let dfRewards: DfRewards + let user1: string + let user2: string + + before(async () => { + factoryAddress = addresses.ERC721Factory.toLowerCase() + Factory = new NftFactory(factoryAddress, web3) + accounts = await web3.eth.getAccounts() + dataToken = new Datatoken(web3) + dfRewards = new DfRewards(addresses.DFRewards, web3) + publisher = accounts[0].toLowerCase() + user1 = accounts[1].toLowerCase() + user2 = accounts[2].toLowerCase() + }) + + it('should publish two datatokens', async () => { + let result = await Factory.createNftWithDatatoken( + publisher, + { + name: nftName, + symbol: nftSymbol, + templateIndex, + tokenURI: '', + transferable: true, + owner: publisher + }, + { + templateIndex, + cap, + feeAmount: publishMarketFeeAmount, + paymentCollector: '0x0000000000000000000000000000000000000000', + feeToken, + minter: publisher, + mpFeeAddress: marketPlaceFeeAddress, + name: 'DT1', + symbol: 'DT1' + } + ) + datatokenAddress1 = result.events.TokenCreated.returnValues[0].toLowerCase() + + result = await Factory.createNftWithDatatoken( + publisher, + { + name: nftName, + symbol: nftSymbol, + templateIndex, + tokenURI: '', + transferable: true, + owner: publisher + }, + { + templateIndex, + cap, + feeAmount: publishMarketFeeAmount, + paymentCollector: '0x0000000000000000000000000000000000000000', + feeToken, + minter: publisher, + mpFeeAddress: marketPlaceFeeAddress, + name: 'DT2', + symbol: 'DT2' + } + ) + datatokenAddress2 = result.events.TokenCreated.returnValues[0].toLowerCase() + }) + + it('should top-up DF Rewards', async () => { + // mint tokens + await dataToken.mint(datatokenAddress1, publisher, '1000') + await dataToken.mint(datatokenAddress2, publisher, '1000') + // approve + await dataToken.approve( + datatokenAddress1, + addresses.DFRewards, + '1000', + publisher + ) + await dataToken.approve( + datatokenAddress2, + addresses.DFRewards, + '1000', + publisher + ) + + // top-up DF Rewards + const tx = await dfRewards.allocateRewards( + publisher, + [user1, user2], + ['100', '200'], + datatokenAddress1 + ) + const user1Balance = await dfRewards.getAvailableRewards( + user1, + datatokenAddress1 + ) + // check subgraph + await sleep(2000) + const initialQuery = { + query: `query { + dfrewards(where: {id:"${user1.toLowerCase()}"}){ + id + receiver { + id + } + availableClaims(where: {token:"${datatokenAddress1.toLowerCase()}"}){ + id + receiver { + id + } + amount + token { + id + } + } + history(orderBy:timestamp,orderDirection:desc){ + id + receiver { + id + } + amount + token { + id + } + type + tx + } + } + }` + } + const initialResponse = await fetch(subgraphUrl, { + method: 'POST', + body: JSON.stringify(initialQuery) + }) + const info = (await initialResponse.json()).data.dfrewards + assert(info[0].receiver.id === user1.toLowerCase()) + assert(String(info[0].availableClaims[0].amount) === user1Balance) + assert( + info[0].availableClaims[0].token.id === datatokenAddress1.toLowerCase() + ) + assert(info[0].history[0].amount === '100') + assert(info[0].history[0].tx === tx.transactionHash.toLowerCase()) + assert(info[0].history[0].type === 'Allocated') + }) + + it('user2 claims some rewards', async () => { + const expectedReward = await dfRewards.getAvailableRewards( + user2, + datatokenAddress1 + ) + await dfRewards.claimRewards(user2, user2, datatokenAddress1) + + const user2Balance = await dfRewards.getAvailableRewards( + user2, + datatokenAddress1 + ) + // check subgraph + await sleep(2000) + const initialQuery = { + query: `query { + dfrewards(where: {id:"${user2.toLowerCase()}"}){ + id + receiver { + id + } + availableClaims(where: {token:"${datatokenAddress1.toLowerCase()}"}){ + id + receiver { + id + } + amount + token { + id + } + } + history(orderBy:timestamp,orderDirection:desc){ + id + receiver { + id + } + amount + token { + id + } + type + tx + } + } + }` + } + const initialResponse = await fetch(subgraphUrl, { + method: 'POST', + body: JSON.stringify(initialQuery) + }) + const info = (await initialResponse.json()).data.dfrewards + assert(info[0].receiver.id === user2.toLowerCase()) + assert(String(info[0].availableClaims[0].amount) === user2Balance) + assert( + info[0].availableClaims[0].token.id === datatokenAddress1.toLowerCase() + ) + assert(info[0].history[0].amount === expectedReward) + assert(info[0].history[0].type === 'Claimed') + }) +})