From a92e773f4afd7a0a0d1c41164c92bb15db7a014b Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Tue, 21 Apr 2026 10:44:21 +0200 Subject: [PATCH] feat: fwss dataset/piece index enrichment --- subgraph/abis/FilecoinWarmStorageService.json | 2389 +++++++++++++++++ subgraph/schema.graphql | 30 + subgraph/src/fwss.ts | 182 ++ subgraph/src/pdp-verifier.ts | 22 +- subgraph/subgraph.yaml | 29 + subgraph/subgraph_mainnet.yaml | 29 + subgraph/subgraph_testnet.yaml | 29 + subgraph/tests/fwss-utils.ts | 246 ++ subgraph/tests/fwss.test.ts | 455 ++++ 9 files changed, 3409 insertions(+), 2 deletions(-) create mode 100644 subgraph/abis/FilecoinWarmStorageService.json create mode 100644 subgraph/src/fwss.ts create mode 100644 subgraph/tests/fwss-utils.ts create mode 100644 subgraph/tests/fwss.test.ts diff --git a/subgraph/abis/FilecoinWarmStorageService.json b/subgraph/abis/FilecoinWarmStorageService.json new file mode 100644 index 0000000..cedddc4 --- /dev/null +++ b/subgraph/abis/FilecoinWarmStorageService.json @@ -0,0 +1,2389 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_pdpVerifierAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "_paymentsContractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "_usdfc", + "type": "address", + "internalType": "contract IERC20Metadata" + }, + { + "name": "_filBeamBeneficiaryAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "_serviceProviderRegistry", + "type": "address", + "internalType": "contract ServiceProviderRegistry" + }, + { + "name": "_sessionKeyRegistry", + "type": "address", + "internalType": "contract SessionKeyRegistry" + }, + { + "name": "_reinitializer_version", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "addApprovedProvider", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "announcePlannedUpgrade", + "inputs": [ + { + "name": "plannedUpgrade", + "type": "tuple", + "internalType": "struct FilecoinWarmStorageService.PlannedUpgrade", + "components": [ + { + "name": "nextImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "afterEpoch", + "type": "uint96", + "internalType": "uint96" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "calculateRatePerEpoch", + "inputs": [ + { + "name": "totalBytes", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "storageRate", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "configureProvingPeriod", + "inputs": [ + { + "name": "_maxProvingPeriod", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "_challengeWindowSize", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "dataSetCreated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "serviceProvider", + "type": "address", + "internalType": "address" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "dataSetDeleted", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "eip712Domain", + "inputs": [], + "outputs": [ + { + "name": "fields", + "type": "bytes1", + "internalType": "bytes1" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "version", + "type": "string", + "internalType": "string" + }, + { + "name": "chainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "verifyingContract", + "type": "address", + "internalType": "address" + }, + { + "name": "salt", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "extensions", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "extsload", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "extsloadStruct", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "size", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "filBeamBeneficiaryAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getEffectiveRates", + "inputs": [], + "outputs": [ + { + "name": "serviceFee", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "spPayment", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getProvingPeriodForEpoch", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "epoch", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getServicePrice", + "inputs": [], + "outputs": [ + { + "name": "pricing", + "type": "tuple", + "internalType": "struct FilecoinWarmStorageService.ServicePricing", + "components": [ + { + "name": "pricePerTiBPerMonthNoCDN", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pricePerTiBCdnEgress", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pricePerTiBCacheMissEgress", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "tokenAddress", + "type": "address", + "internalType": "contract IERC20" + }, + { + "name": "epochsPerMonth", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumPricePerMonth", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "_maxProvingPeriod", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "_challengeWindowSize", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_filBeamControllerAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "_name", + "type": "string", + "internalType": "string" + }, + { + "name": "_description", + "type": "string", + "internalType": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "migrate", + "inputs": [ + { + "name": "_viewContract", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "nextProvingPeriod", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "challengeEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "leafCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "paymentsContractAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pdpVerifierAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "piecesAdded", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "firstAdded", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceData", + "type": "tuple[]", + "internalType": "struct Cids.Cid[]", + "components": [ + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "piecesScheduledRemove", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceIds", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "possessionProven", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "challengeCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "railTerminated", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "terminator", + "type": "address", + "internalType": "address" + }, + { + "name": "endEpoch", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeApprovedProvider", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "serviceProviderRegistry", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ServiceProviderRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "sessionKeyRegistry", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract SessionKeyRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setViewContract", + "inputs": [ + { + "name": "_viewContract", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "settleFilBeamPaymentRails", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "cdnAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "cacheMissAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "storageProviderChanged", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "terminateCDNService", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "terminateService", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "topUpCDNPaymentRails", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "cdnAmountToAdd", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "cacheMissAmountToAdd", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFilBeamController", + "inputs": [ + { + "name": "newController", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updatePricing", + "inputs": [ + { + "name": "newStoragePrice", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "newMinimumRate", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateServiceCommission", + "inputs": [ + { + "name": "newCommissionBps", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "usdfcTokenAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IERC20Metadata" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "validatePayment", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "proposedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "fromEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "toEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "result", + "type": "tuple", + "internalType": "struct IValidator.ValidationResult", + "components": [ + { + "name": "modifiedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "settleUpto", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "note", + "type": "string", + "internalType": "string" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "viewContractAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "CDNPaymentRailsToppedUp", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "cdnAmountAdded", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "totalCdnLockup", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cacheMissAmountAdded", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "totalCacheMissLockup", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "CDNPaymentTerminated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "endEpoch", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cacheMissRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cdnRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "CDNServiceTerminated", + "inputs": [ + { + "name": "caller", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "cacheMissRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cdnRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ContractUpgraded", + "inputs": [ + { + "name": "version", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "implementation", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DataSetCreated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "providerId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "pdpRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cacheMissRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cdnRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "payer", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "serviceProvider", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "payee", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "metadataKeys", + "type": "string[]", + "indexed": false, + "internalType": "string[]" + }, + { + "name": "metadataValues", + "type": "string[]", + "indexed": false, + "internalType": "string[]" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DataSetServiceProviderChanged", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "oldServiceProvider", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newServiceProvider", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "EIP712DomainChanged", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "FaultRecord", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "periodsFaulted", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FilBeamControllerChanged", + "inputs": [ + { + "name": "oldController", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "newController", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FilecoinServiceDeployed", + "inputs": [ + { + "name": "name", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "description", + "type": "string", + "indexed": false, + "internalType": "string" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PDPPaymentTerminated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "endEpoch", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "pdpRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PieceAdded", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "pieceId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "pieceCid", + "type": "tuple", + "indexed": false, + "internalType": "struct Cids.Cid", + "components": [ + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "keys", + "type": "string[]", + "indexed": false, + "internalType": "string[]" + }, + { + "name": "values", + "type": "string[]", + "indexed": false, + "internalType": "string[]" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PricingUpdated", + "inputs": [ + { + "name": "storagePrice", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "minimumRate", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ProviderApproved", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ProviderUnapproved", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RailRateUpdated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "railId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newRate", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ServiceTerminated", + "inputs": [ + { + "name": "caller", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "pdpRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cacheMissRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cdnRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "UpgradeAnnounced", + "inputs": [ + { + "name": "plannedUpgrade", + "type": "tuple", + "indexed": false, + "internalType": "struct FilecoinWarmStorageService.PlannedUpgrade", + "components": [ + { + "name": "nextImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "afterEpoch", + "type": "uint96", + "internalType": "uint96" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ViewContractSet", + "inputs": [ + { + "name": "viewContract", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AtLeastOnePriceMustBeNonZero", + "inputs": [] + }, + { + "type": "error", + "name": "CDNPaymentAlreadyTerminated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "CacheMissPaymentAlreadyTerminated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "CallerNotPayer", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "expectedPayer", + "type": "address", + "internalType": "address" + }, + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "CallerNotPayerOrPayee", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "expectedPayer", + "type": "address", + "internalType": "address" + }, + { + "name": "expectedPayee", + "type": "address", + "internalType": "address" + }, + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "CallerNotPayments", + "inputs": [ + { + "name": "expected", + "type": "address", + "internalType": "address" + }, + { + "name": "actual", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ChallengeWindowTooEarly", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "windowStart", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "nowBlock", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ClientDataSetAlreadyRegistered", + "inputs": [ + { + "name": "clientDataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "CommissionExceedsMaximum", + "inputs": [ + { + "name": "commissionType", + "type": "uint8", + "internalType": "enum Errors.CommissionType" + }, + { + "name": "max", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "DataSetNotFoundForRail", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "DataSetNotRegistered", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "DataSetPaymentAlreadyTerminated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "DataSetPaymentBeyondEndEpoch", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pdpEndEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "currentBlock", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "DivisionByZero", + "inputs": [] + }, + { + "type": "error", + "name": "DuplicateMetadataKey", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "key", + "type": "string", + "internalType": "string" + } + ] + }, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [ + { + "name": "implementation", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967NonPayable", + "inputs": [] + }, + { + "type": "error", + "name": "ExtraDataRequired", + "inputs": [] + }, + { + "type": "error", + "name": "ExtraDataTooLarge", + "inputs": [ + { + "name": "actualSize", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxAllowedSize", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "FailedCall", + "inputs": [] + }, + { + "type": "error", + "name": "FilBeamServiceNotConfigured", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientLockupAllowance", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "lockupAllowance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "lockupUsage", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumLockupRequired", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientLockupFunds", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "minimumRequired", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientMaxLockupPeriod", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "maxLockupPeriod", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requiredLockupPeriod", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientRateAllowance", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "rateAllowance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rateUsage", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumRateRequired", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidChallengeCount", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minExpected", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidChallengeEpoch", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidChallengeWindowSize", + "inputs": [ + { + "name": "maxProvingPeriod", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "challengeWindowSize", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidDataSetId", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidEpochRange", + "inputs": [ + { + "name": "fromEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "toEpoch", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidServiceDescriptionLength", + "inputs": [ + { + "name": "length", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidServiceNameLength", + "inputs": [ + { + "name": "length", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidTopUpAmount", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MaxProvingPeriodZero", + "inputs": [] + }, + { + "type": "error", + "name": "MetadataArrayCountMismatch", + "inputs": [ + { + "name": "metadataArrayCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceCount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MetadataKeyAndValueLengthMismatch", + "inputs": [ + { + "name": "keysLength", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "valuesLength", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MetadataKeyExceedsMaxLength", + "inputs": [ + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "length", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MetadataValueExceedsMaxLength", + "inputs": [ + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "length", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "NextProvingPeriodAlreadyCalled", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "periodDeadline", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "nowBlock", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "NoPDPPaymentRail", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "OnlyFilBeamControllerAllowed", + "inputs": [ + { + "name": "expected", + "type": "address", + "internalType": "address" + }, + { + "name": "actual", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OnlyPDPVerifierAllowed", + "inputs": [ + { + "name": "expected", + "type": "address", + "internalType": "address" + }, + { + "name": "actual", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OperatorNotApproved", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "PaymentRailsNotFinalized", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pdpEndEpoch", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "PriceExceedsMaximum", + "inputs": [ + { + "name": "priceType", + "type": "uint8", + "internalType": "enum Errors.PriceType" + }, + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProofAlreadySubmitted", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProviderAlreadyApproved", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProviderIdMismatchAtIndex", + "inputs": [ + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "providerId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProviderNotInApprovedList", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProviderNotRegistered", + "inputs": [ + { + "name": "provider", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ProvingNotStarted", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProvingPeriodPassed", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "nowBlock", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "RailNotAssociated", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "RailNotFullySettled", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "settledUpTo", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "endEpoch", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ServiceContractMustTerminateRail", + "inputs": [] + }, + { + "type": "error", + "name": "StorageProviderChangesNotSupported", + "inputs": [] + }, + { + "type": "error", + "name": "TooManyMetadataKeys", + "inputs": [ + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "keysLength", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "ZeroAddress", + "inputs": [ + { + "name": "field", + "type": "uint8", + "internalType": "enum Errors.AddressField" + } + ] + } +] \ No newline at end of file diff --git a/subgraph/schema.graphql b/subgraph/schema.graphql index f78cc9b..d842b80 100644 --- a/subgraph/schema.graphql +++ b/subgraph/schema.graphql @@ -12,6 +12,9 @@ type DataSet @entity(immutable: false) { owner: Provider! # address of the provider leafCount: BigInt! # uint256 challengeRange: BigInt! # uint256 + # True iff the dataset has not been deleted (PDPVerifier.DataSetDeleted) + # and the FWSS service has not been terminated (FWSS.ServiceTerminated). + # Note: PDPPaymentTerminated does NOT affect this flag; use pdpPaymentEndEpoch. isActive: Boolean! status: DataSetStatus! lastProvenEpoch: BigInt! # uint256 @@ -38,6 +41,27 @@ type DataSet @entity(immutable: false) { updatedAt: BigInt! blockNumber: BigInt! + # ---- FWSS fields (null / empty for non-FWSS datasets) ---- + # Populated by FWSS.DataSetCreated handler. + fwssProviderId: BigInt # uint256 — FWSS numeric provider ID + fwssPayer: Bytes # address of the payer + fwssServiceProvider: Bytes # address — may diverge from owner after transfers + fwssPdpRailId: BigInt # uint256 — FWSS PDP rail ID + + # Raw metadata from DataSetCreated (kept for completeness; clients should + # prefer the derived booleans below for filter queries). + metadataKeys: [String!]! # empty array when not FWSS + metadataValues: [String!]! # empty array when not FWSS + + # Derived metadata flags (cheap to filter on). + withIPFSIndexing: Boolean! # true iff "withIPFSIndexing" in metadataKeys + withCDN: Boolean! # true iff "withCDN" in metadataKeys + + # Populated by FWSS.PDPPaymentTerminated handler. + # May be in the past (already terminated) or future (terminating). + # Does NOT flip isActive — clients that care must compare to current epoch. + pdpPaymentEndEpoch: BigInt + # Derived relationships roots: [Root!]! @derivedFrom(field: "proofSet") transactions: [Transaction!]! @derivedFrom(field: "proofSet") @@ -113,6 +137,12 @@ type Root @entity(immutable: false) { updatedAt: BigInt! blockNumber: BigInt! + # ---- FWSS fields (null / empty for non-FWSS pieces) ---- + # Populated by FWSS.PieceAdded handler. + metadataKeys: [String!]! # empty array when not FWSS + metadataValues: [String!]! # empty array when not FWSS + ipfsRootCID: String # values[indexOf(keys, "ipfsRootCID")] or null + # Relationship proofSet: DataSet! # Link to DataSet (stores DataSet ID) # Derived relationships diff --git a/subgraph/src/fwss.ts b/subgraph/src/fwss.ts new file mode 100644 index 0000000..983a151 --- /dev/null +++ b/subgraph/src/fwss.ts @@ -0,0 +1,182 @@ +import { BigInt, Bytes, log } from "@graphprotocol/graph-ts"; +import { + DataSetCreated as DataSetCreatedEvent, + PieceAdded as PieceAddedEvent, + ServiceTerminated as ServiceTerminatedEvent, + PDPPaymentTerminated as PDPPaymentTerminatedEvent, + DataSetServiceProviderChanged as DataSetServiceProviderChangedEvent, +} from "../generated/FilecoinWarmStorageService/FilecoinWarmStorageService"; +import { DataSet, Root } from "../generated/schema"; +import { getRootEntityId } from "./pdp-verifier"; +import { saveNetworkMetrics } from "./helper"; + +// ---- Helpers -------------------------------------------------------------- + +function getProofSetEntityId(setId: BigInt): Bytes { + return Bytes.fromByteArray(Bytes.fromBigInt(setId)); +} + +function arrayContains(arr: string[], needle: string): boolean { + for (let i = 0; i < arr.length; i++) { + if (arr[i] == needle) return true; + } + return false; +} + +function extractMetadataValue( + keys: string[], + values: string[], + needle: string +): string | null { + for (let i = 0; i < keys.length; i++) { + if (keys[i] == needle) { + return i < values.length ? values[i] : null; + } + } + return null; +} + +// ---- Handlers ------------------------------------------------------------- + +export function handleFwssDataSetCreated(event: DataSetCreatedEvent): void { + const id = getProofSetEntityId(event.params.dataSetId); + // FWSS.DataSetCreated fires BEFORE PDPVerifier's own DataSetCreated event + // (see PDPVerifier._createDataSet: listener callback runs first, THEN + // `emit DataSetCreated`). If no entity exists yet, create a stub with + // required defaults; pdp-verifier.handleDataSetCreated will run later in + // the same block and fill in PDPVerifier-level fields. Since handlers run + // sequentially and atomically within a block, no GraphQL query can observe + // that intermediate state. + let ds = DataSet.load(id); + if (ds == null) { + ds = new DataSet(id); + ds.setId = event.params.dataSetId; + // PDPVerifier-level non-null fields — safe defaults; handleDataSetCreated + // will overwrite shortly after in this same block. + ds.owner = event.params.serviceProvider; + ds.listener = event.address; + ds.isActive = true; + ds.leafCount = BigInt.fromI32(0); + ds.challengeRange = BigInt.fromI32(0); + ds.lastProvenEpoch = BigInt.fromI32(0); + ds.nextChallengeEpoch = BigInt.fromI32(0); + ds.firstDeadline = BigInt.fromI32(0); + ds.maxProvingPeriod = BigInt.fromI32(0); + ds.challengeWindowSize = BigInt.fromI32(0); + ds.currentDeadlineCount = BigInt.fromI32(0); + ds.nextDeadline = BigInt.fromI32(0); + ds.provenThisPeriod = false; + ds.totalRoots = BigInt.fromI32(0); + ds.nextPieceId = BigInt.fromI32(0); + ds.totalDataSize = BigInt.fromI32(0); + ds.totalFeePaid = BigInt.fromI32(0); + ds.totalFaultedPeriods = BigInt.fromI32(0); + ds.totalFaultedRoots = BigInt.fromI32(0); + ds.totalProofs = BigInt.fromI32(0); + ds.totalProvedRoots = BigInt.fromI32(0); + ds.totalTransactions = BigInt.fromI32(0); + ds.totalEventLogs = BigInt.fromI32(0); + ds.createdAt = event.block.timestamp; + ds.blockNumber = event.block.number; + // status: EMPTY. Imported enum value would be cleaner, but schema.graphql + // defines the enum; matching literal is what the generated code stores. + ds.status = "EMPTY"; + } + + ds.fwssProviderId = event.params.providerId; + ds.fwssPayer = event.params.payer; + ds.fwssServiceProvider = event.params.serviceProvider; + ds.fwssPdpRailId = event.params.pdpRailId; + ds.metadataKeys = event.params.metadataKeys; + ds.metadataValues = event.params.metadataValues; + ds.withIPFSIndexing = arrayContains( + event.params.metadataKeys, + "withIPFSIndexing" + ); + ds.withCDN = arrayContains(event.params.metadataKeys, "withCDN"); + ds.updatedAt = event.block.timestamp; + ds.save(); +} + +export function handleFwssPieceAdded(event: PieceAddedEvent): void { + const id = getRootEntityId(event.params.dataSetId, event.params.pieceId); + const root = Root.load(id); + if (root == null) { + log.warning("FWSS PieceAdded for unknown root {}-{}", [ + event.params.dataSetId.toString(), + event.params.pieceId.toString(), + ]); + return; + } + + root.metadataKeys = event.params.keys; + root.metadataValues = event.params.values; + root.ipfsRootCID = extractMetadataValue( + event.params.keys, + event.params.values, + "ipfsRootCID" + ); + root.updatedAt = event.block.timestamp; + root.save(); +} + +export function handleFwssServiceTerminated( + event: ServiceTerminatedEvent +): void { + const id = getProofSetEntityId(event.params.dataSetId); + const ds = DataSet.load(id); + if (ds == null) { + log.warning("FWSS ServiceTerminated for unknown dataSet {}", [ + event.params.dataSetId.toString(), + ]); + return; + } + + // Guard against double-decrement of totalActiveProofSets in case both + // DataSetDeleted (PDPVerifier) and ServiceTerminated (FWSS) fire for the + // same dataset. Only decrement if this event is the one flipping isActive. + if (ds.isActive) { + saveNetworkMetrics( + ["totalActiveProofSets"], + [BigInt.fromI32(1)], + ["subtract"] + ); + } + ds.isActive = false; + ds.updatedAt = event.block.timestamp; + ds.save(); +} + +export function handleFwssPdpPaymentTerminated( + event: PDPPaymentTerminatedEvent +): void { + const id = getProofSetEntityId(event.params.dataSetId); + const ds = DataSet.load(id); + if (ds == null) { + log.warning("FWSS PDPPaymentTerminated for unknown dataSet {}", [ + event.params.dataSetId.toString(), + ]); + return; + } + + ds.pdpPaymentEndEpoch = event.params.endEpoch; + ds.updatedAt = event.block.timestamp; + ds.save(); +} + +export function handleFwssDataSetServiceProviderChanged( + event: DataSetServiceProviderChangedEvent +): void { + const id = getProofSetEntityId(event.params.dataSetId); + const ds = DataSet.load(id); + if (ds == null) { + log.warning("FWSS DataSetServiceProviderChanged for unknown dataSet {}", [ + event.params.dataSetId.toString(), + ]); + return; + } + + ds.fwssServiceProvider = event.params.newServiceProvider; + ds.updatedAt = event.block.timestamp; + ds.save(); +} diff --git a/subgraph/src/pdp-verifier.ts b/subgraph/src/pdp-verifier.ts index a8d659d..2d687c3 100644 --- a/subgraph/src/pdp-verifier.ts +++ b/subgraph/src/pdp-verifier.ts @@ -103,8 +103,22 @@ export function handleDataSetCreated(event: DataSetCreatedEvent): void { transaction.save(); } - // Create DataSet - let proofSet = new DataSet(proofSetEntityId); + // Create or load DataSet. FWSS.dataSetCreated callback fires before + // PDPVerifier's own DataSetCreated event (see PDPVerifier._createDataSet: + // listener callback runs, THEN `emit DataSetCreated`). If the listener is + // FWSS, handleFwssDataSetCreated has already created a stub entity with + // FWSS-layer fields populated. Load to preserve those fields. + let proofSet = DataSet.load(proofSetEntityId); + if (proofSet == null) { + proofSet = new DataSet(proofSetEntityId); + // FWSS fields — defaulted on create; FWSS handler will overwrite if it fires later. + proofSet.metadataKeys = []; + proofSet.metadataValues = []; + proofSet.withIPFSIndexing = false; + proofSet.withCDN = false; + // fwssProviderId, fwssPayer, fwssServiceProvider, fwssPdpRailId, + // pdpPaymentEndEpoch are nullable — no init needed. + } proofSet.setId = event.params.setId; proofSet.owner = providerEntityId; // Link to Provider via owner address (which is Provider's ID) proofSet.listener = listenerAddr; @@ -1189,6 +1203,10 @@ export function handlePiecesAdded(event: PiecesAddedEvent): void { root.updatedAt = event.block.timestamp; root.blockNumber = event.block.number; root.proofSet = proofSetEntityId; // Link to DataSet + // FWSS fields — defaulted here; patched in FWSS handler if applicable. + root.metadataKeys = []; + root.metadataValues = []; + // ipfsRootCID is nullable — no init needed. root.save(); diff --git a/subgraph/subgraph.yaml b/subgraph/subgraph.yaml index b06def9..ff589ea 100644 --- a/subgraph/subgraph.yaml +++ b/subgraph/subgraph.yaml @@ -50,3 +50,32 @@ dataSources: - event: NextProvingPeriod(indexed uint256,uint256,uint256) handler: handleNextProvingPeriod file: ./src/pdp-verifier.ts + - kind: ethereum + name: FilecoinWarmStorageService + network: filecoin + source: + address: "0x8408502033C418E1bbC97cE9ac48E5528F371A9f" + abi: FilecoinWarmStorageService + startBlock: 5459617 + mapping: + kind: ethereum/events + apiVersion: 0.0.9 + language: wasm/assemblyscript + entities: + - DataSet + - Root + abis: + - name: FilecoinWarmStorageService + file: ./abis/FilecoinWarmStorageService.json + eventHandlers: + - event: DataSetCreated(indexed uint256,indexed uint256,uint256,uint256,uint256,address,address,address,string[],string[]) + handler: handleFwssDataSetCreated + - event: PieceAdded(indexed uint256,indexed uint256,(bytes),string[],string[]) + handler: handleFwssPieceAdded + - event: ServiceTerminated(indexed address,indexed uint256,uint256,uint256,uint256) + handler: handleFwssServiceTerminated + - event: PDPPaymentTerminated(indexed uint256,uint256,uint256) + handler: handleFwssPdpPaymentTerminated + - event: DataSetServiceProviderChanged(indexed uint256,indexed address,indexed address) + handler: handleFwssDataSetServiceProviderChanged + file: ./src/fwss.ts diff --git a/subgraph/subgraph_mainnet.yaml b/subgraph/subgraph_mainnet.yaml index b06def9..ff589ea 100644 --- a/subgraph/subgraph_mainnet.yaml +++ b/subgraph/subgraph_mainnet.yaml @@ -50,3 +50,32 @@ dataSources: - event: NextProvingPeriod(indexed uint256,uint256,uint256) handler: handleNextProvingPeriod file: ./src/pdp-verifier.ts + - kind: ethereum + name: FilecoinWarmStorageService + network: filecoin + source: + address: "0x8408502033C418E1bbC97cE9ac48E5528F371A9f" + abi: FilecoinWarmStorageService + startBlock: 5459617 + mapping: + kind: ethereum/events + apiVersion: 0.0.9 + language: wasm/assemblyscript + entities: + - DataSet + - Root + abis: + - name: FilecoinWarmStorageService + file: ./abis/FilecoinWarmStorageService.json + eventHandlers: + - event: DataSetCreated(indexed uint256,indexed uint256,uint256,uint256,uint256,address,address,address,string[],string[]) + handler: handleFwssDataSetCreated + - event: PieceAdded(indexed uint256,indexed uint256,(bytes),string[],string[]) + handler: handleFwssPieceAdded + - event: ServiceTerminated(indexed address,indexed uint256,uint256,uint256,uint256) + handler: handleFwssServiceTerminated + - event: PDPPaymentTerminated(indexed uint256,uint256,uint256) + handler: handleFwssPdpPaymentTerminated + - event: DataSetServiceProviderChanged(indexed uint256,indexed address,indexed address) + handler: handleFwssDataSetServiceProviderChanged + file: ./src/fwss.ts diff --git a/subgraph/subgraph_testnet.yaml b/subgraph/subgraph_testnet.yaml index 3aeb04a..9c42329 100644 --- a/subgraph/subgraph_testnet.yaml +++ b/subgraph/subgraph_testnet.yaml @@ -50,3 +50,32 @@ dataSources: - event: NextProvingPeriod(indexed uint256,uint256,uint256) handler: handleNextProvingPeriod file: ./src/pdp-verifier.ts + - kind: ethereum + name: FilecoinWarmStorageService + network: filecoin-testnet + source: + address: "0x02925630df557F957f70E112bA06e50965417CA0" + abi: FilecoinWarmStorageService + startBlock: 3141276 + mapping: + kind: ethereum/events + apiVersion: 0.0.9 + language: wasm/assemblyscript + entities: + - DataSet + - Root + abis: + - name: FilecoinWarmStorageService + file: ./abis/FilecoinWarmStorageService.json + eventHandlers: + - event: DataSetCreated(indexed uint256,indexed uint256,uint256,uint256,uint256,address,address,address,string[],string[]) + handler: handleFwssDataSetCreated + - event: PieceAdded(indexed uint256,indexed uint256,(bytes),string[],string[]) + handler: handleFwssPieceAdded + - event: ServiceTerminated(indexed address,indexed uint256,uint256,uint256,uint256) + handler: handleFwssServiceTerminated + - event: PDPPaymentTerminated(indexed uint256,uint256,uint256) + handler: handleFwssPdpPaymentTerminated + - event: DataSetServiceProviderChanged(indexed uint256,indexed address,indexed address) + handler: handleFwssDataSetServiceProviderChanged + file: ./src/fwss.ts diff --git a/subgraph/tests/fwss-utils.ts b/subgraph/tests/fwss-utils.ts new file mode 100644 index 0000000..e5db313 --- /dev/null +++ b/subgraph/tests/fwss-utils.ts @@ -0,0 +1,246 @@ +import { newMockEvent } from "matchstick-as"; +import { ethereum, BigInt, Address, Bytes } from "@graphprotocol/graph-ts"; +import { + DataSetCreated as FwssDataSetCreated, + PieceAdded as FwssPieceAdded, + ServiceTerminated as FwssServiceTerminated, + PDPPaymentTerminated as FwssPdpPaymentTerminated, + DataSetServiceProviderChanged as FwssDataSetServiceProviderChanged, +} from "../generated/FilecoinWarmStorageService/FilecoinWarmStorageService"; + +// FWSS DataSetCreated: +// dataSetId, providerId, pdpRailId, cacheMissRailId, cdnRailId, +// payer, serviceProvider, payee, metadataKeys[], metadataValues[] +export function createFwssDataSetCreatedEvent( + dataSetId: BigInt, + providerId: BigInt, + pdpRailId: BigInt, + payer: Address, + serviceProvider: Address, + metadataKeys: string[], + metadataValues: string[], + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1) +): FwssDataSetCreated { + let ev = changetype(newMockEvent()); + ev.parameters = new Array(); + + ev.parameters.push( + new ethereum.EventParam( + "dataSetId", + ethereum.Value.fromUnsignedBigInt(dataSetId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "providerId", + ethereum.Value.fromUnsignedBigInt(providerId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "pdpRailId", + ethereum.Value.fromUnsignedBigInt(pdpRailId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "cacheMissRailId", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(0)) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "cdnRailId", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(0)) + ) + ); + ev.parameters.push( + new ethereum.EventParam("payer", ethereum.Value.fromAddress(payer)) + ); + ev.parameters.push( + new ethereum.EventParam( + "serviceProvider", + ethereum.Value.fromAddress(serviceProvider) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "payee", + ethereum.Value.fromAddress(serviceProvider) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "metadataKeys", + ethereum.Value.fromStringArray(metadataKeys) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "metadataValues", + ethereum.Value.fromStringArray(metadataValues) + ) + ); + + ev.block.number = blockNumber; + ev.block.timestamp = timestamp; + return ev; +} + +// FWSS PieceAdded: dataSetId, pieceId, Cids.Cid (tuple(bytes)), keys[], values[] +export function createFwssPieceAddedEvent( + dataSetId: BigInt, + pieceId: BigInt, + pieceCidBytes: Bytes, + keys: string[], + values: string[], + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1) +): FwssPieceAdded { + let ev = changetype(newMockEvent()); + ev.parameters = new Array(); + + ev.parameters.push( + new ethereum.EventParam( + "dataSetId", + ethereum.Value.fromUnsignedBigInt(dataSetId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "pieceId", + ethereum.Value.fromUnsignedBigInt(pieceId) + ) + ); + + let cidTuple = new ethereum.Tuple(); + cidTuple.push(ethereum.Value.fromBytes(pieceCidBytes)); + ev.parameters.push( + new ethereum.EventParam("pieceCid", ethereum.Value.fromTuple(cidTuple)) + ); + + ev.parameters.push( + new ethereum.EventParam("keys", ethereum.Value.fromStringArray(keys)) + ); + ev.parameters.push( + new ethereum.EventParam("values", ethereum.Value.fromStringArray(values)) + ); + + ev.block.number = blockNumber; + ev.block.timestamp = timestamp; + return ev; +} + +// FWSS ServiceTerminated: caller, dataSetId, pdpRailId, cacheMissRailId, cdnRailId +export function createFwssServiceTerminatedEvent( + dataSetId: BigInt, + caller: Address, + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1) +): FwssServiceTerminated { + let ev = changetype(newMockEvent()); + ev.parameters = new Array(); + + ev.parameters.push( + new ethereum.EventParam("caller", ethereum.Value.fromAddress(caller)) + ); + ev.parameters.push( + new ethereum.EventParam( + "dataSetId", + ethereum.Value.fromUnsignedBigInt(dataSetId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "pdpRailId", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(0)) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "cacheMissRailId", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(0)) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "cdnRailId", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(0)) + ) + ); + + ev.block.number = blockNumber; + ev.block.timestamp = timestamp; + return ev; +} + +// FWSS PDPPaymentTerminated: dataSetId, endEpoch, pdpRailId +export function createFwssPdpPaymentTerminatedEvent( + dataSetId: BigInt, + endEpoch: BigInt, + pdpRailId: BigInt, + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1) +): FwssPdpPaymentTerminated { + let ev = changetype(newMockEvent()); + ev.parameters = new Array(); + + ev.parameters.push( + new ethereum.EventParam( + "dataSetId", + ethereum.Value.fromUnsignedBigInt(dataSetId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "endEpoch", + ethereum.Value.fromUnsignedBigInt(endEpoch) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "pdpRailId", + ethereum.Value.fromUnsignedBigInt(pdpRailId) + ) + ); + + ev.block.number = blockNumber; + ev.block.timestamp = timestamp; + return ev; +} + +// FWSS DataSetServiceProviderChanged: dataSetId, oldServiceProvider, newServiceProvider +export function createFwssDataSetServiceProviderChangedEvent( + dataSetId: BigInt, + oldServiceProvider: Address, + newServiceProvider: Address, + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1) +): FwssDataSetServiceProviderChanged { + let ev = changetype(newMockEvent()); + ev.parameters = new Array(); + + ev.parameters.push( + new ethereum.EventParam( + "dataSetId", + ethereum.Value.fromUnsignedBigInt(dataSetId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "oldServiceProvider", + ethereum.Value.fromAddress(oldServiceProvider) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "newServiceProvider", + ethereum.Value.fromAddress(newServiceProvider) + ) + ); + + ev.block.number = blockNumber; + ev.block.timestamp = timestamp; + return ev; +} diff --git a/subgraph/tests/fwss.test.ts b/subgraph/tests/fwss.test.ts new file mode 100644 index 0000000..6a8433a --- /dev/null +++ b/subgraph/tests/fwss.test.ts @@ -0,0 +1,455 @@ +import { + assert, + describe, + test, + clearStore, + beforeEach, +} from "matchstick-as/assembly/index"; +import { BigInt, Address, Bytes } from "@graphprotocol/graph-ts"; +import { + handleDataSetCreated, + handlePiecesAdded, + getRootEntityId, +} from "../src/pdp-verifier"; +import { + handleFwssDataSetCreated, + handleFwssPieceAdded, + handleFwssServiceTerminated, + handleFwssPdpPaymentTerminated, + handleFwssDataSetServiceProviderChanged, +} from "../src/fwss"; +import { + createDataSetCreatedEvent, + createRootsAddedEvent, +} from "./pdp-verifier-utils"; +import { + createFwssDataSetCreatedEvent, + createFwssPieceAddedEvent, + createFwssServiceTerminatedEvent, + createFwssPdpPaymentTerminatedEvent, + createFwssDataSetServiceProviderChangedEvent, +} from "./fwss-utils"; + +const SET_ID = BigInt.fromI32(1); +const PROVIDER_ID = BigInt.fromI32(42); +const PDP_RAIL_ID = BigInt.fromI32(99); +const ROOT_ID = BigInt.fromI32(101); +const PROVIDER_ADDRESS = Address.fromString( + "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" +); +const PAYER_ADDRESS = Address.fromString( + "0xb16081f360e3847006db660bae1c6d1b2e17ec2b" +); +const NEW_PROVIDER_ADDRESS = Address.fromString( + "0xc16081f360e3847006db660bae1c6d1b2e17ec2c" +); +const CONTRACT_ADDRESS = Address.fromString( + "0xd16081f360e3847006db660bae1c6d1b2e17ec2d" +); + +const PROOF_SET_ENTITY_ID = Bytes.fromByteArray(Bytes.fromBigInt(SET_ID)); + +function seedDataSet(): void { + let ev = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + Bytes.fromI32(0), + CONTRACT_ADDRESS + ); + handleDataSetCreated(ev); +} + +function seedRoot(): void { + let ev = createRootsAddedEvent( + SET_ID, + [ROOT_ID], + PROVIDER_ADDRESS, + CONTRACT_ADDRESS + ); + handlePiecesAdded(ev); +} + +describe("FWSS handlers", () => { + beforeEach(() => { + clearStore(); + }); + + // -- handleFwssDataSetCreated ------------------------------------------- + + test("PDPVerifier-created DataSet has default FWSS fields", () => { + seedDataSet(); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withIPFSIndexing", + "false" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withCDN", + "false" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "metadataKeys", + "[]" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "metadataValues", + "[]" + ); + }); + + test("handleFwssDataSetCreated populates FWSS fields and derives withIPFSIndexing", () => { + seedDataSet(); + let ev = createFwssDataSetCreatedEvent( + SET_ID, + PROVIDER_ID, + PDP_RAIL_ID, + PAYER_ADDRESS, + PROVIDER_ADDRESS, + ["source", "withIPFSIndexing", "withCDN"], + ["filecoin-pin", "", "true"] + ); + handleFwssDataSetCreated(ev); + + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssProviderId", + PROVIDER_ID.toString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssPayer", + PAYER_ADDRESS.toHexString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssServiceProvider", + PROVIDER_ADDRESS.toHexString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssPdpRailId", + PDP_RAIL_ID.toString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withIPFSIndexing", + "true" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withCDN", + "true" + ); + }); + + test("handleFwssDataSetCreated leaves booleans false when keys absent", () => { + seedDataSet(); + let ev = createFwssDataSetCreatedEvent( + SET_ID, + PROVIDER_ID, + PDP_RAIL_ID, + PAYER_ADDRESS, + PROVIDER_ADDRESS, + ["source"], + ["filecoin-pin"] + ); + handleFwssDataSetCreated(ev); + + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withIPFSIndexing", + "false" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withCDN", + "false" + ); + }); + + test("handleFwssDataSetCreated creates a stub when DataSet doesn't exist yet", () => { + // FWSS.DataSetCreated fires BEFORE PDPVerifier.DataSetCreated in the same + // tx (see PDPVerifier._createDataSet). When our handler runs first, it + // must create a stub with FWSS fields set so the later PDPVerifier handler + // can load it instead of overwriting. + const UNSEEN_SET_ID = BigInt.fromI32(999); + const unseenEntityId = Bytes.fromByteArray( + Bytes.fromBigInt(UNSEEN_SET_ID) + ).toHexString(); + + let ev = createFwssDataSetCreatedEvent( + UNSEEN_SET_ID, + PROVIDER_ID, + PDP_RAIL_ID, + PAYER_ADDRESS, + PROVIDER_ADDRESS, + ["withIPFSIndexing"], + [""] + ); + handleFwssDataSetCreated(ev); + + // Stub was created with FWSS fields populated. + assert.fieldEquals("DataSet", unseenEntityId, "setId", "999"); + assert.fieldEquals("DataSet", unseenEntityId, "fwssPayer", PAYER_ADDRESS.toHexString()); + assert.fieldEquals("DataSet", unseenEntityId, "withIPFSIndexing", "true"); + // Placeholder owner/listener set by the FWSS handler (pdp-verifier will + // overwrite when it runs later in the same block). + assert.fieldEquals("DataSet", unseenEntityId, "owner", PROVIDER_ADDRESS.toHexString()); + }); + + test("FWSS-then-PDPVerifier ordering preserves both field groups", () => { + // Simulates real on-chain ordering: FWSS.DataSetCreated fires before + // PDPVerifier.DataSetCreated. After both handlers run, FWSS and + // PDPVerifier fields must both be populated correctly. + let fwssEv = createFwssDataSetCreatedEvent( + SET_ID, + PROVIDER_ID, + PDP_RAIL_ID, + PAYER_ADDRESS, + PROVIDER_ADDRESS, + ["withIPFSIndexing", "withCDN"], + ["", "true"] + ); + handleFwssDataSetCreated(fwssEv); + + // Then PDPVerifier fires, which must load the stub (not overwrite it). + let pdpEv = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + Bytes.fromI32(0), + CONTRACT_ADDRESS + ); + handleDataSetCreated(pdpEv); + + // FWSS fields preserved + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssProviderId", + PROVIDER_ID.toString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withIPFSIndexing", + "true" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withCDN", + "true" + ); + // PDPVerifier fields set + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "setId", + SET_ID.toString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "isActive", + "true" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "status", + "EMPTY" + ); + }); + + // -- handleFwssPieceAdded ----------------------------------------------- + + test("PDPVerifier-created Root has default FWSS fields", () => { + seedDataSet(); + seedRoot(); + const rootId = getRootEntityId(SET_ID, ROOT_ID).toHexString(); + assert.fieldEquals("Root", rootId, "metadataKeys", "[]"); + assert.fieldEquals("Root", rootId, "metadataValues", "[]"); + }); + + test("handleFwssPieceAdded extracts ipfsRootCID", () => { + seedDataSet(); + seedRoot(); + let ev = createFwssPieceAddedEvent( + SET_ID, + ROOT_ID, + Bytes.fromHexString("0xdeadbeef"), + ["ipfsRootCID"], + ["bafybeiexamplecid"] + ); + handleFwssPieceAdded(ev); + + const rootId = getRootEntityId(SET_ID, ROOT_ID).toHexString(); + assert.fieldEquals("Root", rootId, "ipfsRootCID", "bafybeiexamplecid"); + }); + + test("handleFwssPieceAdded leaves ipfsRootCID null when absent", () => { + seedDataSet(); + seedRoot(); + let ev = createFwssPieceAddedEvent( + SET_ID, + ROOT_ID, + Bytes.fromHexString("0xdeadbeef"), + [], + [] + ); + handleFwssPieceAdded(ev); + + // When a nullable field has no value, matchstick's fieldEquals with "null" + // matches. Verify no crash and empty arrays persist. + const rootId = getRootEntityId(SET_ID, ROOT_ID).toHexString(); + assert.fieldEquals("Root", rootId, "metadataKeys", "[]"); + }); + + test("handleFwssPieceAdded no-ops for unknown pieceId", () => { + seedDataSet(); + // no seedRoot — root doesn't exist + let ev = createFwssPieceAddedEvent( + SET_ID, + BigInt.fromI32(999), + Bytes.fromHexString("0xdeadbeef"), + ["ipfsRootCID"], + ["bafybeinope"] + ); + handleFwssPieceAdded(ev); + + const rootId = getRootEntityId(SET_ID, BigInt.fromI32(999)).toHexString(); + assert.notInStore("Root", rootId); + }); + + // -- handleFwssServiceTerminated ---------------------------------------- + + test("handleFwssServiceTerminated flips isActive to false", () => { + seedDataSet(); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "isActive", + "true" + ); + + let ev = createFwssServiceTerminatedEvent(SET_ID, PROVIDER_ADDRESS); + handleFwssServiceTerminated(ev); + + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "isActive", + "false" + ); + }); + + test("handleFwssServiceTerminated no-ops for unknown dataSetId", () => { + let ev = createFwssServiceTerminatedEvent( + BigInt.fromI32(999), + PROVIDER_ADDRESS + ); + handleFwssServiceTerminated(ev); + assert.notInStore( + "DataSet", + Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString() + ); + }); + + // -- handleFwssPdpPaymentTerminated ------------------------------------- + + test("handleFwssPdpPaymentTerminated stores endEpoch and leaves isActive alone", () => { + seedDataSet(); + let ev = createFwssPdpPaymentTerminatedEvent( + SET_ID, + BigInt.fromI32(12345), + PDP_RAIL_ID + ); + handleFwssPdpPaymentTerminated(ev); + + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "pdpPaymentEndEpoch", + "12345" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "isActive", + "true" + ); + }); + + test("handleFwssPdpPaymentTerminated no-ops for unknown dataSetId", () => { + let ev = createFwssPdpPaymentTerminatedEvent( + BigInt.fromI32(999), + BigInt.fromI32(12345), + PDP_RAIL_ID + ); + handleFwssPdpPaymentTerminated(ev); + assert.notInStore( + "DataSet", + Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString() + ); + }); + + // -- handleFwssDataSetServiceProviderChanged ---------------------------- + + test("handleFwssDataSetServiceProviderChanged updates fwssServiceProvider", () => { + seedDataSet(); + // seed with FWSS initial state + handleFwssDataSetCreated( + createFwssDataSetCreatedEvent( + SET_ID, + PROVIDER_ID, + PDP_RAIL_ID, + PAYER_ADDRESS, + PROVIDER_ADDRESS, + [], + [] + ) + ); + + let ev = createFwssDataSetServiceProviderChangedEvent( + SET_ID, + PROVIDER_ADDRESS, + NEW_PROVIDER_ADDRESS + ); + handleFwssDataSetServiceProviderChanged(ev); + + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssServiceProvider", + NEW_PROVIDER_ADDRESS.toHexString() + ); + }); + + test("handleFwssDataSetServiceProviderChanged no-ops for unknown dataSetId", () => { + let ev = createFwssDataSetServiceProviderChangedEvent( + BigInt.fromI32(999), + PROVIDER_ADDRESS, + NEW_PROVIDER_ADDRESS + ); + handleFwssDataSetServiceProviderChanged(ev); + assert.notInStore( + "DataSet", + Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString() + ); + }); +});