diff --git a/schema.graphql b/schema.graphql index 4432d79..1b0cc16 100644 --- a/schema.graphql +++ b/schema.graphql @@ -9,6 +9,7 @@ type Nft @entity { type Account @entity { id: ID! # address nfts: [Nft!] @derivedFrom(field: "owner") + roleApprovals: [RoleApproval!] @derivedFrom(field: "grantor") } type Role @entity { @@ -21,3 +22,10 @@ type Role @entity { revocable: Boolean! data: Bytes! } + +type RoleApproval @entity { + id: ID! # grantorAddress + operatorAddress + tokenAddress + grantor: Account! + operator: Account! + tokenAddress: String! +} diff --git a/src/erc7432/index.ts b/src/erc7432/index.ts index 25f2239..2643602 100644 --- a/src/erc7432/index.ts +++ b/src/erc7432/index.ts @@ -1,2 +1,3 @@ export { handleRoleGranted } from './role/grant-handler' export { handleRoleRevoked } from './role/revoke-handler' +export { handleRoleApprovalForAll } from './role/role-approval-handler' diff --git a/src/erc7432/role/role-approval-handler.ts b/src/erc7432/role/role-approval-handler.ts new file mode 100644 index 0000000..2227e4e --- /dev/null +++ b/src/erc7432/role/role-approval-handler.ts @@ -0,0 +1,20 @@ +import { RoleApprovalForAll } from '../../../generated/ERC7432-Immutable-Roles/ERC7432' +import { findOrCreateAccount, insertRoleApprovalIfNotExist, deleteRoleApprovalIfExist } from '../../utils/helper' +import { log } from '@graphprotocol/graph-ts' + +export function handleRoleApprovalForAll(event: RoleApprovalForAll): void { + const grantorAddress = event.transaction.from.toHex() + const operatorAddress = event.params._operator.toHex() + const tokenAddress = event.params._tokenAddress.toHex() + const isApproved = event.params._isApproved + + const grantorAccount = findOrCreateAccount(grantorAddress) + const operatorAccount = findOrCreateAccount(operatorAddress) + + if (isApproved) { + const roleApproval = insertRoleApprovalIfNotExist(grantorAccount, operatorAccount, tokenAddress) + log.info('[handleRoleGranted] Updated Role Approval: {}', [roleApproval.id]) + } else { + deleteRoleApprovalIfExist(grantorAccount, operatorAccount, tokenAddress) + } +} diff --git a/src/utils/helper.ts b/src/utils/helper.ts index 4a05a99..775e38a 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -1,5 +1,5 @@ -import { BigInt, Bytes } from '@graphprotocol/graph-ts' -import { Account, Nft, Role } from '../../generated/schema' +import { BigInt, Bytes, store } from '@graphprotocol/graph-ts' +import { Account, Nft, Role, RoleApproval } from '../../generated/schema' import { RoleGranted } from '../../generated/ERC7432-Immutable-Roles/ERC7432' export function findOrCreateAccount(id: string): Account { @@ -45,3 +45,28 @@ export function findOrCreateRole(event: RoleGranted, grantor: Account, grantee: role.save() return role } + +export function generateRoleApprovalId(grantor: Account, operator: Account, tokenAddress: string): string { + return grantor.id + '-' + operator.id + '-' + tokenAddress.toLowerCase() +} + +export function insertRoleApprovalIfNotExist(grantor: Account, operator: Account, tokenAddress: string): RoleApproval { + const roleApprovalId = generateRoleApprovalId(grantor, operator, tokenAddress) + let roleApproval = RoleApproval.load(roleApprovalId) + if (!roleApproval) { + roleApproval = new RoleApproval(roleApprovalId) + roleApproval.grantor = grantor.id + roleApproval.operator = operator.id + roleApproval.tokenAddress = tokenAddress.toLowerCase() + roleApproval.save() + } + return roleApproval +} + +export function deleteRoleApprovalIfExist(grantor: Account, operator: Account, tokenAddress: string): void { + const roleApprovalId = generateRoleApprovalId(grantor, operator, tokenAddress) + const roleApproval = RoleApproval.load(roleApprovalId) + if (roleApproval) { + store.remove('RoleApproval', roleApprovalId) + } +} diff --git a/subgraph-goerli.yaml b/subgraph-goerli.yaml index 772a8fc..93e7eed 100644 --- a/subgraph-goerli.yaml +++ b/subgraph-goerli.yaml @@ -39,6 +39,7 @@ dataSources: - Nft - Account - Role + - RoleApproval abis: - name: ERC7432 file: ./abis/ERC7432.json @@ -47,4 +48,6 @@ dataSources: handler: handleRoleGranted - event: RoleRevoked(indexed bytes32,indexed address,indexed uint256,address,address) handler: handleRoleRevoked + - event: RoleApprovalForAll(indexed address,indexed address,bool) + handler: handleRoleApprovalForAll file: ./src/erc7432/index.ts diff --git a/subgraph-mumbai.yaml b/subgraph-mumbai.yaml index 58a6a31..9dbb61a 100644 --- a/subgraph-mumbai.yaml +++ b/subgraph-mumbai.yaml @@ -19,6 +19,7 @@ dataSources: - Nft - Account - Role + - RoleApproval abis: - name: ERC7432 file: ./abis/ERC7432.json @@ -27,4 +28,6 @@ dataSources: handler: handleRoleGranted - event: RoleRevoked(indexed bytes32,indexed address,indexed uint256,address,address) handler: handleRoleRevoked + - event: RoleApprovalForAll(indexed address,indexed address,bool) + handler: handleRoleApprovalForAll file: ./src/erc7432/index.ts diff --git a/subgraph-polygon.yaml b/subgraph-polygon.yaml index e6be513..5661f9c 100644 --- a/subgraph-polygon.yaml +++ b/subgraph-polygon.yaml @@ -39,6 +39,7 @@ dataSources: - Nft - Account - Role + - RoleApproval abis: - name: ERC7432 file: ./abis/ERC7432.json @@ -47,4 +48,6 @@ dataSources: handler: handleRoleGranted - event: RoleRevoked(indexed bytes32,indexed address,indexed uint256,address,address) handler: handleRoleRevoked + - event: RoleApprovalForAll(indexed address,indexed address,bool) + handler: handleRoleApprovalForAll file: ./src/erc7432/index.ts diff --git a/tests/erc7432/approval-handler.test.ts b/tests/erc7432/approval-handler.test.ts new file mode 100644 index 0000000..3273d9d --- /dev/null +++ b/tests/erc7432/approval-handler.test.ts @@ -0,0 +1,55 @@ +import { assert, describe, test, clearStore, afterEach } from 'matchstick-as' +import { createNewRoleApprovalForAllEvent } from '../helpers/events' +import { validateRoleApproval, createMockRoleApproval } from '../helpers/entities' +import { Addresses } from '../helpers/contants' +import { handleRoleApprovalForAll } from '../../src/erc7432' + +const grantor = Addresses[0] +const operator = Addresses[1] +const tokenAddress = Addresses[2] + +describe('ERC-7432 RoleApprovalForAll Handler', () => { + afterEach(() => { + clearStore() + }) + + test('should not do anything when approval does not exist and is set to false', () => { + assert.entityCount('RoleApproval', 0) + + const event = createNewRoleApprovalForAllEvent(grantor, operator, tokenAddress, false) + handleRoleApprovalForAll(event) + + assert.entityCount('RoleApproval', 0) + }) + + test('should create approval when approval does not exist and is set to true', () => { + assert.entityCount('RoleApproval', 0) + + const event = createNewRoleApprovalForAllEvent(grantor, operator, tokenAddress, true) + handleRoleApprovalForAll(event) + + assert.entityCount('RoleApproval', 1) + validateRoleApproval(grantor, operator, tokenAddress) + }) + + test('should remove approval when approval exists and is set to false', () => { + createMockRoleApproval(grantor, operator, tokenAddress) + assert.entityCount('RoleApproval', 1) + + const event = createNewRoleApprovalForAllEvent(grantor, operator, tokenAddress, false) + handleRoleApprovalForAll(event) + + assert.entityCount('RoleApproval', 0) + }) + + test('should not do anything when approval exists and is set to true', () => { + createMockRoleApproval(grantor, operator, tokenAddress) + assert.entityCount('RoleApproval', 1) + + const event = createNewRoleApprovalForAllEvent(grantor, operator, tokenAddress, true) + handleRoleApprovalForAll(event) + + assert.entityCount('RoleApproval', 1) + validateRoleApproval(grantor, operator, tokenAddress) + }) +}) diff --git a/tests/helpers/entities.ts b/tests/helpers/entities.ts index 96f31fb..5df1ca4 100644 --- a/tests/helpers/entities.ts +++ b/tests/helpers/entities.ts @@ -1,6 +1,6 @@ import { BigInt, Bytes } from '@graphprotocol/graph-ts' -import { Account, Nft, Role } from '../../generated/schema' -import { generateNftId, generateRoleId } from '../../src/utils/helper' +import { Account, Nft, Role, RoleApproval } from '../../generated/schema' +import { generateNftId, generateRoleId, generateRoleApprovalId } from '../../src/utils/helper' import { assert } from 'matchstick-as' export function createMockNft(tokenAddress: string, tokenId: string, ownerAddress: string): Nft { @@ -35,6 +35,16 @@ export function createMockRole(role: Bytes, grantor: string, grantee: string, nf return newRole } +export function createMockRoleApproval(grantor: string, operator: string, tokenAddress: string): RoleApproval { + const roleApprovalId = generateRoleApprovalId(new Account(grantor), new Account(operator), tokenAddress) + const roleApproval = new RoleApproval(roleApprovalId) + roleApproval.grantor = grantor + roleApproval.operator = operator + roleApproval.tokenAddress = tokenAddress + roleApproval.save() + return roleApproval +} + export function validateRole( grantor: Account, grantee: Account, @@ -51,3 +61,14 @@ export function validateRole( assert.fieldEquals('Role', _id, 'expirationDate', expirationDate.toString()) assert.fieldEquals('Role', _id, 'data', data.toHex()) } + +export function validateRoleApproval(grantor: string, operator: string, tokenAddress: string): void { + const roleApprovalId = generateRoleApprovalId( + new Account(grantor.toLowerCase()), + new Account(operator.toLowerCase()), + tokenAddress, + ) + assert.fieldEquals('RoleApproval', roleApprovalId, 'grantor', grantor) + assert.fieldEquals('RoleApproval', roleApprovalId, 'operator', operator) + assert.fieldEquals('RoleApproval', roleApprovalId, 'tokenAddress', tokenAddress) +} diff --git a/tests/helpers/events.ts b/tests/helpers/events.ts index 3d0c5aa..b12cdf1 100644 --- a/tests/helpers/events.ts +++ b/tests/helpers/events.ts @@ -1,7 +1,7 @@ import { newMockEvent } from 'matchstick-as' import { Transfer } from '../../generated/ERC721-Chronos-Traveler/ERC721' import { Address, BigInt, Bytes, ethereum } from '@graphprotocol/graph-ts' -import { RoleGranted, RoleRevoked } from '../../generated/ERC7432-Immutable-Roles/ERC7432' +import { RoleGranted, RoleRevoked, RoleApprovalForAll } from '../../generated/ERC7432-Immutable-Roles/ERC7432' import { Nft } from '../../generated/schema' export function createTransferEvent(from: string, to: string, tokenId: string, address: string): Transfer { @@ -48,6 +48,21 @@ export function createNewRoleGrantedEvent( return event } +export function createNewRoleApprovalForAllEvent( + grantor: string, + operator: string, + tokenAddress: string, + isApproved: boolean, +): RoleApprovalForAll { + const event = changetype(newMockEvent()) + event.parameters = new Array() + event.transaction.from = Address.fromString(grantor) + event.parameters.push(buildEventParamAddress('_tokenAddress', tokenAddress)) + event.parameters.push(buildEventParamAddress('_operator', operator)) + event.parameters.push(buildEventParamBoolean('_isApproved', isApproved)) + return event +} + function buildEventParamBoolean(name: string, value: boolean): ethereum.EventParam { const ethAddress = ethereum.Value.fromBoolean(value) return new ethereum.EventParam(name, ethAddress)