diff --git a/abis/ERC20.json b/abis/ERC20.json new file mode 100644 index 0000000..668d697 --- /dev/null +++ b/abis/ERC20.json @@ -0,0 +1,222 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + } +] \ No newline at end of file diff --git a/schema.graphql b/schema.graphql index af7a991..b193164 100644 --- a/schema.graphql +++ b/schema.graphql @@ -15,3 +15,22 @@ type ProxyUpdated @entity(immutable: true) { blockTimestamp: BigInt! transactionHash: Bytes! } + +type Token @entity { + id: ID! + name: String! + symbol: String! + decimals: BigDecimal! +} + +type Account @entity { + id: ID! + balances: [TokenBalance!]! @derivedFrom(field: "account") +} + +type TokenBalance @entity { + id: ID! + token: Token! + account: Account! + amount: BigDecimal! +} \ No newline at end of file diff --git a/src/erc-20.ts b/src/erc-20.ts new file mode 100644 index 0000000..0f1a498 --- /dev/null +++ b/src/erc-20.ts @@ -0,0 +1,41 @@ +import { Transfer } from "../generated/SEED-ERC20/ERC20" +import { TokenBalance } from "../generated/schema" +import { + fetchTokenDetails, + fetchAccount, + fetchBalance +} from "./utils" +import { BigDecimal } from "@graphprotocol/graph-ts" + +export function handleTransfer(event: Transfer): void { + let token = fetchTokenDetails(event) + if(!token) return + + let fromAddress = event.params.from.toHex() + let toAddress = event.params.to.toHex() + + let fromAccount = fetchAccount(fromAddress) + let toAccount = fetchAccount(toAddress) + + let fromTokenBalance = TokenBalance.load(token.id + "-" + fromAccount.id) + if (!fromTokenBalance) { + fromTokenBalance = new TokenBalance(token.id + "-" + fromAccount.id) + fromTokenBalance.token = token.id + fromTokenBalance.account = fromAccount.id + } + fromTokenBalance.amount = fetchBalance(event.address, event.params.from) + if(fromTokenBalance.amount != BigDecimal.zero()) { + fromTokenBalance.save() + } + + let toTokenBalance = TokenBalance.load(token.id + "-" + toAccount.id) + if (!toTokenBalance) { + toTokenBalance = new TokenBalance(token.id + "-" + toAccount.id) + toTokenBalance.token = token.id + toTokenBalance.account = toAccount.id + } + toTokenBalance.amount = fetchBalance(event.address, event.params.to) + if (toTokenBalance.amount != BigDecimal.zero()) { + toTokenBalance.save() + } +} \ No newline at end of file diff --git a/src/u-child-erc-20-proxy.ts b/src/u-child-erc-20-proxy.ts deleted file mode 100644 index da3ca5a..0000000 --- a/src/u-child-erc-20-proxy.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - ProxyOwnerUpdate as ProxyOwnerUpdateEvent, - ProxyUpdated as ProxyUpdatedEvent -} from "../generated/UChildERC20Proxy/UChildERC20Proxy" -import { ProxyOwnerUpdate, ProxyUpdated } from "../generated/schema" - -export function handleProxyOwnerUpdate(event: ProxyOwnerUpdateEvent): void { - let entity = new ProxyOwnerUpdate( - event.transaction.hash.concatI32(event.logIndex.toI32()) - ) - entity._new = event.params._new - entity._old = event.params._old - - entity.blockNumber = event.block.number - entity.blockTimestamp = event.block.timestamp - entity.transactionHash = event.transaction.hash - - entity.save() -} - -export function handleProxyUpdated(event: ProxyUpdatedEvent): void { - let entity = new ProxyUpdated( - event.transaction.hash.concatI32(event.logIndex.toI32()) - ) - entity._new = event.params._new - entity._old = event.params._old - - entity.blockNumber = event.block.number - entity.blockTimestamp = event.block.timestamp - entity.transactionHash = event.transaction.hash - - entity.save() -} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..8b93ff8 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,56 @@ +import { ERC20 } from "../generated/SEED-ERC20/ERC20" +import { Account, Token } from "../generated/schema" +import { BigDecimal, ethereum, Address } from "@graphprotocol/graph-ts" + +export function fetchTokenDetails(event: ethereum.Event): Token | null { + let token = Token.load(event.address.toHex()) + if (!token) { + token = new Token(event.address.toHex()) + + token.name = "Uɴᴋɴᴏᴡɴ" + token.symbol = "𝑈𝑛𝑘𝑛𝑜𝑤𝑛" + token.decimals = BigDecimal.zero() + + let erc20 = ERC20.bind(event.address) + + let tokenName = erc20.try_name() + if (!tokenName.reverted) { + token.name = tokenName.value + } + + let tokenSymbol = erc20.try_symbol() + if (!tokenSymbol.reverted) { + token.symbol = tokenSymbol.value + } + + let tokenDecimal = erc20.try_decimals() + if (!tokenDecimal.reverted) { + token.decimals = BigDecimal.fromString(tokenDecimal.value.toString()) + } + + token.save() + } + return token +} + +export function fetchAccount(address: string): Account { + let account = Account.load(address) + if (!account) { + account = new Account(address) + account.save() + } + if(!account) throw new Error(`Failed to fetch account: "${address}".`) + return account +} + +export function fetchBalance( + tokenAddress: Address, + accountAddress: Address +): BigDecimal { + let erc20 = ERC20.bind(tokenAddress) + let tokenBalance = erc20.try_balanceOf(accountAddress) + if(tokenBalance.reverted) { + throw new Error(`Failed to fetch balance: "${tokenBalance.reverted.toString()}".`) + } + return tokenBalance.value.toBigDecimal() +} diff --git a/subgraph.yaml b/subgraph.yaml index 82a2977..a0e84c0 100644 --- a/subgraph.yaml +++ b/subgraph.yaml @@ -5,25 +5,46 @@ schema: file: ./schema.graphql dataSources: - kind: ethereum - name: UChildERC20Proxy + name: SEED-ERC20 network: matic source: address: "0xEAeCC18198a475c921B24b8A6c1C1f0f5F3F7EA0" - abi: UChildERC20Proxy + abi: ERC20 startBlock: 21270464 mapping: kind: ethereum/events apiVersion: 0.0.7 language: wasm/assemblyscript entities: - - ProxyOwnerUpdate - - ProxyUpdated + - Token + - Account + - TokenBalance abis: - - name: UChildERC20Proxy - file: ./abis/UChildERC20Proxy.json + - name: ERC20 + file: ./abis/ERC20.json eventHandlers: - - event: ProxyOwnerUpdate(address,address) - handler: handleProxyOwnerUpdate - - event: ProxyUpdated(indexed address,indexed address) - handler: handleProxyUpdated - file: ./src/u-child-erc-20-proxy.ts + - event: Transfer(indexed address,indexed address,uint256) + handler: handleTransfer + file: ./src/erc-20.ts + - kind: ethereum + name: pSEED-ERC20 + network: matic + source: + address: "0x8A8fCd351ED553Fc75Aecbc566A32F94471f302E" + abi: ERC20 + startBlock: 21345454 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - Token + - Account + - TokenBalance + abis: + - name: ERC20 + file: ./abis/ERC20.json + eventHandlers: + - event: Transfer(indexed address,indexed address,uint256) + handler: handleTransfer + file: ./src/erc-20.ts diff --git a/tests/u-child-erc-20-proxy-utils.ts b/tests/u-child-erc-20-proxy-utils.ts deleted file mode 100644 index 54c0648..0000000 --- a/tests/u-child-erc-20-proxy-utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { newMockEvent } from "matchstick-as" -import { ethereum, Address } from "@graphprotocol/graph-ts" -import { - ProxyOwnerUpdate, - ProxyUpdated -} from "../generated/UChildERC20Proxy/UChildERC20Proxy" - -export function createProxyOwnerUpdateEvent( - _new: Address, - _old: Address -): ProxyOwnerUpdate { - let proxyOwnerUpdateEvent = changetype(newMockEvent()) - - proxyOwnerUpdateEvent.parameters = new Array() - - proxyOwnerUpdateEvent.parameters.push( - new ethereum.EventParam("_new", ethereum.Value.fromAddress(_new)) - ) - proxyOwnerUpdateEvent.parameters.push( - new ethereum.EventParam("_old", ethereum.Value.fromAddress(_old)) - ) - - return proxyOwnerUpdateEvent -} - -export function createProxyUpdatedEvent( - _new: Address, - _old: Address -): ProxyUpdated { - let proxyUpdatedEvent = changetype(newMockEvent()) - - proxyUpdatedEvent.parameters = new Array() - - proxyUpdatedEvent.parameters.push( - new ethereum.EventParam("_new", ethereum.Value.fromAddress(_new)) - ) - proxyUpdatedEvent.parameters.push( - new ethereum.EventParam("_old", ethereum.Value.fromAddress(_old)) - ) - - return proxyUpdatedEvent -} diff --git a/tests/u-child-erc-20-proxy.test.ts b/tests/u-child-erc-20-proxy.test.ts deleted file mode 100644 index 946b202..0000000 --- a/tests/u-child-erc-20-proxy.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - assert, - describe, - test, - clearStore, - beforeAll, - afterAll -} from "matchstick-as/assembly/index" -import { Address } from "@graphprotocol/graph-ts" -import { ProxyOwnerUpdate } from "../generated/schema" -import { ProxyOwnerUpdate as ProxyOwnerUpdateEvent } from "../generated/UChildERC20Proxy/UChildERC20Proxy" -import { handleProxyOwnerUpdate } from "../src/u-child-erc-20-proxy" -import { createProxyOwnerUpdateEvent } from "./u-child-erc-20-proxy-utils" - -// Tests structure (matchstick-as >=0.5.0) -// https://thegraph.com/docs/en/developer/matchstick/#tests-structure-0-5-0 - -describe("Describe entity assertions", () => { - beforeAll(() => { - let _new = Address.fromString("0x0000000000000000000000000000000000000001") - let _old = Address.fromString("0x0000000000000000000000000000000000000001") - let newProxyOwnerUpdateEvent = createProxyOwnerUpdateEvent(_new, _old) - handleProxyOwnerUpdate(newProxyOwnerUpdateEvent) - }) - - afterAll(() => { - clearStore() - }) - - // For more test scenarios, see: - // https://thegraph.com/docs/en/developer/matchstick/#write-a-unit-test - - test("ProxyOwnerUpdate created and stored", () => { - assert.entityCount("ProxyOwnerUpdate", 1) - - // 0xa16081f360e3847006db660bae1c6d1b2e17ec2a is the default address used in newMockEvent() function - assert.fieldEquals( - "ProxyOwnerUpdate", - "0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1", - "_new", - "0x0000000000000000000000000000000000000001" - ) - assert.fieldEquals( - "ProxyOwnerUpdate", - "0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1", - "_old", - "0x0000000000000000000000000000000000000001" - ) - - // More assert options: - // https://thegraph.com/docs/en/developer/matchstick/#asserts - }) -})