diff --git a/packages/subgraph/README.md b/packages/subgraph/README.md index 619a2d71..a5854752 100644 --- a/packages/subgraph/README.md +++ b/packages/subgraph/README.md @@ -28,7 +28,7 @@ Follow these steps to build and deploy the subgraph: cd path/to/filecoin-pay-explorer/subgraph ``` -2. **Install Dependencies:** +1. **Install Dependencies:** Install the necessary node modules: ```bash @@ -37,28 +37,21 @@ Follow these steps to build and deploy the subgraph: yarn install ``` -3. **Generate Code:** - The Graph CLI uses the `subgraph.yaml` manifest and GraphQL schema (`schema.graphql`) to generate AssemblyScript types. - - ```bash - npm run codegen - ``` - -4. **Build the Subgraph:** - Compile your subgraph code into WebAssembly (WASM). +1. **Generate Code and Build the Subgraph:** + The Graph CLI uses the `subgraph.yaml` manifest and GraphQL schema (`schema.graphql`) to generate AssemblyScript types. Then compile your subgraph code into WebAssembly (WASM). ```bash npm run build ``` -5. **Authenticate with Goldsky:** +1. **Authenticate with Goldsky:** Log in to your Goldsky account using the CLI. Go to settings section of your Goldsky dashboard to get your API key. ```bash goldsky login ``` -6. **Deploy to Goldsky:** +1. **Deploy to Goldsky:** Use the Goldsky CLI to deploy your built subgraph. ```bash @@ -69,8 +62,8 @@ Follow these steps to build and deploy the subgraph: - Replace `` with a version identifier (e.g., `v0.0.1`). - You can manage your deployments and find your subgraph details in the [Goldsky Dashboard](https://app.goldsky.com/). The deployment command will output the GraphQL endpoint URL for your subgraph upon successful completion. **Copy this URL**, as you will need it for the client. -7. **Tag the Subgraph (Optional):** - Tag the subgraph you deployed in step 6. +1. **Tag the Subgraph (Optional):** + Tag the subgraph you deployed in step 5. ```bash goldsky subgraph tag create / --tag diff --git a/packages/subgraph/config/Payments-abi.json b/packages/subgraph/config/Payments-abi.json index 24277583..6f4637de 100644 --- a/packages/subgraph/config/Payments-abi.json +++ b/packages/subgraph/config/Payments-abi.json @@ -1,15 +1,4 @@ [ - { - "type": "function", - "name": "burnForFees", - "inputs": [ - { "name": "token", "type": "address", "internalType": "contract IERC20" }, - { "name": "recipient", "type": "address", "internalType": "address" }, - { "name": "requested", "type": "uint256", "internalType": "uint256" } - ], - "outputs": [], - "stateMutability": "payable" - }, { "type": "event", "name": "AccountLockupSettled", diff --git a/packages/subgraph/src/payments.ts b/packages/subgraph/src/payments.ts index b18f2f4d..fc0e92c0 100644 --- a/packages/subgraph/src/payments.ts +++ b/packages/subgraph/src/payments.ts @@ -1,7 +1,6 @@ -import { Address, Bytes, log } from "@graphprotocol/graph-ts"; +import { Address, Bytes, DataSourceContext, dataSource, log } from "@graphprotocol/graph-ts"; import { AccountLockupSettled as AccountLockupSettledEvent, - BurnForFeesCall, DepositRecorded as DepositRecordedEvent, OperatorApprovalUpdated as OperatorApprovalUpdatedEvent, RailCreated as RailCreatedEvent, @@ -14,6 +13,8 @@ import { WithdrawRecorded as WithdrawRecordedEvent, } from "../generated/Payments/Payments"; import { Account, FeeAuctionPurchase, OperatorApproval, Rail, Settlement, Token, UserToken } from "../generated/schema"; +import { TokenTemplate } from "../generated/templates"; +import { Transfer as TransferEvent } from "../generated/templates/TokenTemplate/erc20"; import { computeSettledLockup, createOneTimePayment, @@ -33,12 +34,7 @@ import { updateOperatorTokenLockup, updateOperatorTokenRate, } from "./utils/helpers"; -import { - getFeeAuctionPurchaseEntityId, - getIdFromTxHashAndLogIndex, - getRailEntityId, - getSettlementEntityId, -} from "./utils/keys"; +import { getIdFromTxHashAndLogIndex, getRailEntityId, getSettlementEntityId } from "./utils/keys"; import { MetricsCollectionOrchestrator, ONE_BIG_INT, ZERO_BIG_INT } from "./utils/metrics"; export function handleAccountLockupSettled(event: AccountLockupSettledEvent): void { @@ -556,6 +552,15 @@ export function handleDepositRecorded(event: DepositRecordedEvent): void { userToken.funds = userToken.funds.plus(amount); userToken.save(); + // Native FIL has no ERC-20 contract, so no Transfer events to track. + if (isNewToken && !isNativeToken(tokenAddress)) { + const paymentsAddress = event.address; + const context = new DataSourceContext(); + context.setBytes("paymentsAddress", paymentsAddress); + + TokenTemplate.createWithContext(tokenAddress, context); + } + // Collect Metrics MetricsCollectionOrchestrator.collectTokenActivityMetrics( tokenAddress, @@ -723,31 +728,85 @@ export function handleRailFinalized(event: RailFinalizedEvent): void { ); } -// ==================== Call Handlers ==================== +// ==================== ERC-20 Transfer handlers ==================== + +// Function selector for burnForFees(address,address,uint256). +// Used to identify fee-auction purchases from a top-level tx's calldata prefix. +const BURN_FOR_FEES_SELECTOR_0: u8 = 0x1a; +const BURN_FOR_FEES_SELECTOR_1: u8 = 0x25; +const BURN_FOR_FEES_SELECTOR_2: u8 = 0x73; +const BURN_FOR_FEES_SELECTOR_3: u8 = 0x00; + +/** + * FilecoinPay's burnForFees emits no event, but internally calls + * ERC-20 transfer() on the auctioned token, producing a standard Transfer log. + * We anchor on that log to capture the auction purchase without trace_filter. + * + * Disambiguation from withdrawals (also transfer out from FilecoinPay): + * - from == FilecoinPay (the USDFC balance is FilecoinPay's) + * - top-level tx.to == FilecoinPay + * - top-level tx selector == burnForFees + * + * Fee-on-transfer caveat: Transfer.value is the `actual` amount transferred. + * For standard ERC-20 tokens (USDFC), actual == requested. + */ +export function handleFeeAuctionTransfer(event: TransferEvent): void { + const paymentsAddress = dataSource.context().getBytes("paymentsAddress"); + + // Only interested in outflows FROM FilecoinPay (i.e. FilecoinPay paying out + // either a withdrawal or a fee-auction purchase). + if (event.params.from.notEqual(Address.fromBytes(paymentsAddress))) return; + + // Must be a direct top-level call to FilecoinPay; a router would fall through. + const txTo = event.transaction.to; + if (txTo === null) return; + if ((txTo as Address).notEqual(Address.fromBytes(paymentsAddress))) return; + + // Selector check filters withdrawals and other paths that also produce + // Transfer-from-FilecoinPay events. + const input = event.transaction.input; + if (input.length < 4) return; + if ( + input[0] != BURN_FOR_FEES_SELECTOR_0 || + input[1] != BURN_FOR_FEES_SELECTOR_1 || + input[2] != BURN_FOR_FEES_SELECTOR_2 || + input[3] != BURN_FOR_FEES_SELECTOR_3 + ) { + return; + } -export function handleBurnForFees(call: BurnForFeesCall): void { - const tokenAddress = call.inputs.token; - const recipient = call.inputs.recipient; - const requested = call.inputs.requested; - const filBurned = call.transaction.value; + const tokenAddress = event.address; + const recipient = event.params.to; + const amountPurchased = event.params.value; + const filBurned = event.transaction.value; - // Create FeeAuctionPurchase entity - const purchaseId = getFeeAuctionPurchaseEntityId(call.transaction.hash, call.transaction.index); + const purchaseId = getIdFromTxHashAndLogIndex(event.transaction.hash, event.logIndex); const purchase = new FeeAuctionPurchase(purchaseId); purchase.token = tokenAddress; purchase.recipient = recipient; - purchase.amountPurchased = requested; + purchase.amountPurchased = amountPurchased; purchase.filBurned = filBurned; - purchase.blockNumber = call.block.number; - purchase.blockTimestamp = call.block.timestamp; - purchase.transactionHash = call.transaction.hash; + purchase.blockNumber = event.block.number; + purchase.blockTimestamp = event.block.timestamp; + purchase.transactionHash = event.transaction.hash; purchase.save(); + // The running Token.accumulatedFees total is incremented on settlements and + // one-time payments; draw it back down when those fees are auctioned off so + // the total reflects the actual pending balance (mirrors FilecoinPay's + // `fees.funds = available - actual`). + const token = Token.load(tokenAddress); + if (token) { + token.accumulatedFees = token.accumulatedFees.minus(amountPurchased); + token.totalFilBurnedForFees = token.totalFilBurnedForFees.plus(filBurned); + token.save(); + } + MetricsCollectionOrchestrator.collectFeeAuctionMetrics( - requested, + amountPurchased, filBurned, tokenAddress, - call.block.timestamp, - call.block.number, + event.block.timestamp, + event.block.number, ); } diff --git a/packages/subgraph/src/utils/keys.ts b/packages/subgraph/src/utils/keys.ts index 9230e23b..79ee9dc5 100644 --- a/packages/subgraph/src/utils/keys.ts +++ b/packages/subgraph/src/utils/keys.ts @@ -35,7 +35,3 @@ export function getSettlementEntityId(txHash: Bytes, logIndex: BigInt): Bytes { export function getPaymentsMetricEntityId(): Bytes { return Bytes.fromUTF8(PAYMENTS_NETWORK_STATS_ID); } - -export function getFeeAuctionPurchaseEntityId(txHash: Bytes, txIndex: BigInt): Bytes { - return txHash.concatI32(txIndex.toI32()); -} diff --git a/packages/subgraph/templates/subgraph.template.yaml b/packages/subgraph/templates/subgraph.template.yaml index 3e36bf76..ea8a3150 100644 --- a/packages/subgraph/templates/subgraph.template.yaml +++ b/packages/subgraph/templates/subgraph.template.yaml @@ -29,7 +29,6 @@ dataSources: - Account - Operator - Payments - - FeeAuctionPurchase abis: - name: Payments file: ./config/Payments-abi.json @@ -58,7 +57,30 @@ dataSources: handler: handleWithdrawRecorded - event: AccountLockupSettled(indexed address,indexed address,uint256,uint256,uint256) handler: handleAccountLockupSettled - callHandlers: - - function: burnForFees(address,address,uint256) - handler: handleBurnForFees + file: ./src/payments.ts + +templates: + + # Token Transfer events from FilecoinPay capture fee-auction purchases (burnForFees) + # without needing trace_filter. burnForFees emits no event directly; instead it calls + # ERC-20 transfer to the auction recipient, producing a standard Transfer log we can + # anchor on. We filter by from == FilecoinPay and the top-level tx's function selector. + - name: TokenTemplate + kind: ethereum + network: "{{network}}" + source: + abi: erc20 + mapping: + kind: ethereum/events + apiVersion: 0.0.9 + language: wasm/assemblyscript + entities: + - Token + - FeeAuctionPurchase + abis: + - name: erc20 + file: ./config/erc20-abi.json + eventHandlers: + - event: Transfer(indexed address,indexed address,uint256) + handler: handleFeeAuctionTransfer file: ./src/payments.ts