I had a lot of fun working on this project. My approach was to create a parser that could be easily extended with new repositories and ethereum clients. Most of the code is tested with unit tests and I tried to keep it as clean as possible. The most interesting parts are commented in the code so that reviewers can understand my thought process.
I created different packages to have a separation of concerns and I used the internal
package for the components that I don't want to expose to the public.
-
internal
e2e
e2e test suite.ethereum
logic to interact with the needed methods of the ethereum client. It relies on a genericRPCClient
interface that can be implemented by any client. Inside the package, there are some utility functions to deal with ethereum hex numbers.jsonrpc
logic to interact with any JSON-RPC server. It is used by my ethereum client. Inside, there are some transport-layer types. TheHTTPRequestBuilder
is a really simple builder, and it could be replaced with a more generic one.storage
implementation of an in-memory concurrency safeTransactionsRepository
andAddressesRepository
.mock
mocks for the tests.testdata
test data used by the tests. Here I used go:embed to simply load a json file with real ethereum block data.
-
parser
logic to parse and observe the ethereum blocks and transactions. The parser accepts different options to enhance the default implementation with more sophisticated components. -
types
types used by the main components.
The default parser can be initialized in the following way:
ctx := context.Background()
log := slog.New(slog.NewJSONHandler(os.Stdout, nil))
p, err := parser.NewParser(
"https://cloudflare-eth.com",
log,
)
// handle the error
err = p.Run(ctx)
The parser can be extended with different components by passing options to the constructor. For example, to use a different repository, you can pass the WithTransactionsRepository
option:
repo := NewTransactionsRepository()
p, err := parser.NewParser(
"https://cloudflare-eth.com",
log,
parser.WithTransactionsRepository(repo),
)
All the available options are defined in the parser/options.go file.
All the unit tests can be run with the following command:
make test
To display the tests coverage in an interactive html page, run the following command:
make display_coverage
In addition, I created a simple e2e test suite that runs the parser with a real ethereum client to show that it works as
expected. The test is located in the tests/e2e
folder. Usually I would run this with a docker-compose file that would start an ethereum emulator, but wanted to keep it simple for this project. To
start the test, just run the following command:
make test_e2e
To lint the code, run the following command:
make lint
To fix linting issues, run the following command:
make fmt
I added a simple GitHub Actions workflow that runs the tests and linter on every pull request.
type Parser interface {
GetCurrentBlock() int
Subscribe(address string) bool
GetTransactions(address string) []Transaction
}
During the implementation, I thought about some improvements that could be made to the public interface of the parser.
context
is not being used in the parser methods. I would definitely add it in a real-world scenario to allow the caller to cancel the operation and pass trace information.- The
Subscribe
andGetTransactions
methods could be improved to return an error. This way, the caller could know if the subscription failed and why (really useful it there is a cache or a network error). - The parser doesn't have a method to start parsing. I assumed that it would start parsing when the parser is created. I really don't like this approach this is why I added a Run method to the parser. This way, the caller can start the parser and control it with a context.
- The
GetCurrentBlock
returns an int. I would change it to return an uint64 to avoid possible future overflow issues.