1

everybody. I have a task to search in mempool for transactions like "swap tokens for eth" or any other type where a wallet sells tokens and its transaction can affect token reserves. There are many tokens and their contract adresses are stored in an array.

I couldn't find how to filter pending transactions for my task, without high load for CPU. The only option I came up with is to filter out all transactions that have value zero (because 98% of "swap tokens for eth" have value 0.

And so, now my code can filter out most of the transactions that I definitely don't need. But now I need to simulate the ones with value == 0. I found alchemy api to simulate transactions, but it requires filling in the "From" field. But go-ethereum/geth method "SubscribeFullPendingTransactions", does not provide a way to know who sent the transaction. It is possible to find out who sent it using other tools, but it requires many lines of code with type conversions, very many. I found this method unworkable because transactions with value == 0 are 11-14 per second on average.

And I need to find out information about how many tokens a wallet wants to sell before its transaction is executed.

I've been thinking for many days now, and I can't figure out how to solve my problem. If you know how I can solve it, please help me.

1
  • Let me know if my answer helps.
    – Mila A
    Commented Jul 15 at 10:52

1 Answer 1

1

Let's split it into smaller tasks to tackle the problem.

  1. In reality, a properly implemented node should provide you with the from address as well, [as per the Ethereum transaction specification](https://ethereum.org/en/developers/docs/transactions/#whats-a-transaction).

    A submitted transaction includes the following information:

    • from – the address of the sender, that will be signing the transaction. This will be an externally-owned account as contract accounts cannot send transactions.
    • to – the receiving address (if an externally-owned account, the transaction will transfer value. If a contract account, the transaction will execute the contract code)
    • signature – the identifier of the sender. This is generated when the sender's private key signs the transaction and confirms the sender has authorized this transaction
    • nonce - a sequentially incrementing counter which indicates the transaction number from the account
    • value - amount of ETH to transfer from sender to recipient (denominated in WEI, where 1ETH equals 1e+18wei)
    • input data – optional field to include arbitrary data
    • gasLimit – the maximum amount of gas units that can be consumed by the transaction. The EVM specifies the units of gas required by each computational step
    • maxPriorityFeePerGas the maximum price of the consumed gas to be included as a tip to the validator
    • maxFeePerGas the maximum fee per unit of gas willing to be paid for the transaction (inclusive of baseFeePerGas and maxPriorityFeePerGas)

    And the go-ethereum's transaction object looks this way:

    // Transaction is an Ethereum transaction.
    type Transaction struct {
        inner TxData    // Consensus contents of a transaction
        time  time.Time // Time first seen locally (spam avoidance)
    
        // caches
        hash atomic.Pointer[common.Hash]
        size atomic.Uint64
        from atomic.Pointer[sigCache] // the FROM address
    }
    

    For instance, slightly modifying the go-ethereum's test suite, we can see that the from address was logged by the event listener accumulator too, by slightly modifying the go-ethereum/build/ci.go file's doTest function:

    // Running The Tests
    //
    // "tests" also includes static analysis tools such as vet.
    
    func doTest(cmdline []string) {
        var (
            dlgo     = flag.Bool("dlgo", false, "Download Go and build with it")
            arch     = flag.String("arch", "", "Run tests for given architecture")
            cc       = flag.String("cc", "", "Sets C compiler binary")
            coverage = flag.Bool("coverage", false, "Whether to record code coverage")
            verbose  = flag.Bool("v", true, "Whether to log verbosely")
            race     = flag.Bool("race", false, "Execute the race detector")
            short    = flag.Bool("short", false, "Pass the 'short'-flag to go test")
            cachedir = flag.String("cachedir", "./build/cache", "directory for caching downloads")
        )
        flag.CommandLine.Parse(cmdline)
    
        // Get test fixtures.
        csdb := build.MustLoadChecksums("build/checksums.txt")
        downloadSpecTestFixtures(csdb, *cachedir)
    
        // Configure the toolchain.
        tc := build.GoToolchain{GOARCH: *arch, CC: *cc}
        if *dlgo {
            tc.Root = build.DownloadGo(csdb)
        }
        gotest := tc.Go("test")
    
        // CI needs a bit more time for the statetests (default 10m).
        gotest.Args = append(gotest.Args, "-timeout=20m")
    
        // Enable CKZG backend in CI.
        gotest.Args = append(gotest.Args, "-tags=ckzg")
    
        // Enable integration-tests
        gotest.Args = append(gotest.Args, "-tags=integrationtests")
    
        // Test a single package at a time. CI builders are slow
        // and some tests run into timeouts under load.
        gotest.Args = append(gotest.Args, "-p", "1")
        if *coverage {
            gotest.Args = append(gotest.Args, "-covermode=atomic", "-cover")
        }
        if *verbose {
            gotest.Args = append(gotest.Args, "-v")
        }
        if *race {
            gotest.Args = append(gotest.Args, "-race")
        }
        if *short {
            gotest.Args = append(gotest.Args, "-short")
        }
    
        packages := []string{"./ethclient/gethclient"} // <----- just specifying a particular test directory to test the SubscribeFullPendingTransactions function in action
        if len(flag.CommandLine.Args()) > 0 {
            packages = flag.CommandLine.Args()
        }
        gotest.Args = append(gotest.Args, packages...)
        build.MustRun(gotest)
    }
    

    And running the following slightly modified ethclient/gethclient/gethclient_test.go test with make test:

    func testSubscribeFullPendingTransactions(t *testing.T, client *rpc.Client) {
        ec := New(client)
        ethcl := ethclient.NewClient(client)
        // Subscribe to Transactions
        ch := make(chan *types.Transaction)
        ec.SubscribeFullPendingTransactions(context.Background(), ch)
        // Send a transaction
        chainID, err := ethcl.ChainID(context.Background())
        if err != nil {
            t.Fatal(err)
        }
        // Create transaction
        tx := types.NewTransaction(1, common.Address{1}, big.NewInt(1), 22000, big.NewInt(1), nil)
        signer := types.LatestSignerForChainID(chainID)
        signature, err := crypto.Sign(signer.Hash(tx).Bytes(), testKey)
        if err != nil {
            t.Fatal(err)
        }
        signedTx, err := tx.WithSignature(signer, signature)
        if err != nil {
            t.Fatal(err)
        }
        // Send transaction
        err = ethcl.SendTransaction(context.Background(), signedTx)
        if err != nil {
            t.Fatal(err)
        }
        // Check that the transaction was sent over the channel
        tx = <-ch
        if tx.Hash() != signedTx.Hash() {
            t.Fatalf("Invalid tx hash received, got %v, want %v", tx.Hash(), signedTx.Hash())
        }
    
        fromAddress, err := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx)
    
        fmt.Printf("The sender of the transaction indeed is: %s\n", fromAddress)
    }
    

    For fmt.Printf to work properly, please also make sure that you add the required import ("fmt") among the other imports at the top of the file gethclient_test.go file.


    Finally, you'll see the address from getting logged:

    ...
    === RUN   TestGethClient/TestSubscribePendingTxs
    The sender of the transaction indeed is: 0x71562b71999873DB5b286dF957af199Ec94617F7
    === RUN   TestGethClient/TestCallContract
    === RUN   TestGethClient/TestCallContractWithBlockOverrides
    ... (clipped contents)
    

    Voilà!


    • By the way, the legacy transaction has the following structure:
      // LegacyTx is the transaction data of the original Ethereum transactions.
      type LegacyTx struct {
          Nonce    uint64          // nonce of sender account
          GasPrice *big.Int        // wei per gas
          Gas      uint64          // gas limit
          To       *common.Address `rlp:"nil"` // nil means contract creation
          Value    *big.Int        // wei amount
          Data     []byte          // contract invocation input data
          V, R, S  *big.Int        // signature values
      }
      

      The legacy LegacyTx type is also why it's often confused right now like there's no from field in the Transaction type, whereas that property actually exists there in type Transaction right now.

  2. Finally, simulating the transaction is more complex.

    But you can use tools like Alchemy or this package: https://github.com/EnsoFinance/temper. (Dislaimer: I'm not a contributor nor a creator of this package, but it looks very interesting at the quick glance!)

    If approached with a service like the Alchemy Simulation API, you can get exactly precise results from a simulation in any block: alchemy-simulation-api-example

    Alternatively, you can use https://www.quicknode.com/docs/ethereum/eth_call by Quicknode.

    But I believe that you'll need to make an external API request, or make a direct request to the Ethereum (or L2) RPC, calling eth_call to simulate a transaction without actually submitting it to the blockchain.

    Still, all of these require some sort of external API/RPC integration.


Hope it helps! Ping me in the comments if you need something else.


(Edited)

Nethermind's node RPC API supports a simulation eth_call and eth_callMany methods.

Maybe you can try simulating a transaction by referring to the Nethermind node's methods? nethermind-simulate-transaction

E.g.:

curl localhost:8545 \
  -X POST \
  -H "Content-Type: application/json" \
  --data '{
      "jsonrpc": "2.0",
      "id": 0,
      "method": "trace_call",
      "params": [call, traceTypes, blockParameter]
    }'

Where the call is an object:

  • address: string (address)
  • blobVersionedHashes: array of string (hex data)
  • blockHash: string (hash)
  • blockNumber: string (hex integer)
  • chainId: string (hex integer)
  • data: string (hex data)
  • from: string (address)
  • gas: string (hex integer)
  • gasPrice: string (hex integer)
  • hash: string (hash)
  • input: string (hex data)
  • isSystemTx: boolean
  • maxFeePerBlobGas: string (hex integer)
  • maxFeePerGas: string (hex integer)
  • maxPriorityFeePerGas: string (hex integer)
  • mint: string (hex integer)
  • nonce: string (hex integer)
  • r: string (hex integer)
  • s: string (hex integer)
  • sourceHash: string (hash)
  • to: string (address)
  • transactionIndex: string (hex integer)
  • type: integer
  • v: string (hex integer)
  • value: string (hex integer)
  • blockParameter: string (block number or hash or either of earliest, finalized, latest, pending, or safe)

**You can populate the object with corresponding data using SubscribeFullPendingTransactions' callbacks, and the decoded from field.

11
  • 1
    Thank you for your reply! I guess it's my fault for not notifying here that I've solved my problem. I get ‘from’ using this function func GetFrom(tx *types.Transaction) (string, error) { from, err := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx) return from.String(), err } And use the response in a query to the alchemy api . Using a package rather than third party services would probably be better. But this package is written in Rust, but I don't know it.
    – solo11
    Commented Jul 16 at 12:56
  • Aaah, great you've found the solution!!! Glad my answer was helpful, thank you @solo11 🙂
    – Mila A
    Commented Jul 16 at 13:47
  • 1
    Oh, turns out I was wrong. I won't be able to solve my problem because Alchemi api happens to provide daily limits, which are not written anywhere. A plan without limits is very expensive ;//
    – solo11
    Commented Jul 16 at 18:38
  • At least the Step №1 is now clear: extracting the from address is possible from a transaction WS receipt listening to SubscribeFullPendingTransactions. Yeah, Alchemy is powerful, yet their prices are quite pesky. Maybe you can use the NPM package then? (github.com/EnsoFinance/temper) It requires a local/remote RPC node running, these are available for free, or at low costs plans can be purchased from Ankr. Probably the time efficiency will be much weaker though... @solo11
    – Mila A
    Commented Jul 16 at 20:43
  • I have nethermind node, it is installed on my server. I don't understand how I can interact with this package, it is written in Rust.
    – solo11
    Commented Jul 17 at 8:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.