From de85f2791cb2f8205edd6908e3fc780762ecba15 Mon Sep 17 00:00:00 2001 From: Jonathan Smirnoff Date: Tue, 28 Jan 2025 11:05:00 -0300 Subject: [PATCH] feat: add RSKOwner contract to the subgraph --- abis/RSKOwner.json | 462 +++++++++++++++++++++++++++++++++++++++ networks.json | 3 + schema.graphql | 77 ++++++- src/rsk-owner.ts | 78 +++++++ src/utils.ts | 31 +++ subgraph.yaml | 23 ++ tests/.latest.json | 2 +- tests/rsk-owner-utils.ts | 127 +++++++++++ tests/rsk-owner.test.ts | 31 +++ 9 files changed, 832 insertions(+), 2 deletions(-) create mode 100644 abis/RSKOwner.json create mode 100644 src/rsk-owner.ts create mode 100644 tests/rsk-owner-utils.ts create mode 100644 tests/rsk-owner.test.ts diff --git a/abis/RSKOwner.json b/abis/RSKOwner.json new file mode 100644 index 0000000..e6ad289 --- /dev/null +++ b/abis/RSKOwner.json @@ -0,0 +1,462 @@ +[ + { + "inputs": [ + { + "internalType": "contract TokenRegistrar", + "name": "_previousRegistrar", + "type": "address" + }, + { + "internalType": "contract AbstractRNS", + "name": "_rns", + "type": "address" + }, + { "internalType": "bytes32", "name": "_rootNode", "type": "bytes32" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "expirationTime", + "type": "uint256" + } + ], + "name": "ExpirationChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "label", "type": "bytes32" }, + { + "internalType": "contract TokenDeed", + "name": "deed", + "type": "address" + }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "acceptRegistrarTransfer", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "registrar", "type": "address" } + ], + "name": "addRegistrar", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "renewer", "type": "address" } + ], + "name": "addRenewer", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "approve", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "available", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "expirationTime", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "getApproved", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "operator", "type": "address" } + ], + "name": "isApprovedForAll", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isOwner", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "address", "name": "registrar", "type": "address" } + ], + "name": "isRegistrar", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "address", "name": "renewer", "type": "address" } + ], + "name": "isRenewer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "reclaim", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "label", "type": "bytes32" }, + { "internalType": "address", "name": "tokenOwner", "type": "address" }, + { "internalType": "uint256", "name": "duration", "type": "uint256" } + ], + "name": "register", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "uint256[]", "name": "tokenIds", "type": "uint256[]" } + ], + "name": "removeExpired", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "registrar", "type": "address" } + ], + "name": "removeRegistrar", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "renewer", "type": "address" } + ], + "name": "removeRenewer", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "label", "type": "bytes32" }, + { "internalType": "uint256", "name": "time", "type": "uint256" } + ], + "name": "renew", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "safeTransferFrom", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, + { "internalType": "bytes", "name": "_data", "type": "bytes" } + ], + "name": "safeTransferFrom", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "bool", "name": "approved", "type": "bool" } + ], + "name": "setApprovalForAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "resolver", "type": "address" } + ], + "name": "setRootResolver", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "uint64", "name": "ttl", "type": "uint64" }], + "name": "setRootTTL", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" } + ], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "transferOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/networks.json b/networks.json index dadd93a..29b0988 100644 --- a/networks.json +++ b/networks.json @@ -6,6 +6,9 @@ }, "RNS": { "address": "0xcb868aeabd31e2b66f74e9a55cf064abb31a4ad5" + }, + "RSKOwner": { + "address": "0x45d3e4FB311982A06bA52359d44cb4f5980e0ef1" } } } \ No newline at end of file diff --git a/schema.graphql b/schema.graphql index 97ea096..3a8c908 100644 --- a/schema.graphql +++ b/schema.graphql @@ -36,9 +36,13 @@ type Domain @entity { "The account that owns the domain" owner: Account! + "The account that owns the ERC721 NFT for the domain" + registrant: Account "The expiry date for the domain, from either the registration, or the wrapped domain if PCC is burned" expiryDate: BigInt + "The registration associated with the domain" + registration: Registration @derivedFrom(field: "domain") } type Account @entity { @@ -141,4 +145,75 @@ type NewTTL implements DomainEvent @entity { transactionID: Bytes! "The new TTL value (in seconds) associated with the domain" ttl: BigInt! -} \ No newline at end of file +} + +type ExpiryExtended implements DomainEvent @entity { + "The unique identifier of the event" + id: ID! + "The domain name associated with the event" + domain: Domain! + "The block number at which the event occurred" + blockNumber: Int! + "The transaction hash of the transaction that triggered the event" + transactionID: Bytes! + "The new expiry date associated with the domain after the extension event" + expiryDate: BigInt! +} + +interface RegistrationEvent { + "The unique identifier of the registration event" + id: ID! + "The registration associated with the event" + registration: Registration! + "The block number of the event" + blockNumber: Int! + "The transaction ID associated with the event" + transactionID: Bytes! +} + +type NameRegistered implements RegistrationEvent @entity { + "The unique identifier of the NameRegistered event" + id: ID! + "The registration associated with the event" + registration: Registration! + "The block number of the event" + blockNumber: Int! + "The transaction ID associated with the event" + transactionID: Bytes! + "The account that registered the name" + registrant: Account! + "The expiry date of the registration" + expiryDate: BigInt! +} + +type Registration @entity { + "The unique identifier of the registration" + id: ID! + "The domain name associated with the registration" + domain: Domain! + "The registration date of the domain" + registrationDate: BigInt! + "The expiry date of the domain" + expiryDate: BigInt! + "The cost associated with the domain registration" + cost: BigInt + "The account that registered the domain" + registrant: Account! + "The human-readable label name associated with the domain registration" + labelName: String + "The events associated with the domain registration" + events: [RegistrationEvent!]! @derivedFrom(field: "registration") +} + +type NameTransferred implements RegistrationEvent @entity { + "The ID of the event" + id: ID! + "The registration associated with the event" + registration: Registration! + "The block number of the event" + blockNumber: Int! + "The transaction ID of the event" + transactionID: Bytes! + "The new owner of the domain" + newOwner: Account! +} diff --git a/src/rsk-owner.ts b/src/rsk-owner.ts new file mode 100644 index 0000000..30500bd --- /dev/null +++ b/src/rsk-owner.ts @@ -0,0 +1,78 @@ +import { ByteArray, crypto, ens, log } from "@graphprotocol/graph-ts"; +import { + Approval as ApprovalEvent, + ApprovalForAll as ApprovalForAllEvent, + ExpirationChanged as ExpirationChangedEvent, + OwnershipTransferred as OwnershipTransferredEvent, + Transfer as TransferEvent, +} from "../generated/RSKOwner/RSKOwner" +import { + Account, + Domain, + NameRegistered, + NameTransferred, + Registration, + Transfer, +} from "../generated/schema" +import { checkValidLabel, concat, createEventID, RSK_NODE, uint256ToByteArray } from "./utils"; + +var rootNode: ByteArray = ByteArray.fromHexString(RSK_NODE); + +export function handleExpirationChanged(event: ExpirationChangedEvent): void { + let label = uint256ToByteArray(event.params.tokenId); + let domain = Domain.load(crypto.keccak256(concat(rootNode, label)).toHex()); + + // Workaround for the case when the domain is not found + if (domain == null) return ; + + let registration = new Registration(label.toHex()); + + registration.domain = domain.id; + registration.registrationDate = event.block.timestamp; + registration.expiryDate = event.params.expirationTime; + registration.registrant = domain.owner; + + domain.expiryDate = event.params.expirationTime; + + + let labelName = ens.nameByHash(label.toHexString()); + if (checkValidLabel(labelName)) { + domain.labelName = labelName; + domain.name = labelName! + ".rsk"; + registration.labelName = labelName; + } + domain.save(); + registration.save(); + + let registrationEvent = new NameRegistered(createEventID(event)); + registrationEvent.registration = registration.id; + registrationEvent.blockNumber = event.block.number.toI32(); + registrationEvent.transactionID = event.transaction.hash; + registrationEvent.expiryDate = event.params.expirationTime; + registrationEvent.registrant = domain.owner; + registrationEvent.save(); +} + +export function handleTransfer(event: TransferEvent): void { + let account = new Account(event.params.to.toHex()); + account.save(); + + let label = uint256ToByteArray(event.params.tokenId); + let registration = Registration.load(label.toHex()); + if (registration == null) return; + + let domain = Domain.load(crypto.keccak256(concat(rootNode, label)).toHex())!; + + registration.registrant = account.id; + domain.registrant = account.id; + + domain.save(); + registration.save(); + + let transferEvent = new NameTransferred(createEventID(event)); + transferEvent.registration = label.toHex(); + transferEvent.blockNumber = event.block.number.toI32(); + transferEvent.transactionID = event.transaction.hash; + transferEvent.newOwner = account.id; + transferEvent.save(); +} diff --git a/src/utils.ts b/src/utils.ts index 632962f..c076971 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,7 @@ import { BigInt, ByteArray, ethereum, log } from "@graphprotocol/graph-ts"; +export const RSK_NODE = + "0x0cd5c10192478cd220936e91293afc15e3f6de4d419de5de7506b679cbdd8ec4"; export const ROOT_NODE = "0x0000000000000000000000000000000000000000000000000000000000000000"; export const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"; @@ -45,4 +47,33 @@ export function checkValidLabel(name: string | null): boolean { } return true; +} + +export function byteArrayFromHex(s: string): ByteArray { + if (s.length % 2 !== 0) { + throw new TypeError("Hex string must have an even number of characters"); + } + let out = new Uint8Array(s.length / 2); + for (var i = 0; i < s.length; i += 2) { + out[i / 2] = parseInt(s.substring(i, i + 2), 16) as u32; + } + return changetype(out); } + +export function uint256ToByteArray(i: BigInt): ByteArray { +let hex = i.toHex().slice(2).padStart(64, "0"); +return byteArrayFromHex(hex); +} + + // Helper for concatenating two byte arrays +export function concat(a: ByteArray, b: ByteArray): ByteArray { + let out = new Uint8Array(a.length + b.length); + for (let i = 0; i < a.length; i++) { + out[i] = a[i]; + } + for (let j = 0; j < b.length; j++) { + out[a.length + j] = b[j]; + } + // return out as ByteArray + return changetype(out); +} diff --git a/subgraph.yaml b/subgraph.yaml index ff251ec..98e8bde 100644 --- a/subgraph.yaml +++ b/subgraph.yaml @@ -33,3 +33,26 @@ dataSources: - event: NewTTL(indexed bytes32,uint64) handler: handleNewTTL file: ./src/rns.ts + - kind: ethereum + name: RSKOwner + network: rootstock + source: + address: "0x45d3e4FB311982A06bA52359d44cb4f5980e0ef1" + abi: RSKOwner + startBlock: 1891388 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - OwnershipTransferred + - RSKOwnerTransfer + abis: + - name: RSKOwner + file: ./abis/RSKOwner.json + eventHandlers: + - event: ExpirationChanged(uint256,uint256) + handler: handleExpirationChanged + - event: Transfer(indexed address,indexed address,indexed uint256) + handler: handleTransfer + file: ./src/rsk-owner.ts diff --git a/tests/.latest.json b/tests/.latest.json index 44eae77..e4b4cca 100644 --- a/tests/.latest.json +++ b/tests/.latest.json @@ -1,4 +1,4 @@ { "version": "0.6.0", - "timestamp": 1737655167212 + "timestamp": 1738072715812 } \ No newline at end of file diff --git a/tests/rsk-owner-utils.ts b/tests/rsk-owner-utils.ts new file mode 100644 index 0000000..fa5774e --- /dev/null +++ b/tests/rsk-owner-utils.ts @@ -0,0 +1,127 @@ +import { newMockEvent } from "matchstick-as" +import { ethereum, Address, BigInt } from "@graphprotocol/graph-ts" +import { + Approval, + ApprovalForAll, + ExpirationChanged, + OwnershipTransferred, + Transfer +} from "../generated/RSKOwner/RSKOwner" + +export function createApprovalEvent( + owner: Address, + approved: Address, + tokenId: BigInt +): Approval { + let approvalEvent = changetype(newMockEvent()) + + approvalEvent.parameters = new Array() + + approvalEvent.parameters.push( + new ethereum.EventParam("owner", ethereum.Value.fromAddress(owner)) + ) + approvalEvent.parameters.push( + new ethereum.EventParam("approved", ethereum.Value.fromAddress(approved)) + ) + approvalEvent.parameters.push( + new ethereum.EventParam( + "tokenId", + ethereum.Value.fromUnsignedBigInt(tokenId) + ) + ) + + return approvalEvent +} + +export function createApprovalForAllEvent( + owner: Address, + operator: Address, + approved: boolean +): ApprovalForAll { + let approvalForAllEvent = changetype(newMockEvent()) + + approvalForAllEvent.parameters = new Array() + + approvalForAllEvent.parameters.push( + new ethereum.EventParam("owner", ethereum.Value.fromAddress(owner)) + ) + approvalForAllEvent.parameters.push( + new ethereum.EventParam("operator", ethereum.Value.fromAddress(operator)) + ) + approvalForAllEvent.parameters.push( + new ethereum.EventParam("approved", ethereum.Value.fromBoolean(approved)) + ) + + return approvalForAllEvent +} + +export function createExpirationChangedEvent( + tokenId: BigInt, + expirationTime: BigInt +): ExpirationChanged { + let expirationChangedEvent = changetype(newMockEvent()) + + expirationChangedEvent.parameters = new Array() + + expirationChangedEvent.parameters.push( + new ethereum.EventParam( + "tokenId", + ethereum.Value.fromUnsignedBigInt(tokenId) + ) + ) + expirationChangedEvent.parameters.push( + new ethereum.EventParam( + "expirationTime", + ethereum.Value.fromUnsignedBigInt(expirationTime) + ) + ) + + return expirationChangedEvent +} + +export function createOwnershipTransferredEvent( + previousOwner: Address, + newOwner: Address +): OwnershipTransferred { + let ownershipTransferredEvent = + changetype(newMockEvent()) + + ownershipTransferredEvent.parameters = new Array() + + ownershipTransferredEvent.parameters.push( + new ethereum.EventParam( + "previousOwner", + ethereum.Value.fromAddress(previousOwner) + ) + ) + ownershipTransferredEvent.parameters.push( + new ethereum.EventParam("newOwner", ethereum.Value.fromAddress(newOwner)) + ) + + return ownershipTransferredEvent +} + +export function createTransferEvent( + from: Address, + to: Address, + tokenId: BigInt +): Transfer { + let transferEvent = changetype(newMockEvent()) + + transferEvent.parameters = new Array() + + transferEvent.parameters.push( + new ethereum.EventParam("from", ethereum.Value.fromAddress(from)) + ) + transferEvent.parameters.push( + new ethereum.EventParam("to", ethereum.Value.fromAddress(to)) + ) + transferEvent.parameters.push( + new ethereum.EventParam( + "tokenId", + ethereum.Value.fromUnsignedBigInt(tokenId) + ) + ) + + return transferEvent +} diff --git a/tests/rsk-owner.test.ts b/tests/rsk-owner.test.ts new file mode 100644 index 0000000..4389d6e --- /dev/null +++ b/tests/rsk-owner.test.ts @@ -0,0 +1,31 @@ +import { + assert, + describe, + test, + clearStore, + beforeAll, + afterAll +} from "matchstick-as/assembly/index" +import { Address, BigInt } from "@graphprotocol/graph-ts" +import { Approval as ApprovalEvent } from "../generated/RSKOwner/RSKOwner" +import { createApprovalEvent, createExpirationChangedEvent } from "./rsk-owner-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(() => { + //TODO: Implement test + }) + + afterAll(() => { + clearStore() + }) + + // For more test scenarios, see: + // https://thegraph.com/docs/en/developer/matchstick/#write-a-unit-test + + test("Approval created and stored", () => { + //TODO: Implement test + }) +})