ponder-sh / ponder

The backend framework for crypto apps
https://ponder.sh
MIT License
664 stars 105 forks source link

[Feature] Native transfers #1157

Open typedarray opened 1 month ago

typedarray commented 1 month ago

We should add an intuitive high-level API for native transfers, such that you can write indexing functions that fire when a specific account (or list of accounts specified by a factory) sends or receives any amount of the native token.

khaidarkairbek commented 1 month ago

If you could clarify on the native transfers. As I understand, they are not shown in the logs, but in call traces, right? Would it be an additional filter on the call traces then?

khaidarkairbek commented 1 month ago

I am suggesting the API similar to contracts in the createConfig:

export default createConfig({
  networks: {
    mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
  },
  contracts: {
    SudoswapPool: {
      abi: SudoswapPoolAbi,
      network: "mainnet",
      factory: {
        // The address of the factory contract that creates instances of this child contract.
        address: "0xb16c1342E617A5B6E4b631EB114483FDB289c0A4",
        // The event emitted by the factory that announces a new instance of this child contract.
        event: parseAbiItem("event NewPair(address poolAddress)"),
        // The name of the parameter that contains the address of the new child contract.
        parameter: "poolAddress",
      },
      startBlock: 14645816,
    },
  },
  accounts: {
    Alice: {
      network: "mainnet", 
      address: [
        "0x...", 
        "0x...", 
        "0x..."
      ], 
      startBlock: 14645816
    }
  }
});

Similarly, if we would like to track accounts generated from the factory:

export default createConfig({
  networks: {
    mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
  },
  contracts: {
    SudoswapPool: {
      abi: SudoswapPoolAbi,
      network: "mainnet",
      address: "0xb16c1342E617A5B6E4b631EB114483FDB289c0A4"
      startBlock: 14645816,
    },
  },
  accounts: {
    SudoswapUsers: {
      network: "mainnet", 
      factory: {
        // The address of the factory contract that creates instances of this child contract.
        address: "0xb16c1342E617A5B6E4b631EB114483FDB289c0A4",
        // The event emitted by the factory that announces a new instance of this child contract.
        event: parseAbiItem("event Deposit(address sender, uint256 amount)"),
        // The name of the parameter that contains the address of the new child contract.
        parameter: "sender",
      }, 
      startBlock: 14645816
    }
  }
});

This way our accounts config api would be close to what we have for contracts already.

typedarray commented 1 month ago

This is a great start. Some follow-up questions to consider:

Here's a concrete user scenario that will use this feature: A smart account infrastructure provider wants to build an API that serves a list of tokens (native, ERC20, ERC721) for each account created using their tool. The smart accounts are contracts (not EOAs) that get created via a factory contract that is compatible with our existing factory pattern.

khaidarkairbek commented 1 month ago

The first idea that comes to mind:

export default createConfig({
  networks: {
    mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
  },
  accounts: {
    Alice: {
      network: "mainnet", 
      address: [                        // only native transfers (if no events are given)
        "0x...", 
        "0x...", 
        "0x..."
      ], 
      startBlock: 14645816
    }, 
    Bob: {
      network: "mainnet", 
      address: [                        // no native transfers, but event logs
        "0x...", 
        "0x...", 
        "0x..."
      ], 
      events: [                        //list of events to index with corresponding event + parameter(s)
        {
          abi: ERC20Abi,                  //first version, uses an ABI and specifies only eventName
          eventName: 'TransferFrom', 
          parameter: ["from", "to"]     
        }, 
        {
          event: parseAbiItem(
            "event TransferFrom(address indexed from, address indexed to, uint256 value)"
          ),                              //second version, uses parseAbiItem similar to factory config
          parameter: "from"
        },
        ...
      ],
      startBlock: 14645816, 
      includeNativeTransfers: false               // true or false, by default? 
    }, 
  }
});

Then, the indexing functions would look as follows:

//The indexing function for events: 
ponder.on("AccountName:EventName", async ({event, context}) => {

})

//The indexing function for native transfers:
ponder.on("AccountName", async ({event, context}) => {

})

The types for both events I would assume can be identical to those used for indexing contract events and callTraces.

kyscott18 commented 1 month ago

Somewhat unrelated to the comments above, I think another feature we want with accounts is the ability to index all transactions to and from an account.

A concrete example of when this would be useful: tracking the sequencer address on the base chain for a layer 2 rollup.

khaidarkairbek commented 1 month ago

Good point, we would be also interested in filtering native transfers based on "from", "to" parameters. The revised version of the api design would be as shown below.

export default createConfig({
  networks: {
    mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
  },
  accounts: { 
    Bob: {                                     //the user needs to specify events, nativeTransfers or transactions
      network: "mainnet", 
      address: [                        
        "0x...", 
        "0x...", 
        "0x..."
      ], 
      events: [                        //list of events to index with corresponding event + parameter(s)
        {
          abi: ERC20Abi,                  //first version, uses an ABI and specifies only eventName
          eventName: 'TransferFrom', 
          parameter: ["from", "to"]     
        }, 
        {
          event: parseAbiItem(
            "event TransferFrom(address indexed from, address indexed to, uint256 value)"
          ),                              //second version, uses parseAbiItem similar to factory config
          parameter: "from"
        },
        ...
      ],
      nativeTransfers: "from" | "to" | ["from", "to"],
      transactions: "from" | "to" | ["from", "to"],
      startBlock: 14645816
    }, 
  }
});

For transactions, the indexing function might look as follows:

ponder.on("AccountName:transaction", async ({event, context}) => {

})

And type of the event for transaction would be:

{
  block: Block,
  transaction: Transaction, 
  transactionReceipt: TransactionReceipt | undefined
}
kyscott18 commented 1 month ago

We decided that were gonna focus on the internal filter types and work towards offering a low-level api that allows users to directly define a filter. Later on, we can build a high-level "account" api that abstracts away the filters.

Notes on filter types

Filters should be deliberately designed, expressive, and modeled after the fundamentals of the Ethereum blockchain, not necessarily the Ethereum standard JSON-RPC. 


type TransferFilter = {

 type: "transfer";

 chainId: number;
  fromAddress: Address | Address[] | Factory | undefined;
  toAddress: Address | Address[] | Factory | undefined;
  fromBlock: number;
  toBlock: number | undefined;
  
  includeTransactionReceipt: boolean;
}

type TransactionFilter = {
  type: "transaction";
  chainId: number;
  fromAddress: Address | Address[] | Factory | undefined;
  toAddress: Address | Address[] | Factory | undefined;
  fromBlock: number;
  toBlock: number | undefined;
  
  includeTransactionReceipt: boolean;
}

I realized TransactionFilter is very similar to CallTraceFilter, except that it only contains top-level calls. I think it's still fine to separate them, but worth keeping in mind.