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.
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:
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.