From 391d74e2b35eee0063c01ad850c12aa681855e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Mon, 18 May 2026 17:32:17 -0300 Subject: [PATCH 1/3] feat: operators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- packages/subgraph/schema.graphql | 54 +++++ .../subgraph/src/config/arbitrum-one/index.ts | 5 +- .../src/config/arbitrum-one/operator-seed.ts | 227 ++++++++++++++++++ packages/subgraph/src/config/test/index.ts | 5 +- .../subgraph/src/config/test/operator-seed.ts | 3 + packages/subgraph/src/config/types.ts | 10 +- packages/subgraph/src/entities/operator.ts | 38 +++ .../src/entities/operatorAuthorization.ts | 56 +++++ packages/subgraph/src/handlers/migration.ts | 60 +++++ packages/subgraph/src/handlers/operator.ts | 47 ++++ packages/subgraph/src/mapping.ts | 1 + packages/subgraph/subgraph.yaml | 5 + packages/subgraph/tests/migration.test.ts | 171 ++++++++++++- packages/subgraph/tests/operator.test.ts | 213 ++++++++++++++++ packages/tools/package.json | 4 +- packages/tools/src/onchain.ts | 13 + packages/tools/src/seed/operators.ts | 130 ++++++++++ packages/tools/src/validation/internal.ts | 112 ++++++++- .../tools/src/validation/onchain/operators.ts | 89 +++++++ 19 files changed, 1236 insertions(+), 7 deletions(-) create mode 100644 packages/subgraph/src/config/arbitrum-one/operator-seed.ts create mode 100644 packages/subgraph/src/config/test/operator-seed.ts create mode 100644 packages/subgraph/src/entities/operator.ts create mode 100644 packages/subgraph/src/entities/operatorAuthorization.ts create mode 100644 packages/subgraph/src/handlers/operator.ts create mode 100644 packages/subgraph/tests/operator.test.ts create mode 100644 packages/tools/src/seed/operators.ts create mode 100644 packages/tools/src/validation/onchain/operators.ts diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index 1d7579d..f043b2d 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -48,6 +48,8 @@ type ServiceProvider @entity(immutable: false) { delegationPools: [DelegationPool!]! @derivedFrom(field: "serviceProvider") "Provision thaw requests for this service provider" provisionThawRequests: [ProvisionThawRequest!]! @derivedFrom(field: "serviceProvider") + "Operator authorizations for this service provider" + operatorAuthorizations: [OperatorAuthorization!]! @derivedFrom(field: "serviceProvider") # Counts "Number of active provisions" @@ -103,6 +105,8 @@ type DataService @entity(immutable: false) { delegationPools: [DelegationPool!]! @derivedFrom(field: "dataService") "Provision thaw requests for this data service" provisionThawRequests: [ProvisionThawRequest!]! @derivedFrom(field: "dataService") + "Operator authorizations for this data service" + operatorAuthorizations: [OperatorAuthorization!]! @derivedFrom(field: "dataService") # Counts "Active service providers with provisions to this data service" @@ -282,3 +286,53 @@ type ProvisionThawRequest @entity(immutable: false) { "Timestamp when entity was last updated" updatedAt: BigInt! } + +type Operator @entity(immutable: false) { + "Operator address" + id: Bytes! + + # Relationships + "Authorizations granted to this operator" + authorizations: [OperatorAuthorization!]! @derivedFrom(field: "operator") + + # Counts + "Active authorizations (allowed=true)" + countAuthorizations: Int! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! +} + +type OperatorAuthorization @entity(immutable: false) { + "Composite ID: operator-serviceProvider-dataService" + id: Bytes! + + # Relationships + "Operator that is authorized" + operator: Operator! + "Service provider that granted the authorization" + serviceProvider: ServiceProvider! + "Data service this authorization applies to" + dataService: DataService! + + # State + "Whether the operator is currently authorized" + allowed: Boolean! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! +} diff --git a/packages/subgraph/src/config/arbitrum-one/index.ts b/packages/subgraph/src/config/arbitrum-one/index.ts index b818257..68633f1 100644 --- a/packages/subgraph/src/config/arbitrum-one/index.ts +++ b/packages/subgraph/src/config/arbitrum-one/index.ts @@ -2,6 +2,7 @@ import { Address } from "@graphprotocol/graph-ts" import { NetworkConfig } from "../types" import { SERVICE_PROVIDER_ADDRESSES } from "./indexer-seed" import { DELEGATED_INDEXER_ADDRESSES, LEGACY_INDEXER_REWARD_CUTS } from "./delegation-seed" +import { OPERATOR_SERVICE_PROVIDERS, OPERATORS } from "./operator-seed" export const config = new NetworkConfig( "arbitrum-one", @@ -10,5 +11,7 @@ export const config = new NetworkConfig( 408_825_706, SERVICE_PROVIDER_ADDRESSES, DELEGATED_INDEXER_ADDRESSES, - LEGACY_INDEXER_REWARD_CUTS + LEGACY_INDEXER_REWARD_CUTS, + OPERATOR_SERVICE_PROVIDERS, + OPERATORS ) diff --git a/packages/subgraph/src/config/arbitrum-one/operator-seed.ts b/packages/subgraph/src/config/arbitrum-one/operator-seed.ts new file mode 100644 index 0000000..898dafe --- /dev/null +++ b/packages/subgraph/src/config/arbitrum-one/operator-seed.ts @@ -0,0 +1,227 @@ +// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY +// Regenerate with: cd packages/tools && NETWORK=arbitrum-one pnpm seed:operators +// Generated: 2026-05-18T20:24:20.323Z +// Network: arbitrum-one +// Block: 408825706 +// Count: 107 + +// Parallel arrays: OPERATOR_SERVICE_PROVIDERS[i] authorized OPERATORS[i] +export const OPERATOR_SERVICE_PROVIDERS: string[] = [ + "0x0058223c6617cca7ce76fc929ec9724cd43d4542", + "0x0058223c6617cca7ce76fc929ec9724cd43d4542", + "0x01f17c392614c7ea586e7272ed348efee21b90a3", + "0x0874e792462406dc12ee96b75e52a3bdbba3a123", + "0x089f78d8cf0a5ae1b7a581b1910a73f8cb3e4774", + "0x090f7382f9ea85c733cd501f4d87f16cb5b83ed3", + "0x0b9d582b7fdd387ba13ad7f453d49af255a8ed5e", + "0x0ee85c30ff1797d9f041261b88c4a58d6d68fbbf", + "0x0fd8fd1dc8162148cb9413062fe6c6b144335dbf", + "0x17def1a43a323c711c7a32101ecf41e58eff54a2", + "0x19a4fe7d0c76490cca77b45580846cdb38b9a406", + "0x1b7e0068ca1d7929c8c56408d766e1510e54d98d", + "0x1b92e4cba0f82c85c1298af861247849988c788c", + "0x2121bc6437100fc21d19a9eea30898419e020afa", + "0x269ebeee083ce6f70486a67dc8036a889bf322a9", + "0x2b3c7d1ef5fdfc0557934019c531d3e70d6200ae", + "0x2e15f3f0d37b191c33ee06e953c8cce4c493b47a", + "0x2e8d26e9b0d280738728e71c37bf05e70a636238", + "0x2e8d26e9b0d280738728e71c37bf05e70a636238", + "0x2f09092aacd80196fc984908c5a9a7ab3ee4f1ce", + "0x326c584e0f0eab1f1f83c93cc6ae1acc0feba0bc", + "0x32bbd16a94ebb289edceebe77f35acc82664157b", + "0x35917c0eb91d2e21bef40940d028940484230c06", + "0x35917c0eb91d2e21bef40940d028940484230c06", + "0x3717cef8020bddee7a18f4efb2bfa88fefdcb1bc", + "0x3717cef8020bddee7a18f4efb2bfa88fefdcb1bc", + "0x3717cef8020bddee7a18f4efb2bfa88fefdcb1bc", + "0x3717cef8020bddee7a18f4efb2bfa88fefdcb1bc", + "0x3863a65ce278a240f9aa2a4b4a48493be59e6139", + "0x38f412c8d6346a17a53ff9ceecd2e01acecd27c0", + "0x3b9ba748691f135b71582dc3292e5e3ed7e13341", + "0x3e1536fc83cd5bed83a521a26034ff3e59c6a7c4", + "0x3f74870f80ff7449fe4c6ff257da5fa72734c970", + "0x474e571ab6dd77489ec3c7ddf9cbc893fcba684c", + "0x4d67938e9b07681526fe0345a45b770bba88c659", + "0x4e5c87772c29381bcabc58c3f182b6633b5a274a", + "0x51637a35f7f054c98ed51904de939b9561d37885", + "0x53dbbc9d916b1840a2c4c26b150ba2e13f36e10f", + "0x550c1f4814a85aa10f5f061ca8c45e2ee9620226", + "0x59749d1fa9635cd0413aeff5ee295490a7e87f54", + "0x5af569b692b0598721461027dbbecde74d465d99", + "0x5b3c8f7245dfbd9bea22d9c4b975df60a638e5a3", + "0x5ddee9720e17aad28febb55643cd8ab50c51c60b", + "0x600f2b53719e1dbacf340572b31a9df9921b82fa", + "0x60df13b7a598772e992f9365fba5ed6e1529e79a", + "0x63c9dc729ba7a22bb8605216b24a34b902e5fe94", + "0x6f3ce93a09f30f18d728d2364268b5fe9444b89e", + "0x6f8a032b4b1ee622ef2f0fc091bdbb98cfae81a3", + "0x6f9bb7e454f5b3eb2310343f0e99269dc2bb8a1d", + "0x735f422922bd4d459ea491f46e1bb9295b89f961", + "0x74dbb201ecc0b16934e68377bc13013883d9417b", + "0x74dbb201ecc0b16934e68377bc13013883d9417b", + "0x7bb834017672b1135466661d8dd69c5dd0b3bf51", + "0x8bbe94c2894f76406568dfb44e905dac4b7df699", + "0x8cc22436ba6f07a4d5dd2043e3109267eee5aab8", + "0x8d632dfc2454d624910fe982e85a5b15d2ae93c5", + "0x8f689a83dd52eaa1d5ad6a40c46189b4a0d70b06", + "0x9082f497bdc512d08ffde50d6fce28e72c2addcf", + "0x918fcc24e6b7f5ec73b4cf766e2393d8fe707541", + "0x920fdeb00ee04dd72f62d8a8f80f13c82ef76c1e", + "0x9611663c4057cbfc9d2589a04fcac24dbd38612d", + "0x9af3fc811a66dbbca44acce94906d8743f9cf0d0", + "0x9da1017766bfeb2835db4f811516eea68996538b", + "0xa01b06b0e9feb016d5ab669ce89d059bc666e569", + "0xa181d0f242b3730f8a244cc94eda05faf17a43e8", + "0xa28a99b0219a34142a9398a19460fcd69250a2b2", + "0xa28a99b0219a34142a9398a19460fcd69250a2b2", + "0xa4d629ba2ffb3321008d8cec37cca077696bf24d", + "0xa6ff993e0f6253f1b7f55c873577a2f0f0ceb325", + "0xaa988dcb035518bc0e20082a3148a5d3dfd1776d", + "0xaddd3e23599d2b7267067afbcd18830aefca640a", + "0xae9bfdf9eeec808f4f3f6f455cb1968445cc6f2f", + "0xb4b4570df6f7fe320f10fdfb702dba7e35244550", + "0xb54c7c9fa1a51300e6054b70ae9952c1fb2800b4", + "0xb8ca929e2bd96548cabcabc56cfc9a5147cef0ff", + "0xbdfb5ee5a2abf4fc7bb1bd1221067aef7f9de491", + "0xc55c63563efb36f7cc65ac3060c52987c6694b37", + "0xc9014686f6336ad558b539565d5dff840b339082", + "0xd9819426c82e2b8fc58b9b62a78efe93f78077c6", + "0xda20dde459c8d918f81566995d899a046d4d8503", + "0xda20dde459c8d918f81566995d899a046d4d8503", + "0xdc53e62df89fd07b31ed4ff886397b9e7ae4625e", + "0xdc7daef4d0751a9f6ec28b06d6d9475b13eb0918", + "0xdeb712db301285ed483ef9e02dd08a1980f273f1", + "0xdec965f0604125be05cd8a136c85d02ef344d61a", + "0xdecba5154aab37ae5e381a19f804f3af4d1bcbb5", + "0xe0ceec6daa59cc951f3f71d6fc4221e55ef6c386", + "0xe13840a2e92e0cb17a246609b432d0fa2e418774", + "0xe48b586eeb81bde60f14b0b8d80ddd06c7a24720", + "0xe63e935fba572784d5aa40715e372e7948bbdb12", + "0xe6de2325ef1aac1f058fae59d3c38a472f569846", + "0xe91273727203bcc827521fc8b0c762d435c3c5d0", + "0xe9e284277648fcdb09b8efc1832c73c09b5ecf59", + "0xeccdf8231326a9c5aad32df76a633aaa4c49b104", + "0xedca8740873152ff30a2696add66d1ab41882beb", + "0xedca8740873152ff30a2696add66d1ab41882beb", + "0xeeeee689aa442c607105f29f06d00d2f748776b2", + "0xf00f7157fa8fd0420b87956d46058a16b2f23adc", + "0xf03e9a7e40f09772c3c368b9de14c6d7370717b9", + "0xf435dee64819590c1a3f5913822e1c04afebe695", + "0xf7793bf9561c32ffbac603ce572fa55643f9cf72", + "0xf9123292b4d958c53aaad8c5df0138ee0e62944b", + "0xf92f430dd8567b0d466358c79594ab58d919a6d4", + "0xfb168335f5a3868a03696904ed38fa95fd167c0e", + "0xfc842f81490dcb37e82d416b2d28327dfb24ba9a", + "0xfc842f81490dcb37e82d416b2d28327dfb24ba9a", + "0xfeff9093f6b32d0e5cddba743b06a1fedb87c004", +] + +export const OPERATORS: string[] = [ + "0xcb587db2cdce8d70fbd57bb3a0a23cbd51cadc23", + "0xf1f36b1aab2f68c44e211ea655ee3a6576ee1cf4", + "0x21cad52a1e4d16376bc56c37e2a4f53c30c34790", + "0x81dfde2e53f282f7bd774d469e682abf1a33c32c", + "0x1328d449ec11a3ee466016b642ea8ff85870ebb9", + "0x008847c358253ac18ae6bf787954d9fc57e87e1f", + "0x9aec09debdac0dd52a5d7b15b4ae90fb20d2b6a2", + "0xd0d121095d212d0d2bb7867313a762ce8e89ed8a", + "0x7a361db89c9419699def3349b5f6f1cba294267d", + "0xb31afa14502b13287c1c0bdb955b34bb1189573a", + "0xe4b3fb76309adcd591f68213617fa225a18ecbab", + "0xd96d4b52cab35cf3df1d58765bd2ea7cb1fb6016", + "0xa4a5b542d7d0a820f5f90834ca65e0bb343d44ea", + "0xbe5f65208a0a01e79fdb6582d02cade4984fd9bf", + "0xe4401820cb60376ece83070d4bf133f12a1043bb", + "0xaa586c2ac571c3ca8cebd8520008ccdfab91af29", + "0x81c02eb4adc18686e8c1f4cd18bd6a3c3cc27bd5", + "0x31c1439b961645a3b2e04b987f275b8cc16e8bb6", + "0xd343449961f5a4f0b2b7fbcc4e8cfff86f57bfb1", + "0xf100e16d856bedee8ae0e0eb4f568f7aab21b5b7", + "0xd084de43362e1d67460864d1ed3ac2f435d93e26", + "0x2a3be7ac64e532e9a840c728a4750c5b8fea618e", + "0x898b80b6c99f8099f7244053cc604ccdff55212e", + "0xd4b1775640d9c4ba2ce5bf2b3201b35c4031aa47", + "0x1e1f84c07e0471fc979f6f08228b0bd34cda9e54", + "0x471c477a7624d456e6a93924385235c353d537bf", + "0x488d2b84c9edc11f4caa47ba6795cf3353f2aea1", + "0xa3c29d40eda9ab40afd6b9cfa55090602fa2bbc6", + "0x7db53586e8facac79ca934494bb2230171162c9e", + "0x7fbdfa921f014f00761d60fbc26e288fd0d3a053", + "0x9ca90235aa00c825785ec87dbdcc7921c03c3eed", + "0xa38bd08c240815230fea21f81d67f443f9651192", + "0x721bc8d6f143744c6febfc496cb6a8d66ce078a8", + "0x99161027ef082a5649ad244ddc4d52f264f89348", + "0x04a969d08da88024997127dd0748ad3b51955839", + "0xc56961836857210e256d71c91a62e90865075380", + "0x147d9046efc8184247dfb2e048feac7a4f3aa1e2", + "0xc326fffb2afece3510ebe11c540549a6f6b302d1", + "0xccb880d9e1034508a834ac5b9bc3aaa218f8f4e6", + "0xe462ddeac1af0e7521acc0c29964d2db627a59c2", + "0x63f59ef3211ba89626e9efdf91457cf1513a0af5", + "0x0c154108df7020f281be6ff2742525d0abce6036", + "0x1282e5da81954cd69d69e265c9ea9255fe504ed4", + "0x02b6e83e39357582df034e85fae232df327b2a89", + "0x0262a6546eeadc954567b4ba97bbf387ebe040b9", + "0xd6c6329d76be22ed86292ed26fd462b001b83a9f", + "0x89c8179a69344ad27cb35cda25e6d619a311afb1", + "0x2963e4767f5fa21f637bb11fafa619fead21f4d1", + "0x6309487599498698b459c34c466aeb29f25435c1", + "0x9294e7e1639e267165ed03b08e904184f8ac457f", + "0x64e4e9e67faef83532f5756ea691c0f5188011d4", + "0xd48aa9ec9f9ca173191102f255022ce7f811b77b", + "0x8a6573089682b8d224af27f83a375ef6048d317d", + "0x85e02223c30250c66aef6413f6e04087a988c325", + "0x5bcbb94c65cbbd952463a63890c236294c897d3e", + "0x703d5b484dfed095eade0fc6456eebef9d87c71a", + "0x2e6d49c98ceee380548a67c14897576bc132dcd8", + "0x44329367298f28cd32e26b73ff64bdd172379a94", + "0xe151a98ca6788b7103008c2e4061f23fda6d2042", + "0x7eb26055fd210b1a80cb5e6c0caac1d6399f90ed", + "0xe426e658bedc8d0afe9d0a57ff705dd42a50c6fa", + "0x8221f968c7bf43938f262bb2ecb70f8e0dbf06d2", + "0xda8e435d1659558a8813eadf36945be8041d7374", + "0x545f313f5cdf901cfda0c1319a43604dcfc71f39", + "0xf613218a0b702e983fbbfadd3c7105e7dcdf0589", + "0x0d3de01db98be8b9c7ce5745ae5699380947165f", + "0xe647ef7fbbdf2c49cf71f656e56f6a82b5600466", + "0xdd0caf9acb11e753f8d86b2fc7f537463135d678", + "0x80a8733485048ff51e2481016734a49358db5f1f", + "0x16a327ada54f947c1ae6ca75ab23065a8bd3dd5d", + "0x3f1ba83607efc8ef9c4fb95e55c6aaa2cc2b2b88", + "0x6374212f939d626b72a50d961c82b23e769c7ad5", + "0xc3874c96bd6f1577df94a56e3462afb414de7d55", + "0xa3d7ea74d91dde6438551446594fa2675a275f5a", + "0x8a1a6137047218f5a38f33580f73f4be10f79f6c", + "0x8b34351ad4e6fb049c14adfc90ac5ac8923ba900", + "0xfc34b8b6ae8fc85a11fcac84fe0713407e23ac0d", + "0xf1d87a5da43676393ca4c621435f612a874f6257", + "0x4f0107cb03b15a4eebd803f8c4ef53a5c2b65cfe", + "0x48032c6a1e521fbae6412f499126257999fe46f8", + "0xe104763679499bcd9c788eef027315a2f1bd085a", + "0xb3a0ddf05b7b89af588173a92f8c0089aa5a72fd", + "0xcbb88411bdef7b877c16e68ea2b548d32e200266", + "0xaa1bea18d2dd7f5e73a29bc9ed7dfe7eb58c8321", + "0x9c8e14065758e52402b79520f838d70aaaaa62a8", + "0xc4e706f488751cecc0a184e5a7acff5f35b0d5bb", + "0xecede8fe27400f2ae144d6e1e30a7f387f8fa1ea", + "0xf6bad1dca165c492006317b4f6e03a1ef5414e23", + "0x532cb76a4002e9553b1b7e307a609cf0ea19578a", + "0x5d8006da36be0fe9ef389400b804ce6e52a3940e", + "0x21628b7c040d8380dfac7e91ea1b827c51b048a1", + "0x0a00b06b5bac696e9321ba1e29f05408e7c32f8c", + "0xa404aa997f72de2a2df3adc34ae33c898c1193c4", + "0x2cbfd91c6d7a04064d1930815c6a0f0c4f3de119", + "0x1e1f84c07e0471fc979f6f08228b0bd34cda9e54", + "0x49d581486438aad93f4114084ac5b09a8b7c9685", + "0xcd09fc5fc328dfeb2792248b03fee9a0c0b216aa", + "0xb0cccab26193c6df7d9997a88e146b0f0a7d3e36", + "0xfcd4b1b00d1fd1fc8035a196c7fdb28715c393cd", + "0xdab89eb9985e1dffdb1a7e3385f91be9fe70a7e3", + "0xdb9ae7fe5daf3db0edc5bca2bf712fee60e4b515", + "0xd1e69306435ecb5ccde99d58d8c80fa76beadcf6", + "0x4ecb19a2ac49c5decfa5e65b6669c7e7fab5da9d", + "0xd48c6bdfc69fac9352e4bee7adf13b38c6da1033", + "0x0c08b61c622b9097e3e06963371c1b8dd3e5ab9c", + "0x7af775d066afe4453c10ad438ff44c9a960847b1", + "0x2e916eb44545fd90a9f9cc540d741c4d2f34551a", +] diff --git a/packages/subgraph/src/config/test/index.ts b/packages/subgraph/src/config/test/index.ts index 6fbb272..9efb70a 100644 --- a/packages/subgraph/src/config/test/index.ts +++ b/packages/subgraph/src/config/test/index.ts @@ -2,6 +2,7 @@ import { Address } from "@graphprotocol/graph-ts" import { NetworkConfig } from "../types" import { SERVICE_PROVIDER_ADDRESSES } from "./indexer-seed" import { DELEGATED_INDEXER_ADDRESSES, LEGACY_INDEXER_REWARD_CUTS } from "./delegation-seed" +import { OPERATOR_SERVICE_PROVIDERS, OPERATORS } from "./operator-seed" export const config = new NetworkConfig( "test", @@ -10,5 +11,7 @@ export const config = new NetworkConfig( 1, SERVICE_PROVIDER_ADDRESSES, DELEGATED_INDEXER_ADDRESSES, - LEGACY_INDEXER_REWARD_CUTS + LEGACY_INDEXER_REWARD_CUTS, + OPERATOR_SERVICE_PROVIDERS, + OPERATORS ) diff --git a/packages/subgraph/src/config/test/operator-seed.ts b/packages/subgraph/src/config/test/operator-seed.ts new file mode 100644 index 0000000..413ea2f --- /dev/null +++ b/packages/subgraph/src/config/test/operator-seed.ts @@ -0,0 +1,3 @@ +// Test operator seed - empty for unit tests +export const OPERATOR_SERVICE_PROVIDERS: string[] = [] +export const OPERATORS: string[] = [] diff --git a/packages/subgraph/src/config/types.ts b/packages/subgraph/src/config/types.ts index ccdec33..78a93aa 100644 --- a/packages/subgraph/src/config/types.ts +++ b/packages/subgraph/src/config/types.ts @@ -15,6 +15,10 @@ export class NetworkConfig { // Parallel array with delegatedIndexerAddresses - same index = same indexer // Used to calculate delegation rewards from legacy indexing rewards legacyIndexerRewardCuts: i32[] + // Legacy operator authorizations at Horizon genesis + // Parallel arrays: operatorServiceProviders[i] authorized operators[i] + operatorServiceProviders: string[] + operators: string[] constructor( network: string, @@ -23,7 +27,9 @@ export class NetworkConfig { startBlock: i32, serviceProviderAddresses: string[], delegatedIndexerAddresses: string[], - legacyIndexerRewardCuts: i32[] + legacyIndexerRewardCuts: i32[], + operatorServiceProviders: string[], + operators: string[] ) { this.network = network this.horizonStakingAddress = horizonStakingAddress @@ -32,5 +38,7 @@ export class NetworkConfig { this.serviceProviderAddresses = serviceProviderAddresses this.delegatedIndexerAddresses = delegatedIndexerAddresses this.legacyIndexerRewardCuts = legacyIndexerRewardCuts + this.operatorServiceProviders = operatorServiceProviders + this.operators = operators } } diff --git a/packages/subgraph/src/entities/operator.ts b/packages/subgraph/src/entities/operator.ts new file mode 100644 index 0000000..ab31190 --- /dev/null +++ b/packages/subgraph/src/entities/operator.ts @@ -0,0 +1,38 @@ +import { BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" +import { Operator } from "../../generated/schema" + +export class OperatorResult { + entity: Operator + isNew: boolean + + constructor(entity: Operator, isNew: boolean) { + this.entity = entity + this.isNew = isNew + } +} + +export function getOrCreateOperator( + operatorAddress: Bytes, + blockNumber: BigInt, + timestamp: BigInt +): OperatorResult { + let entity = Operator.load(operatorAddress) + let isNew = entity == null + + if (entity == null) { + entity = new Operator(operatorAddress) + entity.countAuthorizations = 0 + entity.createdAtBlock = blockNumber + entity.createdAt = timestamp + entity.updatedAtBlock = blockNumber + entity.updatedAt = timestamp + } + + return new OperatorResult(entity, isNew) +} + +export function saveOperator(operator: Operator, block: ethereum.Block): void { + operator.updatedAtBlock = block.number + operator.updatedAt = block.timestamp + operator.save() +} diff --git a/packages/subgraph/src/entities/operatorAuthorization.ts b/packages/subgraph/src/entities/operatorAuthorization.ts new file mode 100644 index 0000000..be089df --- /dev/null +++ b/packages/subgraph/src/entities/operatorAuthorization.ts @@ -0,0 +1,56 @@ +import { BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" +import { OperatorAuthorization } from "../../generated/schema" +import { threePartId } from "../common/ids" + +export function getOperatorAuthorizationId( + operator: Bytes, + serviceProvider: Bytes, + dataService: Bytes +): Bytes { + return threePartId(operator, serviceProvider, dataService) +} + +export class OperatorAuthorizationResult { + entity: OperatorAuthorization + isNew: boolean + + constructor(entity: OperatorAuthorization, isNew: boolean) { + this.entity = entity + this.isNew = isNew + } +} + +export function getOrCreateOperatorAuthorization( + operator: Bytes, + serviceProvider: Bytes, + dataService: Bytes, + blockNumber: BigInt, + timestamp: BigInt +): OperatorAuthorizationResult { + let id = getOperatorAuthorizationId(operator, serviceProvider, dataService) + let entity = OperatorAuthorization.load(id) + let isNew = entity == null + + if (entity == null) { + entity = new OperatorAuthorization(id) + entity.operator = operator + entity.serviceProvider = serviceProvider + entity.dataService = dataService + entity.allowed = false + entity.createdAtBlock = blockNumber + entity.createdAt = timestamp + entity.updatedAtBlock = blockNumber + entity.updatedAt = timestamp + } + + return new OperatorAuthorizationResult(entity, isNew) +} + +export function saveOperatorAuthorization( + authorization: OperatorAuthorization, + block: ethereum.Block +): void { + authorization.updatedAtBlock = block.number + authorization.updatedAt = block.timestamp + authorization.save() +} diff --git a/packages/subgraph/src/handlers/migration.ts b/packages/subgraph/src/handlers/migration.ts index 6ae8607..9f47506 100644 --- a/packages/subgraph/src/handlers/migration.ts +++ b/packages/subgraph/src/handlers/migration.ts @@ -4,6 +4,11 @@ import { getOrCreateGraphNetwork, saveGraphNetwork } from "../entities/graphNetw import { getOrCreateServiceProvider, saveServiceProvider } from "../entities/serviceProvider" import { getOrCreateDataService, saveDataService } from "../entities/dataService" import { getOrCreateDelegationPool, saveDelegationPool } from "../entities/delegationPool" +import { getOrCreateOperator, saveOperator } from "../entities/operator" +import { + getOrCreateOperatorAuthorization, + saveOperatorAuthorization, +} from "../entities/operatorAuthorization" import { config } from "../config" import { NetworkConfig } from "../config/types" import { @@ -34,6 +39,7 @@ const MULTICALL_BATCH_SIZE = 100 export function handleHorizonGenesisBlock(block: ethereum.Block): void { migrateServiceProviders(block, config) migrateDelegationPools(block, config) + migrateOperators(block, config) } /** @@ -186,3 +192,57 @@ export function migrateDelegationPools(block: ethereum.Block, networkConfig: Net saveDataService(dataService.entity, block) saveGraphNetwork(graphNetwork) } + +/** + * Seeds Operator and OperatorAuthorization entities for legacy operators. + * + * Before Horizon, operators were global (not per-verifier). After Horizon, + * these legacy operators are considered authorized for the Subgraph Service. + * This function creates the necessary entities from a seed list generated + * by querying the legacy network subgraph at Horizon genesis block. + * + * Uses parallel arrays: operatorServiceProviders[i] authorized operators[i] + */ +export function migrateOperators(block: ethereum.Block, networkConfig: NetworkConfig): void { + // Skip if no operators to migrate + if (networkConfig.operators.length == 0) { + return + } + + assert( + networkConfig.operatorServiceProviders.length == networkConfig.operators.length, + "Operator seed arrays must have same length" + ) + + let verifier = networkConfig.subgraphServiceAddress + let verifierBytes = Bytes.fromHexString(verifier.toHexString()) as Bytes + + let dataService = getOrCreateDataService(verifierBytes, block.number, block.timestamp) + + for (let i = 0; i < networkConfig.operators.length; i++) { + let operatorAddress = Address.fromString(networkConfig.operators[i]) + let serviceProviderAddress = Address.fromString(networkConfig.operatorServiceProviders[i]) + + let operatorBytes = Bytes.fromHexString(operatorAddress.toHexString()) as Bytes + let serviceProviderBytes = Bytes.fromHexString(serviceProviderAddress.toHexString()) as Bytes + + // Get or create Operator entity + let operator = getOrCreateOperator(operatorBytes, block.number, block.timestamp) + operator.entity.countAuthorizations += 1 + saveOperator(operator.entity, block) + + // Create OperatorAuthorization entity + let authorization = getOrCreateOperatorAuthorization( + operatorBytes, + serviceProviderBytes, + verifierBytes, + block.number, + block.timestamp + ) + assert(authorization.isNew, "OperatorAuthorization already exists during migration") + authorization.entity.allowed = true + saveOperatorAuthorization(authorization.entity, block) + } + + saveDataService(dataService.entity, block) +} diff --git a/packages/subgraph/src/handlers/operator.ts b/packages/subgraph/src/handlers/operator.ts new file mode 100644 index 0000000..825cd59 --- /dev/null +++ b/packages/subgraph/src/handlers/operator.ts @@ -0,0 +1,47 @@ +import { Bytes } from "@graphprotocol/graph-ts" +import { OperatorSet } from "../../generated/HorizonStaking/HorizonStaking" +import { getOrCreateOperator, saveOperator } from "../entities/operator" +import { + getOrCreateOperatorAuthorization, + saveOperatorAuthorization, +} from "../entities/operatorAuthorization" + +/** + * Handles OperatorSet event. + * Creates or updates Operator and OperatorAuthorization entities when a service provider + * authorizes or revokes an operator for a specific data service. + */ +export function handleOperatorSet(event: OperatorSet): void { + let operatorBytes = Bytes.fromHexString(event.params.operator.toHexString()) as Bytes + let serviceProviderBytes = Bytes.fromHexString(event.params.serviceProvider.toHexString()) as Bytes + let dataServiceBytes = Bytes.fromHexString(event.params.verifier.toHexString()) as Bytes + + // Get or create Operator entity + let operator = getOrCreateOperator(operatorBytes, event.block.number, event.block.timestamp) + + // Get or create OperatorAuthorization entity + let authorization = getOrCreateOperatorAuthorization( + operatorBytes, + serviceProviderBytes, + dataServiceBytes, + event.block.number, + event.block.timestamp + ) + + // Track authorization count changes on Operator + let wasAllowed = authorization.entity.allowed + let isAllowed = event.params.allowed + + if (!wasAllowed && isAllowed) { + // New authorization + operator.entity.countAuthorizations += 1 + } else if (wasAllowed && !isAllowed) { + // Revoked authorization + operator.entity.countAuthorizations -= 1 + } + + authorization.entity.allowed = isAllowed + + saveOperator(operator.entity, event.block) + saveOperatorAuthorization(authorization.entity, event.block) +} diff --git a/packages/subgraph/src/mapping.ts b/packages/subgraph/src/mapping.ts index 86299a8..a655204 100644 --- a/packages/subgraph/src/mapping.ts +++ b/packages/subgraph/src/mapping.ts @@ -26,3 +26,4 @@ export { handleThawRequestFulfilled } from "./handlers/thawRequest" export { handleDelegationFeeCutSet } from "./handlers/feeCut" +export { handleOperatorSet } from "./handlers/operator" diff --git a/packages/subgraph/subgraph.yaml b/packages/subgraph/subgraph.yaml index 1b5f7cd..5787a6d 100644 --- a/packages/subgraph/subgraph.yaml +++ b/packages/subgraph/subgraph.yaml @@ -23,6 +23,8 @@ dataSources: - DelegationPool - ProvisionThawRequest - ProvisionFeeCut + - Operator + - OperatorAuthorization abis: - name: HorizonStaking file: ./abis/HorizonStaking.json @@ -62,6 +64,9 @@ dataSources: handler: handleDelegationSlashed - event: DelegationFeeCutSet(indexed address,indexed address,indexed uint8,uint256) handler: handleDelegationFeeCutSet + # Operator events + - event: OperatorSet(indexed address,indexed address,indexed address,bool) + handler: handleOperatorSet # Thaw request events - event: ThawRequestCreated(indexed uint8,indexed address,indexed address,address,uint256,uint64,bytes32,uint256) handler: handleThawRequestCreated diff --git a/packages/subgraph/tests/migration.test.ts b/packages/subgraph/tests/migration.test.ts index 2350f8e..7a4e5a9 100644 --- a/packages/subgraph/tests/migration.test.ts +++ b/packages/subgraph/tests/migration.test.ts @@ -8,10 +8,11 @@ import { createMockedFunction, } from "matchstick-as" import { Address, BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" -import { migrateServiceProviders } from "../src/handlers/migration" +import { migrateServiceProviders, migrateOperators } from "../src/handlers/migration" import { GRAPH_NETWORK_ID } from "../src/common/constants" import { testConfig, NetworkConfig } from "../src/config" import { encodeGetStake } from "../src/common/multicall" +import { getOperatorAuthorizationId } from "../src/entities/operatorAuthorization" // Helper to create a mock block function createMockBlock(number: i32, timestamp: i32): ethereum.Block { @@ -155,7 +156,9 @@ describe("migrateServiceProviders with empty config", () => { 1, [], // empty service provider addresses [], // empty delegated indexer addresses - [] // empty legacy indexer reward cuts + [], // empty legacy indexer reward cuts + [], // empty operator service providers + [] // empty operators ) let block = createMockBlock(100, 1000) @@ -170,3 +173,167 @@ describe("migrateServiceProviders with empty config", () => { assert.entityCount("ServiceProvider", 0) }) }) + +describe("migrateOperators", () => { + beforeEach(() => { + clearStore() + }) + + afterEach(() => { + clearStore() + }) + + test("creates Operator and OperatorAuthorization entities for each operator in config", () => { + // Create config with operator data + let operatorConfig = new NetworkConfig( + "test-operators", + testConfig.horizonStakingAddress, + testConfig.subgraphServiceAddress, + 1, + [], + [], + [], + [ + "0x1111111111111111111111111111111111111111", // SP1 + "0x1111111111111111111111111111111111111111", // SP1 (has 2 operators) + "0x2222222222222222222222222222222222222222", // SP2 + ], + [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // Operator A for SP1 + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", // Operator B for SP1 + "0xcccccccccccccccccccccccccccccccccccccccc", // Operator C for SP2 + ] + ) + + let block = createMockBlock(100, 1000) + migrateOperators(block, operatorConfig) + + // Should have 3 unique operators + assert.entityCount("Operator", 3) + + // Should have 3 authorizations + assert.entityCount("OperatorAuthorization", 3) + + // Verify Operator entities + let operatorA = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + let operatorB = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + let operatorC = "0xcccccccccccccccccccccccccccccccccccccccc" + + assert.fieldEquals("Operator", operatorA, "countAuthorizations", "1") + assert.fieldEquals("Operator", operatorB, "countAuthorizations", "1") + assert.fieldEquals("Operator", operatorC, "countAuthorizations", "1") + + // Verify all authorizations are allowed + let sp1 = Bytes.fromHexString("0x1111111111111111111111111111111111111111") + let sp2 = Bytes.fromHexString("0x2222222222222222222222222222222222222222") + let verifier = Bytes.fromHexString(testConfig.subgraphServiceAddress.toHexString()) + + let authIdA = getOperatorAuthorizationId( + Bytes.fromHexString(operatorA), + sp1, + verifier + ).toHexString() + let authIdB = getOperatorAuthorizationId( + Bytes.fromHexString(operatorB), + sp1, + verifier + ).toHexString() + let authIdC = getOperatorAuthorizationId( + Bytes.fromHexString(operatorC), + sp2, + verifier + ).toHexString() + + assert.fieldEquals("OperatorAuthorization", authIdA, "allowed", "true") + assert.fieldEquals("OperatorAuthorization", authIdB, "allowed", "true") + assert.fieldEquals("OperatorAuthorization", authIdC, "allowed", "true") + }) + + test("same operator authorized by multiple service providers", () => { + // Operator A is authorized by both SP1 and SP2 + let operatorConfig = new NetworkConfig( + "test-shared-operator", + testConfig.horizonStakingAddress, + testConfig.subgraphServiceAddress, + 1, + [], + [], + [], + [ + "0x1111111111111111111111111111111111111111", // SP1 + "0x2222222222222222222222222222222222222222", // SP2 + ], + [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // Same operator A + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // Same operator A + ] + ) + + let block = createMockBlock(100, 1000) + migrateOperators(block, operatorConfig) + + // Should have only 1 unique operator + assert.entityCount("Operator", 1) + + // Should have 2 authorizations (one per SP) + assert.entityCount("OperatorAuthorization", 2) + + // Operator should have countAuthorizations = 2 + let operatorA = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + assert.fieldEquals("Operator", operatorA, "countAuthorizations", "2") + }) + + test("handles empty operator arrays gracefully", () => { + let emptyOperatorConfig = new NetworkConfig( + "test-no-operators", + testConfig.horizonStakingAddress, + testConfig.subgraphServiceAddress, + 1, + [], + [], + [], + [], // empty + [] // empty + ) + + let block = createMockBlock(100, 1000) + migrateOperators(block, emptyOperatorConfig) + + assert.entityCount("Operator", 0) + assert.entityCount("OperatorAuthorization", 0) + }) + + test("sets correct block metadata on entities", () => { + let operatorConfig = new NetworkConfig( + "test-metadata", + testConfig.horizonStakingAddress, + testConfig.subgraphServiceAddress, + 1, + [], + [], + [], + ["0x1111111111111111111111111111111111111111"], + ["0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] + ) + + let block = createMockBlock(408825706, 1700000000) + migrateOperators(block, operatorConfig) + + let operatorA = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + assert.fieldEquals("Operator", operatorA, "createdAtBlock", "408825706") + assert.fieldEquals("Operator", operatorA, "createdAt", "1700000000") + assert.fieldEquals("Operator", operatorA, "updatedAtBlock", "408825706") + assert.fieldEquals("Operator", operatorA, "updatedAt", "1700000000") + + let sp = Bytes.fromHexString("0x1111111111111111111111111111111111111111") + let verifier = Bytes.fromHexString(testConfig.subgraphServiceAddress.toHexString()) + let authId = getOperatorAuthorizationId( + Bytes.fromHexString(operatorA), + sp, + verifier + ).toHexString() + + assert.fieldEquals("OperatorAuthorization", authId, "createdAtBlock", "408825706") + assert.fieldEquals("OperatorAuthorization", authId, "createdAt", "1700000000") + }) +}) diff --git a/packages/subgraph/tests/operator.test.ts b/packages/subgraph/tests/operator.test.ts new file mode 100644 index 0000000..42f1d25 --- /dev/null +++ b/packages/subgraph/tests/operator.test.ts @@ -0,0 +1,213 @@ +import { + describe, + test, + beforeEach, + clearStore, + assert, + newTypedMockEvent, +} from "matchstick-as" +import { Address, BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" +import { OperatorSet } from "../generated/HorizonStaking/HorizonStaking" +import { handleOperatorSet } from "../src/handlers/operator" +import { getOperatorAuthorizationId } from "../src/entities/operatorAuthorization" + +// Test addresses +const OPERATOR_ADDRESS = Address.fromString("0x1111111111111111111111111111111111111111") +const OPERATOR_ADDRESS_2 = Address.fromString("0x2222222222222222222222222222222222222222") +const SP_ADDRESS = Address.fromString("0x3333333333333333333333333333333333333333") +const SP_ADDRESS_2 = Address.fromString("0x4444444444444444444444444444444444444444") +const VERIFIER_ADDRESS = Address.fromString("0x5555555555555555555555555555555555555555") +const VERIFIER_ADDRESS_2 = Address.fromString("0x6666666666666666666666666666666666666666") + +// Helper to create OperatorSet event +function createOperatorSetEvent( + serviceProvider: Address, + verifier: Address, + operator: Address, + allowed: boolean, + blockNumber: i32 = 100, + timestamp: i32 = 1000 +): OperatorSet { + let event = newTypedMockEvent() + event.parameters = new Array() + event.parameters.push(new ethereum.EventParam("serviceProvider", ethereum.Value.fromAddress(serviceProvider))) + event.parameters.push(new ethereum.EventParam("verifier", ethereum.Value.fromAddress(verifier))) + event.parameters.push(new ethereum.EventParam("operator", ethereum.Value.fromAddress(operator))) + event.parameters.push(new ethereum.EventParam("allowed", ethereum.Value.fromBoolean(allowed))) + event.block.number = BigInt.fromI32(blockNumber) + event.block.timestamp = BigInt.fromI32(timestamp) + return event +} + +function getAuthorizationIdString(operator: Address, sp: Address, verifier: Address): string { + return getOperatorAuthorizationId( + Bytes.fromHexString(operator.toHexString()), + Bytes.fromHexString(sp.toHexString()), + Bytes.fromHexString(verifier.toHexString()) + ).toHexString() +} + +describe("OperatorSet", () => { + beforeEach(() => { + clearStore() + }) + + test("creates Operator and OperatorAuthorization entities when operator is set", () => { + let event = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, true) + handleOperatorSet(event) + + let operatorId = Bytes.fromHexString(OPERATOR_ADDRESS.toHexString()).toHexString() + let authorizationId = getAuthorizationIdString(OPERATOR_ADDRESS, SP_ADDRESS, VERIFIER_ADDRESS) + + // Verify Operator entity + assert.entityCount("Operator", 1) + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "1") + assert.fieldEquals("Operator", operatorId, "createdAtBlock", "100") + assert.fieldEquals("Operator", operatorId, "createdAt", "1000") + + // Verify OperatorAuthorization entity + assert.entityCount("OperatorAuthorization", 1) + assert.fieldEquals("OperatorAuthorization", authorizationId, "operator", operatorId) + assert.fieldEquals("OperatorAuthorization", authorizationId, "serviceProvider", Bytes.fromHexString(SP_ADDRESS.toHexString()).toHexString()) + assert.fieldEquals("OperatorAuthorization", authorizationId, "dataService", Bytes.fromHexString(VERIFIER_ADDRESS.toHexString()).toHexString()) + assert.fieldEquals("OperatorAuthorization", authorizationId, "allowed", "true") + assert.fieldEquals("OperatorAuthorization", authorizationId, "createdAtBlock", "100") + assert.fieldEquals("OperatorAuthorization", authorizationId, "createdAt", "1000") + }) + + test("revokes authorization when allowed is false", () => { + // First authorize + let authorizeEvent = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, true, 100, 1000) + handleOperatorSet(authorizeEvent) + + let operatorId = Bytes.fromHexString(OPERATOR_ADDRESS.toHexString()).toHexString() + let authorizationId = getAuthorizationIdString(OPERATOR_ADDRESS, SP_ADDRESS, VERIFIER_ADDRESS) + + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "1") + assert.fieldEquals("OperatorAuthorization", authorizationId, "allowed", "true") + + // Then revoke + let revokeEvent = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, false, 200, 2000) + handleOperatorSet(revokeEvent) + + // Verify authorization is revoked + assert.fieldEquals("OperatorAuthorization", authorizationId, "allowed", "false") + assert.fieldEquals("OperatorAuthorization", authorizationId, "updatedAtBlock", "200") + assert.fieldEquals("OperatorAuthorization", authorizationId, "updatedAt", "2000") + + // Verify count decreased + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "0") + }) + + test("re-authorizing after revoke increments count", () => { + let operatorId = Bytes.fromHexString(OPERATOR_ADDRESS.toHexString()).toHexString() + + // Authorize + let event1 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, true, 100, 1000) + handleOperatorSet(event1) + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "1") + + // Revoke + let event2 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, false, 200, 2000) + handleOperatorSet(event2) + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "0") + + // Re-authorize + let event3 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, true, 300, 3000) + handleOperatorSet(event3) + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "1") + }) + + test("multiple authorizations for same operator", () => { + let operatorId = Bytes.fromHexString(OPERATOR_ADDRESS.toHexString()).toHexString() + + // Authorize for first SP + verifier + let event1 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, true, 100, 1000) + handleOperatorSet(event1) + + // Authorize for second SP + let event2 = createOperatorSetEvent(SP_ADDRESS_2, VERIFIER_ADDRESS, OPERATOR_ADDRESS, true, 101, 1010) + handleOperatorSet(event2) + + // Authorize for second verifier + let event3 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS_2, OPERATOR_ADDRESS, true, 102, 1020) + handleOperatorSet(event3) + + // Verify counts + assert.entityCount("Operator", 1) + assert.entityCount("OperatorAuthorization", 3) + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "3") + }) + + test("multiple operators authorized by same service provider", () => { + // Authorize first operator + let event1 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, true, 100, 1000) + handleOperatorSet(event1) + + // Authorize second operator + let event2 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS_2, true, 101, 1010) + handleOperatorSet(event2) + + let operatorId1 = Bytes.fromHexString(OPERATOR_ADDRESS.toHexString()).toHexString() + let operatorId2 = Bytes.fromHexString(OPERATOR_ADDRESS_2.toHexString()).toHexString() + + assert.entityCount("Operator", 2) + assert.entityCount("OperatorAuthorization", 2) + assert.fieldEquals("Operator", operatorId1, "countAuthorizations", "1") + assert.fieldEquals("Operator", operatorId2, "countAuthorizations", "1") + }) + + test("setting allowed to same value does not change count", () => { + let operatorId = Bytes.fromHexString(OPERATOR_ADDRESS.toHexString()).toHexString() + + // Authorize + let event1 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, true, 100, 1000) + handleOperatorSet(event1) + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "1") + + // Authorize again (same value) + let event2 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, true, 200, 2000) + handleOperatorSet(event2) + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "1") + }) + + test("revoking already revoked does not change count", () => { + let operatorId = Bytes.fromHexString(OPERATOR_ADDRESS.toHexString()).toHexString() + let authorizationId = getAuthorizationIdString(OPERATOR_ADDRESS, SP_ADDRESS, VERIFIER_ADDRESS) + + // Authorize + let event1 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, true, 100, 1000) + handleOperatorSet(event1) + + // Revoke + let event2 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, false, 200, 2000) + handleOperatorSet(event2) + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "0") + + // Revoke again (same value) + let event3 = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, false, 300, 3000) + handleOperatorSet(event3) + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "0") + + // Verify timestamps updated + assert.fieldEquals("OperatorAuthorization", authorizationId, "updatedAtBlock", "300") + assert.fieldEquals("OperatorAuthorization", authorizationId, "updatedAt", "3000") + }) + + test("authorization entity is created even when initially set to false", () => { + // Edge case: setting allowed=false before any authorization exists + let event = createOperatorSetEvent(SP_ADDRESS, VERIFIER_ADDRESS, OPERATOR_ADDRESS, false) + handleOperatorSet(event) + + let operatorId = Bytes.fromHexString(OPERATOR_ADDRESS.toHexString()).toHexString() + let authorizationId = getAuthorizationIdString(OPERATOR_ADDRESS, SP_ADDRESS, VERIFIER_ADDRESS) + + // Both entities should exist + assert.entityCount("Operator", 1) + assert.entityCount("OperatorAuthorization", 1) + + // Count should be 0 (was false -> false, no change) + assert.fieldEquals("Operator", operatorId, "countAuthorizations", "0") + assert.fieldEquals("OperatorAuthorization", authorizationId, "allowed", "false") + }) +}) diff --git a/packages/tools/package.json b/packages/tools/package.json index 997b5e1..612b166 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -6,11 +6,13 @@ "scripts": { "seed:indexers": "tsx src/seed/indexers.ts", "seed:delegations": "tsx src/seed/delegations.ts", + "seed:operators": "tsx src/seed/operators.ts", "validate:internal": "tsx src/validation/internal.ts", "validate:onchain:service-providers": "tsx src/validation/onchain/service-providers.ts", "validate:onchain:provisions": "tsx src/validation/onchain/provisions.ts", "validate:onchain:delegations": "tsx src/validation/onchain/delegations.ts", - "validate:onchain:fee-cuts": "tsx src/validation/onchain/fee-cuts.ts" + "validate:onchain:fee-cuts": "tsx src/validation/onchain/fee-cuts.ts", + "validate:onchain:operators": "tsx src/validation/onchain/operators.ts" }, "devDependencies": { "@types/node": "22.15.18", diff --git a/packages/tools/src/onchain.ts b/packages/tools/src/onchain.ts index 1743887..5f88ebc 100644 --- a/packages/tools/src/onchain.ts +++ b/packages/tools/src/onchain.ts @@ -6,6 +6,7 @@ const GET_SERVICE_PROVIDER_SELECTOR = "0x8cc01c86" // getServiceProvider(address const GET_PROVISION_SELECTOR = "0x25d9897e" // getProvision(address,address) const GET_DELEGATION_POOL_SELECTOR = "0x561285e4" // getDelegationPool(address,address) const GET_DELEGATION_FEE_CUT_SELECTOR = "0x7573ef4f" // getDelegationFeeCut(address,address,uint8) +const IS_AUTHORIZED_SELECTOR = "0x7c145cc7" // isAuthorized(address,address,address) const MULTICALL_SELECTOR = "0xac9650d8" // multicall(bytes[]) export interface ServiceProviderData { @@ -144,6 +145,18 @@ export async function getDelegationFeeCut( return BigInt(result) } +export async function isAuthorized( + serviceProvider: string, + verifier: string, + operator: string +): Promise { + const config = getConfig() + const callData = IS_AUTHORIZED_SELECTOR + padAddress(serviceProvider) + padAddress(verifier) + padAddress(operator) + const result = await ethCall(config.stakingAddress, callData) + // Result is a boolean encoded as uint256 (0 or 1) + return BigInt(result) === 1n +} + // ============================================================================ // Multicall // ============================================================================ diff --git a/packages/tools/src/seed/operators.ts b/packages/tools/src/seed/operators.ts new file mode 100644 index 0000000..70bed79 --- /dev/null +++ b/packages/tools/src/seed/operators.ts @@ -0,0 +1,130 @@ +/** + * Fetches all legacy operator authorizations from the legacy Graph Network subgraph. + * Outputs to packages/subgraph/src/config/{network}/operator-seed.ts + * + * Usage: NETWORK=arbitrum-one pnpm seed:operators + * + * Requires GRAPH_API_KEY in .env + */ + +import * as fs from "fs" +import * as path from "path" +import { getConfig, getLegacySubgraphUrl } from "../config" +import { querySubgraph } from "../common" + +interface Indexer { + id: string + account: { + operators: { id: string }[] + } +} + +interface OperatorPair { + serviceProvider: string + operator: string +} + +async function main() { + const config = getConfig() + const subgraphUrl = getLegacySubgraphUrl() + + console.log("=== Operator Seed Export ===") + console.log(`Network: ${config.name}`) + console.log(`Legacy Subgraph: ${config.legacySubgraphId}`) + console.log(`Block: ${config.horizonGenesisBlock}`) + console.log("") + + // Fetch all indexers with their operators at the specified block + console.log("Fetching indexers with operators...") + let allIndexers: Indexer[] = [] + let lastId = "" + + while (true) { + const whereClause = lastId + ? `where: { stakedTokens_gt: "0", id_gt: "${lastId}" }` + : `where: { stakedTokens_gt: "0" }` + + const data = await querySubgraph<{ indexers: Indexer[] }>( + subgraphUrl, + `{ + indexers(first: 1000, orderBy: id, block: { number: ${config.horizonGenesisBlock} }, ${whereClause}) { + id + account { + operators { + id + } + } + } + }` + ) + + if (data.indexers.length === 0) break + + allIndexers.push(...data.indexers) + lastId = data.indexers[data.indexers.length - 1].id + + if (data.indexers.length < 1000) break + } + + // Flatten to (serviceProvider, operator) pairs + const operatorPairs: OperatorPair[] = [] + for (const indexer of allIndexers) { + if (indexer.account && indexer.account.operators) { + for (const operator of indexer.account.operators) { + operatorPairs.push({ + serviceProvider: indexer.id, + operator: operator.id, + }) + } + } + } + + // Sort for deterministic output + operatorPairs.sort((a, b) => { + const spCompare = a.serviceProvider.localeCompare(b.serviceProvider) + if (spCompare !== 0) return spCompare + return a.operator.localeCompare(b.operator) + }) + + console.log(`Found ${allIndexers.length} indexers`) + console.log(`Found ${operatorPairs.length} operator authorizations`) + console.log("") + + // Generate output file in subgraph package + if (!fs.existsSync(config.subgraphConfigPath)) { + console.error(`Error: Subgraph config directory not found: ${config.subgraphConfigPath}`) + process.exit(1) + } + + const seedFilePath = path.join(config.subgraphConfigPath, "operator-seed.ts") + + // Create parallel arrays + const serviceProviders = operatorPairs.map((p) => p.serviceProvider) + const operators = operatorPairs.map((p) => p.operator) + + const output = `// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY +// Regenerate with: cd packages/tools && NETWORK=${config.name} pnpm seed:operators +// Generated: ${new Date().toISOString()} +// Network: ${config.name} +// Block: ${config.horizonGenesisBlock} +// Count: ${operatorPairs.length} + +// Parallel arrays: OPERATOR_SERVICE_PROVIDERS[i] authorized OPERATORS[i] +export const OPERATOR_SERVICE_PROVIDERS: string[] = [ +${serviceProviders.map((sp) => ` "${sp}",`).join("\n")} +] + +export const OPERATORS: string[] = [ +${operators.map((op) => ` "${op}",`).join("\n")} +] +` + + fs.writeFileSync(seedFilePath, output) + console.log(`Written: ${seedFilePath}`) + console.log(` ${operatorPairs.length} operator authorizations`) +} + +main().catch((err) => { + console.error("Error:", err) + process.exit(1) +}) diff --git a/packages/tools/src/validation/internal.ts b/packages/tools/src/validation/internal.ts index 4134d6a..89ebd47 100644 --- a/packages/tools/src/validation/internal.ts +++ b/packages/tools/src/validation/internal.ts @@ -85,6 +85,19 @@ interface ProvisionFeeCut { feeCut: string } +interface Operator { + id: string + countAuthorizations: number +} + +interface OperatorAuthorization { + id: string + operator: { id: string } + serviceProvider: { id: string } + dataService: { id: string } + allowed: boolean +} + // ============================================================================ // Queries // ============================================================================ @@ -168,6 +181,23 @@ const PROVISION_FEE_CUTS_QUERY = `{ } }` +const OPERATORS_QUERY = `{ + operators(first: 1000) { + id + countAuthorizations + } +}` + +const OPERATOR_AUTHORIZATIONS_QUERY = `{ + operatorAuthorizations(first: 1000) { + id + operator { id } + serviceProvider { id } + dataService { id } + allowed + } +}` + // ============================================================================ // Main // ============================================================================ @@ -180,7 +210,7 @@ async function main(): Promise { // Fetch all data console.log("=== Fetching subgraph data ===") - const [networkData, spData, dsData, provisionData, poolData, thawRequestData, feeCutData] = await Promise.all([ + const [networkData, spData, dsData, provisionData, poolData, thawRequestData, feeCutData, operatorData, authorizationData] = await Promise.all([ querySubgraph<{ graphNetwork: GraphNetwork }>(subgraphUrl, GRAPH_NETWORK_QUERY), querySubgraph<{ serviceProviders: ServiceProvider[] }>(subgraphUrl, SERVICE_PROVIDERS_QUERY), querySubgraph<{ dataServices: DataService[] }>(subgraphUrl, DATA_SERVICES_QUERY), @@ -188,6 +218,8 @@ async function main(): Promise { querySubgraph<{ delegationPools: DelegationPool[] }>(subgraphUrl, DELEGATION_POOLS_QUERY), querySubgraph<{ provisionThawRequests: ProvisionThawRequest[] }>(subgraphUrl, PROVISION_THAW_REQUESTS_QUERY), querySubgraph<{ provisionFeeCuts: ProvisionFeeCut[] }>(subgraphUrl, PROVISION_FEE_CUTS_QUERY), + querySubgraph<{ operators: Operator[] }>(subgraphUrl, OPERATORS_QUERY), + querySubgraph<{ operatorAuthorizations: OperatorAuthorization[] }>(subgraphUrl, OPERATOR_AUTHORIZATIONS_QUERY), ]) const graphNetwork = networkData.graphNetwork @@ -202,6 +234,8 @@ async function main(): Promise { const pools = poolData.delegationPools const thawRequests = thawRequestData.provisionThawRequests const feeCuts = feeCutData.provisionFeeCuts + const operators = operatorData.operators + const authorizations = authorizationData.operatorAuthorizations // Filter to only SPs with stake > 0 (matches countServiceProviders semantics) const stakedSPs = serviceProviders.filter((sp) => BigInt(sp.tokensStaked) > 0n) @@ -213,6 +247,10 @@ async function main(): Promise { const pendingThawRequests = thawRequests.filter((t) => !t.fulfilled) const fulfilledThawRequests = thawRequests.filter((t) => t.fulfilled) + // Filter authorizations by status + const allowedAuthorizations = authorizations.filter((a) => a.allowed) + const revokedAuthorizations = authorizations.filter((a) => !a.allowed) + console.log(` GraphNetwork: found`) console.log(` ServiceProviders: ${serviceProviders.length} total, ${stakedSPs.length} with stake`) console.log(` DataServices: ${dataServices.length}`) @@ -220,6 +258,8 @@ async function main(): Promise { console.log(` DelegationPools: ${pools.length} total, ${activePools.length} with tokens`) console.log(` ProvisionThawRequests: ${thawRequests.length} total, ${pendingThawRequests.length} pending, ${fulfilledThawRequests.length} fulfilled`) console.log(` ProvisionFeeCuts: ${feeCuts.length}`) + console.log(` Operators: ${operators.length}`) + console.log(` OperatorAuthorizations: ${authorizations.length} total, ${allowedAuthorizations.length} allowed, ${revokedAuthorizations.length} revoked`) console.log("") // ============================================================================ @@ -507,6 +547,76 @@ async function main(): Promise { warnings += fcWarnings + // ============================================================================ + // Operator Count Validations + // ============================================================================ + + console.log("=== Operator Count Validations ===") + let opWarnings = 0 + + for (const operator of operators) { + // Count allowed authorizations for this operator + const operatorAuthorizations = authorizations.filter( + (a) => a.operator.id === operator.id && a.allowed + ) + + if (operator.countAuthorizations !== operatorAuthorizations.length) { + opWarnings++ + console.log(`WARNING: ${operator.id}`) + console.log(` countAuthorizations: Operator=${operator.countAuthorizations}, actual=${operatorAuthorizations.length}`) + console.log("") + } + } + + if (opWarnings === 0) { + console.log("All Operator counts match!") + console.log("") + } + + warnings += opWarnings + + // ============================================================================ + // OperatorAuthorization Referential Integrity + // ============================================================================ + + console.log("=== OperatorAuthorization Referential Integrity ===") + let oaWarnings = 0 + + // Build lookup sets + const operatorIds = new Set(operators.map((o) => o.id)) + + for (const auth of authorizations) { + const issues: string[] = [] + + if (!operatorIds.has(auth.operator.id)) { + issues.push(`references non-existent Operator: ${auth.operator.id}`) + } + + if (!spIds.has(auth.serviceProvider.id)) { + issues.push(`references non-existent ServiceProvider: ${auth.serviceProvider.id}`) + } + + if (!dsIds.has(auth.dataService.id)) { + issues.push(`references non-existent DataService: ${auth.dataService.id}`) + } + + if (issues.length > 0) { + oaWarnings++ + console.log(`WARNING: ${auth.id}`) + for (const issue of issues) { + console.log(` ${issue}`) + } + console.log("") + } + } + + if (oaWarnings === 0) { + console.log("All OperatorAuthorization references are valid!") + console.log("") + } + + warnings += oaWarnings + // ============================================================================ // Summary // ============================================================================ diff --git a/packages/tools/src/validation/onchain/operators.ts b/packages/tools/src/validation/onchain/operators.ts new file mode 100644 index 0000000..52493c3 --- /dev/null +++ b/packages/tools/src/validation/onchain/operators.ts @@ -0,0 +1,89 @@ +/** + * Validates subgraph OperatorAuthorization entities against on-chain HorizonStaking.isAuthorized() + * + * Usage: NETWORK=arbitrum-one pnpm validate:onchain:operators + */ + +import { isAuthorized } from "../../onchain" +import { + querySubgraph, + getSubgraphUrlFromArgs, + printHeader, + delay, + runValidation, + printValidationSummary, + type ValidationResult, +} from "../../common" + +interface OperatorAuthorization { + id: string + operator: { id: string } + serviceProvider: { id: string } + dataService: { id: string } + allowed: boolean +} + +async function main(): Promise { + const subgraphUrl = getSubgraphUrlFromArgs() + printHeader(subgraphUrl) + + // Fetch all OperatorAuthorizations + console.log("=== Fetching OperatorAuthorizations ===") + const authData = await querySubgraph<{ operatorAuthorizations: OperatorAuthorization[] }>( + subgraphUrl, + `{ operatorAuthorizations(first: 1000) { + id + operator { id } + serviceProvider { id } + dataService { id } + allowed + } }` + ) + const authorizations = authData.operatorAuthorizations + + // Filter to only allowed authorizations (revoked ones should return false on-chain) + const allowedAuths = authorizations.filter((a) => a.allowed) + const revokedAuths = authorizations.filter((a) => !a.allowed) + + console.log(` Found ${authorizations.length} authorizations (${allowedAuths.length} allowed, ${revokedAuths.length} revoked)`) + console.log("") + + // Compare each OperatorAuthorization against on-chain + console.log("=== Comparing OperatorAuthorizations against on-chain state ===") + let mismatches = 0 + let matches = 0 + + for (const auth of authorizations) { + const onChainAllowed = await isAuthorized( + auth.serviceProvider.id, + auth.dataService.id, + auth.operator.id + ) + + if (auth.allowed !== onChainAllowed) { + mismatches++ + console.log( + `MISMATCH: operator=${auth.operator.id} sp=${auth.serviceProvider.id} ds=${auth.dataService.id}` + ) + console.log(` subgraph: allowed=${auth.allowed}`) + console.log(` on-chain: allowed=${onChainAllowed}`) + console.log("") + } else { + matches++ + } + + await delay() + } + + // Summary + const results: ValidationResult[] = [{ label: "OperatorAuthorizations", total: authorizations.length, matches, mismatches }] + printValidationSummary(results) + + if (mismatches === 0) { + console.log("All operator authorizations match on-chain state!") + } + + return mismatches > 0 ? 1 : 0 +} + +runValidation(main) From 30e831a3318632ba5d3e48138bda8053a5428237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Wed, 20 May 2026 11:48:12 -0300 Subject: [PATCH 2/3] fix: operator and delegator fees can be set without pre existing other entities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- packages/subgraph/schema.graphql | 12 ++++--- .../subgraph/src/entities/provisionFeeCut.ts | 2 ++ packages/subgraph/tests/feeCut.test.ts | 23 +++++++++++++ .../tools/src/validation/onchain/fee-cuts.ts | 32 +++++++++++-------- .../tools/src/validation/onchain/operators.ts | 16 ++++++++-- 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index f043b2d..40f0dcb 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -228,12 +228,16 @@ type Provision @entity(immutable: false) { } type ProvisionFeeCut @entity(immutable: false) { - "Composite ID: provision-paymentType" + "Composite ID: serviceProvider-dataService-paymentType" id: Bytes! # Relationships "Provision this fee cut belongs to" - provision: Provision! + provision: Provision + "Service provider" + serviceProvider: ServiceProvider + "Data service" + dataService: DataService # State "Payment type (maps to PaymentTypes enum: 0 = QueryFee, 1 = IndexingFee, 2 = IndexingReward, ...)" @@ -318,9 +322,9 @@ type OperatorAuthorization @entity(immutable: false) { "Operator that is authorized" operator: Operator! "Service provider that granted the authorization" - serviceProvider: ServiceProvider! + serviceProvider: ServiceProvider "Data service this authorization applies to" - dataService: DataService! + dataService: DataService # State "Whether the operator is currently authorized" diff --git a/packages/subgraph/src/entities/provisionFeeCut.ts b/packages/subgraph/src/entities/provisionFeeCut.ts index 81f894a..4cf251e 100644 --- a/packages/subgraph/src/entities/provisionFeeCut.ts +++ b/packages/subgraph/src/entities/provisionFeeCut.ts @@ -31,6 +31,8 @@ export function getOrCreateProvisionFeeCut( if (entity == null) { entity = new ProvisionFeeCut(id) + entity.serviceProvider = serviceProvider + entity.dataService = dataService entity.provision = getProvisionId(serviceProvider, dataService) entity.paymentType = paymentType entity.feeCut = BIGINT_ZERO diff --git a/packages/subgraph/tests/feeCut.test.ts b/packages/subgraph/tests/feeCut.test.ts index 8a513fa..e1c581f 100644 --- a/packages/subgraph/tests/feeCut.test.ts +++ b/packages/subgraph/tests/feeCut.test.ts @@ -123,12 +123,35 @@ describe("DelegationFeeCutSet", () => { assert.entityCount("ProvisionFeeCut", 1) assert.fieldEquals("ProvisionFeeCut", entityId, "provision", provisionId) + assert.fieldEquals("ProvisionFeeCut", entityId, "serviceProvider", SP_ADDRESS.toHexString()) + assert.fieldEquals("ProvisionFeeCut", entityId, "dataService", VERIFIER_ADDRESS.toHexString()) assert.fieldEquals("ProvisionFeeCut", entityId, "paymentType", PAYMENT_TYPE_QUERY_FEE.toString()) assert.fieldEquals("ProvisionFeeCut", entityId, "feeCut", feeCut.toString()) assert.fieldEquals("ProvisionFeeCut", entityId, "updatedAtBlock", "300") assert.fieldEquals("ProvisionFeeCut", entityId, "updatedAt", "3000") }) + test("creates ProvisionFeeCut without existing provision", () => { + // Set fee cut WITHOUT creating provision first + let feeCut = BigInt.fromI32(100000) // 10% in PPM + let event = createDelegationFeeCutSetEvent( + SP_ADDRESS, + VERIFIER_ADDRESS, + PAYMENT_TYPE_QUERY_FEE, + feeCut + ) + handleDelegationFeeCutSet(event) + + let entityId = getFeeCutIdString(SP_ADDRESS, VERIFIER_ADDRESS, PAYMENT_TYPE_QUERY_FEE) + + // Entity should exist with serviceProvider and dataService set + assert.entityCount("ProvisionFeeCut", 1) + assert.fieldEquals("ProvisionFeeCut", entityId, "serviceProvider", SP_ADDRESS.toHexString()) + assert.fieldEquals("ProvisionFeeCut", entityId, "dataService", VERIFIER_ADDRESS.toHexString()) + assert.fieldEquals("ProvisionFeeCut", entityId, "paymentType", PAYMENT_TYPE_QUERY_FEE.toString()) + assert.fieldEquals("ProvisionFeeCut", entityId, "feeCut", feeCut.toString()) + }) + test("creates separate entities for different payment types", () => { setupServiceProviderAndProvision() diff --git a/packages/tools/src/validation/onchain/fee-cuts.ts b/packages/tools/src/validation/onchain/fee-cuts.ts index 4a30c03..8fc206d 100644 --- a/packages/tools/src/validation/onchain/fee-cuts.ts +++ b/packages/tools/src/validation/onchain/fee-cuts.ts @@ -18,11 +18,8 @@ import { interface ProvisionFeeCut { id: string - provision: { - id: string - serviceProvider: { id: string } - dataService: { id: string } - } + serviceProvider: { id: string } | null + dataService: { id: string } | null paymentType: number feeCut: string } @@ -37,11 +34,8 @@ async function main(): Promise { subgraphUrl, `{ provisionFeeCuts(first: 1000) { id - provision { - id - serviceProvider { id } - dataService { id } - } + serviceProvider { id } + dataService { id } paymentType feeCut } }` @@ -55,11 +49,18 @@ async function main(): Promise { console.log("=== Comparing ProvisionFeeCuts against on-chain state ===") let mismatches = 0 let matches = 0 + let skipped = 0 for (const feeCut of feeCuts) { + // Skip if serviceProvider or dataService entity doesn't exist yet + if (!feeCut.serviceProvider || !feeCut.dataService) { + skipped++ + continue + } + const onChainFeeCut = await getDelegationFeeCut( - feeCut.provision.serviceProvider.id, - feeCut.provision.dataService.id, + feeCut.serviceProvider.id, + feeCut.dataService.id, feeCut.paymentType ) @@ -69,7 +70,7 @@ async function main(): Promise { if (fieldMismatches.length > 0) { mismatches++ console.log( - `MISMATCH: ${feeCut.provision.serviceProvider.id} -> ${feeCut.provision.dataService.id} (paymentType=${feeCut.paymentType})` + `MISMATCH: ${feeCut.serviceProvider.id} -> ${feeCut.dataService.id} (paymentType=${feeCut.paymentType})` ) for (const m of fieldMismatches) { console.log(m.message) @@ -82,6 +83,11 @@ async function main(): Promise { await delay() } + if (skipped > 0) { + console.log(` Skipped ${skipped} fee cuts (serviceProvider or dataService entity doesn't exist)`) + console.log("") + } + // Summary const results: ValidationResult[] = [{ label: "ProvisionFeeCuts", total: feeCuts.length, matches, mismatches }] printValidationSummary(results) diff --git a/packages/tools/src/validation/onchain/operators.ts b/packages/tools/src/validation/onchain/operators.ts index 52493c3..8e37c73 100644 --- a/packages/tools/src/validation/onchain/operators.ts +++ b/packages/tools/src/validation/onchain/operators.ts @@ -18,8 +18,8 @@ import { interface OperatorAuthorization { id: string operator: { id: string } - serviceProvider: { id: string } - dataService: { id: string } + serviceProvider: { id: string } | null + dataService: { id: string } | null allowed: boolean } @@ -53,7 +53,14 @@ async function main(): Promise { let mismatches = 0 let matches = 0 + let skipped = 0 for (const auth of authorizations) { + // Skip if serviceProvider or dataService entity doesn't exist yet + if (!auth.serviceProvider || !auth.dataService) { + skipped++ + continue + } + const onChainAllowed = await isAuthorized( auth.serviceProvider.id, auth.dataService.id, @@ -75,6 +82,11 @@ async function main(): Promise { await delay() } + if (skipped > 0) { + console.log(` Skipped ${skipped} authorizations (serviceProvider or dataService entity doesn't exist)`) + console.log("") + } + // Summary const results: ValidationResult[] = [{ label: "OperatorAuthorizations", total: authorizations.length, matches, mismatches }] printValidationSummary(results) From 3410c81044f8a5a6e10a76fb1c061c1f32aeb804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Wed, 20 May 2026 15:18:15 -0300 Subject: [PATCH 3/3] fix: validation scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- packages/tools/src/validation/internal.ts | 15 +++++++++------ packages/tools/src/validation/onchain/fee-cuts.ts | 14 ++++++++++---- .../tools/src/validation/onchain/operators.ts | 14 ++++++++++---- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/tools/src/validation/internal.ts b/packages/tools/src/validation/internal.ts index 89ebd47..cebca34 100644 --- a/packages/tools/src/validation/internal.ts +++ b/packages/tools/src/validation/internal.ts @@ -80,7 +80,7 @@ interface ProvisionThawRequest { interface ProvisionFeeCut { id: string - provision: { id: string } + provision: { id: string } | null paymentType: number feeCut: string } @@ -93,8 +93,8 @@ interface Operator { interface OperatorAuthorization { id: string operator: { id: string } - serviceProvider: { id: string } - dataService: { id: string } + serviceProvider: { id: string } | null + dataService: { id: string } | null allowed: boolean } @@ -517,7 +517,8 @@ async function main(): Promise { for (const fc of feeCuts) { const issues: string[] = [] - if (!provisionIds.has(fc.provision.id)) { + // Check provision reference (null is ok - provision may not exist yet) + if (fc.provision && !provisionIds.has(fc.provision.id)) { issues.push(`references non-existent Provision: ${fc.provision.id}`) } @@ -592,11 +593,13 @@ async function main(): Promise { issues.push(`references non-existent Operator: ${auth.operator.id}`) } - if (!spIds.has(auth.serviceProvider.id)) { + // Check serviceProvider reference (null is ok - SP entity may not exist yet) + if (auth.serviceProvider && !spIds.has(auth.serviceProvider.id)) { issues.push(`references non-existent ServiceProvider: ${auth.serviceProvider.id}`) } - if (!dsIds.has(auth.dataService.id)) { + // Check dataService reference (null is ok - DS entity may not exist yet) + if (auth.dataService && !dsIds.has(auth.dataService.id)) { issues.push(`references non-existent DataService: ${auth.dataService.id}`) } diff --git a/packages/tools/src/validation/onchain/fee-cuts.ts b/packages/tools/src/validation/onchain/fee-cuts.ts index 8fc206d..77a135e 100644 --- a/packages/tools/src/validation/onchain/fee-cuts.ts +++ b/packages/tools/src/validation/onchain/fee-cuts.ts @@ -49,12 +49,15 @@ async function main(): Promise { console.log("=== Comparing ProvisionFeeCuts against on-chain state ===") let mismatches = 0 let matches = 0 - let skipped = 0 + const skippedEntities: { id: string; reason: string }[] = [] for (const feeCut of feeCuts) { // Skip if serviceProvider or dataService entity doesn't exist yet if (!feeCut.serviceProvider || !feeCut.dataService) { - skipped++ + const missing = [] + if (!feeCut.serviceProvider) missing.push("ServiceProvider") + if (!feeCut.dataService) missing.push("DataService") + skippedEntities.push({ id: feeCut.id, reason: `${missing.join(", ")} entity doesn't exist` }) continue } @@ -83,8 +86,11 @@ async function main(): Promise { await delay() } - if (skipped > 0) { - console.log(` Skipped ${skipped} fee cuts (serviceProvider or dataService entity doesn't exist)`) + if (skippedEntities.length > 0) { + console.log(`=== Skipped ${skippedEntities.length} fee cuts ===`) + for (const skipped of skippedEntities) { + console.log(` ${skipped.id}: ${skipped.reason}`) + } console.log("") } diff --git a/packages/tools/src/validation/onchain/operators.ts b/packages/tools/src/validation/onchain/operators.ts index 8e37c73..c26c606 100644 --- a/packages/tools/src/validation/onchain/operators.ts +++ b/packages/tools/src/validation/onchain/operators.ts @@ -52,12 +52,15 @@ async function main(): Promise { console.log("=== Comparing OperatorAuthorizations against on-chain state ===") let mismatches = 0 let matches = 0 + const skippedEntities: { id: string; reason: string }[] = [] - let skipped = 0 for (const auth of authorizations) { // Skip if serviceProvider or dataService entity doesn't exist yet if (!auth.serviceProvider || !auth.dataService) { - skipped++ + const missing = [] + if (!auth.serviceProvider) missing.push("ServiceProvider") + if (!auth.dataService) missing.push("DataService") + skippedEntities.push({ id: auth.id, reason: `${missing.join(", ")} entity doesn't exist` }) continue } @@ -82,8 +85,11 @@ async function main(): Promise { await delay() } - if (skipped > 0) { - console.log(` Skipped ${skipped} authorizations (serviceProvider or dataService entity doesn't exist)`) + if (skippedEntities.length > 0) { + console.log(`=== Skipped ${skippedEntities.length} authorizations ===`) + for (const skipped of skippedEntities) { + console.log(` ${skipped.id}: ${skipped.reason}`) + } console.log("") }