Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions packages/subgraph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -69,8 +62,8 @@ Follow these steps to build and deploy the subgraph:
- Replace `<version>` 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 <your-subgraph-name>/<version> --tag <tag-name>
Expand Down
11 changes: 0 additions & 11 deletions packages/subgraph/config/Payments-abi.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
105 changes: 82 additions & 23 deletions packages/subgraph/src/payments.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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).
Comment thread
rvagg marked this conversation as resolved.
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,
);
}
4 changes: 0 additions & 4 deletions packages/subgraph/src/utils/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
30 changes: 26 additions & 4 deletions packages/subgraph/templates/subgraph.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ dataSources:
- Account
- Operator
- Payments
- FeeAuctionPurchase
abis:
- name: Payments
file: ./config/Payments-abi.json
Expand Down Expand Up @@ -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
Loading