Commit b83924db authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into contributing-doc-patch

parents 603807bc 6f6a308c
---
'@eth-optimism/atst': patch
---
Release ATST
......@@ -39,6 +39,9 @@ packages/contracts-bedrock/deployments/anvil
# vim
*.sw*
# jetbrains
.idea/
.env
.env*
!.env.example
......
......@@ -9,8 +9,10 @@ import (
"syscall"
"time"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/op-batcher/rpc"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
oppprof "github.com/ethereum-optimism/optimism/op-service/pprof"
......@@ -42,17 +44,16 @@ func Main(version string, cliCtx *cli.Context) error {
return err
}
l.Info("Starting Batch Submitter")
if err := batchSubmitter.Start(); err != nil {
l.Error("Unable to start Batch Submitter", "error", err)
return err
if !cfg.Stopped {
if err := batchSubmitter.Start(); err != nil {
l.Error("Unable to start Batch Submitter", "error", err)
return err
}
}
defer batchSubmitter.Stop()
defer batchSubmitter.StopIfRunning()
ctx, cancel := context.WithCancel(context.Background())
l.Info("Batch Submitter started")
pprofConfig := cfg.PprofConfig
if pprofConfig.Enabled {
l.Info("starting pprof", "addr", pprofConfig.ListenAddr, "port", pprofConfig.ListenPort)
......@@ -81,6 +82,13 @@ func Main(version string, cliCtx *cli.Context) error {
rpcCfg.ListenPort,
version,
)
if rpcCfg.EnableAdmin {
server.AddAPI(gethrpc.API{
Namespace: "admin",
Service: rpc.NewAdminAPI(batchSubmitter),
})
l.Info("Admin RPC enabled")
}
if err := server.Start(); err != nil {
cancel()
return fmt.Errorf("error starting RPC server: %w", err)
......@@ -97,5 +105,4 @@ func Main(version string, cliCtx *cli.Context) error {
cancel()
_ = server.Stop()
return nil
}
......@@ -9,12 +9,12 @@ import (
"github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/op-batcher/flags"
"github.com/ethereum-optimism/optimism/op-batcher/rpc"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/sources"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
oppprof "github.com/ethereum-optimism/optimism/op-service/pprof"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
opsigner "github.com/ethereum-optimism/optimism/op-signer/client"
)
......@@ -81,7 +81,7 @@ type CLIConfig struct {
// PrivateKey is the private key used to submit sequencer transactions.
PrivateKey string
RPCConfig oprpc.CLIConfig
RPCConfig rpc.CLIConfig
/* Optional Params */
......@@ -98,6 +98,8 @@ type CLIConfig struct {
// compression algorithm.
ApproxComprRatio float64
Stopped bool
LogConfig oplog.CLIConfig
MetricsConfig opmetrics.CLIConfig
......@@ -145,10 +147,11 @@ func NewConfig(ctx *cli.Context) CLIConfig {
TargetL1TxSize: ctx.GlobalUint64(flags.TargetL1TxSizeBytesFlag.Name),
TargetNumFrames: ctx.GlobalInt(flags.TargetNumFramesFlag.Name),
ApproxComprRatio: ctx.GlobalFloat64(flags.ApproxComprRatioFlag.Name),
Stopped: ctx.GlobalBool(flags.StoppedFlag.Name),
Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name),
SequencerHDPath: ctx.GlobalString(flags.SequencerHDPathFlag.Name),
PrivateKey: ctx.GlobalString(flags.PrivateKeyFlag.Name),
RPCConfig: oprpc.ReadCLIConfig(ctx),
RPCConfig: rpc.ReadCLIConfig(ctx),
LogConfig: oplog.ReadCLIConfig(ctx),
MetricsConfig: opmetrics.ReadCLIConfig(ctx),
PprofConfig: oppprof.ReadCLIConfig(ctx),
......
......@@ -29,6 +29,9 @@ type BatchSubmitter struct {
ctx context.Context
cancel context.CancelFunc
mutex sync.Mutex
running bool
// lastStoredBlock is the last block loaded into `state`. If it is empty it should be set to the l2 safe head.
lastStoredBlock eth.BlockID
......@@ -95,17 +98,14 @@ func NewBatchSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger) (*BatchSubmitte
},
}
return NewBatchSubmitter(batcherCfg, l)
return NewBatchSubmitter(ctx, batcherCfg, l)
}
// NewBatchSubmitter initializes the BatchSubmitter, gathering any resources
// that will be needed during operation.
func NewBatchSubmitter(cfg Config, l log.Logger) (*BatchSubmitter, error) {
ctx, cancel := context.WithCancel(context.Background())
func NewBatchSubmitter(ctx context.Context, cfg Config, l log.Logger) (*BatchSubmitter, error) {
balance, err := cfg.L1Client.BalanceAt(ctx, cfg.From, nil)
if err != nil {
cancel()
return nil, err
}
......@@ -117,26 +117,59 @@ func NewBatchSubmitter(cfg Config, l log.Logger) (*BatchSubmitter, error) {
txMgr: NewTransactionManager(l,
cfg.TxManagerConfig, cfg.Rollup.BatchInboxAddress, cfg.Rollup.L1ChainID,
cfg.From, cfg.L1Client),
done: make(chan struct{}),
// TODO: this context only exists because the event loop doesn't reach done
// if the tx manager is blocking forever due to e.g. insufficient balance.
ctx: ctx,
cancel: cancel,
state: NewChannelManager(l, cfg.Channel),
state: NewChannelManager(l, cfg.Channel),
}, nil
}
func (l *BatchSubmitter) Start() error {
l.log.Info("Starting Batch Submitter")
l.mutex.Lock()
defer l.mutex.Unlock()
if l.running {
return errors.New("batcher is already running")
}
l.running = true
l.done = make(chan struct{})
// TODO: this context only exists because the event loop doesn't reach done
// if the tx manager is blocking forever due to e.g. insufficient balance.
l.ctx, l.cancel = context.WithCancel(context.Background())
l.state.Clear()
l.lastStoredBlock = eth.BlockID{}
l.wg.Add(1)
go l.loop()
l.log.Info("Batch Submitter started")
return nil
}
func (l *BatchSubmitter) Stop() {
func (l *BatchSubmitter) StopIfRunning() {
_ = l.Stop()
}
func (l *BatchSubmitter) Stop() error {
l.log.Info("Stopping Batch Submitter")
l.mutex.Lock()
defer l.mutex.Unlock()
if !l.running {
return errors.New("batcher is not running")
}
l.running = false
l.cancel()
close(l.done)
l.wg.Wait()
l.log.Info("Batch Submitter stopped")
return nil
}
// loadBlocksIntoState loads all blocks since the previous stored block
......@@ -199,7 +232,7 @@ func (l *BatchSubmitter) calculateL2BlockRangeToStore(ctx context.Context) (eth.
}
// Check last stored to see if it needs to be set on startup OR set if is lagged behind.
// It lagging implies that the op-node processed some batches that where submitted prior to the current instance of the batcher being alive.
// It lagging implies that the op-node processed some batches that were submitted prior to the current instance of the batcher being alive.
if l.lastStoredBlock == (eth.BlockID{}) {
l.log.Info("Starting batch-submitter work at safe-head", "safe", syncStatus.SafeL2)
l.lastStoredBlock = syncStatus.SafeL2.ID()
......@@ -263,7 +296,7 @@ func (l *BatchSubmitter) loop() {
// hack to exit this loop. Proper fix is to do request another send tx or parallel tx sending
// from the channel manager rather than sending the channel in a loop. This stalls b/c if the
// context is cancelled while sending, it will never fuilly clearing the pending txns.
// context is cancelled while sending, it will never fully clear the pending txns.
select {
case <-l.ctx.Done():
break blockLoop
......
......@@ -3,6 +3,7 @@ package flags
import (
"github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/op-batcher/rpc"
opservice "github.com/ethereum-optimism/optimism/op-service"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
......@@ -98,6 +99,11 @@ var (
Value: 1.0,
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "APPROX_COMPR_RATIO"),
}
StoppedFlag = cli.BoolFlag{
Name: "stopped",
Usage: "Initialize the batcher in a stopped state. The batcher can be started using the admin_startBatcher RPC",
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "STOPPED"),
}
MnemonicFlag = cli.StringFlag{
Name: "mnemonic",
Usage: "The mnemonic used to derive the wallets for either the " +
......@@ -133,6 +139,7 @@ var optionalFlags = []cli.Flag{
TargetL1TxSizeBytesFlag,
TargetNumFramesFlag,
ApproxComprRatioFlag,
StoppedFlag,
MnemonicFlag,
SequencerHDPathFlag,
PrivateKeyFlag,
......@@ -145,6 +152,7 @@ func init() {
optionalFlags = append(optionalFlags, opmetrics.CLIFlags(envVarPrefix)...)
optionalFlags = append(optionalFlags, oppprof.CLIFlags(envVarPrefix)...)
optionalFlags = append(optionalFlags, opsigner.CLIFlags(envVarPrefix)...)
optionalFlags = append(optionalFlags, rpc.CLIFlags(envVarPrefix)...)
Flags = append(requiredFlags, optionalFlags...)
}
......
package rpc
import (
"context"
)
type batcherClient interface {
Start() error
Stop() error
}
type adminAPI struct {
b batcherClient
}
func NewAdminAPI(dr batcherClient) *adminAPI {
return &adminAPI{
b: dr,
}
}
func (a *adminAPI) StartBatcher(_ context.Context) error {
return a.b.Start()
}
func (a *adminAPI) StopBatcher(_ context.Context) error {
return a.b.Stop()
}
package rpc
import (
"github.com/urfave/cli"
opservice "github.com/ethereum-optimism/optimism/op-service"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
)
const (
EnableAdminFlagName = "rpc.enable-admin"
)
func CLIFlags(envPrefix string) []cli.Flag {
return []cli.Flag{
cli.BoolFlag{
Name: EnableAdminFlagName,
Usage: "Enable the admin API (experimental)",
EnvVar: opservice.PrefixEnvVar(envPrefix, "RPC_ENABLE_ADMIN"),
},
}
}
type CLIConfig struct {
oprpc.CLIConfig
EnableAdmin bool
}
func ReadCLIConfig(ctx *cli.Context) CLIConfig {
return CLIConfig{
CLIConfig: oprpc.ReadCLIConfig(ctx),
EnableAdmin: ctx.GlobalBool(EnableAdminFlagName),
}
}
......@@ -155,3 +155,11 @@ func (s *L2Sequencer) ActBuildToL1HeadExclUnsafe(t Testing) {
s.ActL2EndBlock(t)
}
}
func (s *L2Sequencer) ActBuildL2ToRegolith(t Testing) {
require.NotNil(t, s.rollupCfg.RegolithTime, "cannot activate Regolith when it is not scheduled")
for s.L2Unsafe().Time < *s.rollupCfg.RegolithTime {
s.ActL2StartBlock(t)
s.ActL2EndBlock(t)
}
}
......@@ -4,6 +4,7 @@ import (
"math/rand"
"testing"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
......@@ -12,7 +13,13 @@ import (
"github.com/ethereum-optimism/optimism/op-node/testlog"
)
// TestCrossLayerUser tests that common actions of the CrossLayerUser actor work:
type regolithScheduledTest struct {
name string
regolithTime *hexutil.Uint64
activateRegolith bool
}
// TestCrossLayerUser tests that common actions of the CrossLayerUser actor work in various regolith configurations:
// - transact on L1
// - transact on L2
// - deposit on L1
......@@ -20,9 +27,28 @@ import (
// - prove tx on L1
// - wait 1 week + 1 second
// - finalize withdrawal on L1
func TestCrossLayerUser(gt *testing.T) {
func TestCrossLayerUser(t *testing.T) {
zeroTime := hexutil.Uint64(0)
futureTime := hexutil.Uint64(20)
farFutureTime := hexutil.Uint64(2000)
tests := []regolithScheduledTest{
{name: "NoRegolith", regolithTime: nil, activateRegolith: false},
{name: "NotYetRegolith", regolithTime: &farFutureTime, activateRegolith: false},
{name: "RegolithAtGenesis", regolithTime: &zeroTime, activateRegolith: true},
{name: "RegolithAfterGenesis", regolithTime: &futureTime, activateRegolith: true},
}
for _, test := range tests {
test := test // Use a fixed reference as the tests run in parallel
t.Run(test.name, func(gt *testing.T) {
runCrossLayerUserTest(gt, test)
})
}
}
func runCrossLayerUserTest(gt *testing.T, test regolithScheduledTest) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
dp.DeployConfig.L2GenesisRegolithTimeOffset = test.regolithTime
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
......@@ -64,6 +90,21 @@ func TestCrossLayerUser(gt *testing.T) {
alice.L1.SetUserEnv(l1UserEnv)
alice.L2.SetUserEnv(l2UserEnv)
// Build at least one l2 block so we have an unsafe head with a deposit info tx (genesis block doesn't)
seq.ActL2StartBlock(t)
seq.ActL2EndBlock(t)
if test.activateRegolith {
// advance L2 enough to activate regolith fork
seq.ActBuildL2ToRegolith(t)
}
// Check Regolith is active or not by confirming the system info tx is not a system tx
infoTx, err := l2Cl.TransactionInBlock(t.Ctx(), seq.L2Unsafe().Hash, 0)
require.NoError(t, err)
require.True(t, infoTx.IsDepositTx())
// Should only be a system tx if regolith is not enabled
require.Equal(t, !test.activateRegolith, infoTx.IsSystemTx())
// regular L2 tx, in new L2 block
alice.L2.ActResetTxOpts(t)
alice.L2.ActSetTxToAddr(&dp.Addresses.Bob)(t)
......@@ -158,4 +199,11 @@ func TestCrossLayerUser(gt *testing.T) {
miner.ActL1EndBlock(t)
// check withdrawal succeeded
alice.L1.ActCheckReceiptStatusOfLastTx(true)(t)
// Check Regolith wasn't activated during the test unintentionally
infoTx, err = l2Cl.TransactionInBlock(t.Ctx(), seq.L2Unsafe().Hash, 0)
require.NoError(t, err)
require.True(t, infoTx.IsDepositTx())
// Should only be a system tx if regolith is not enabled
require.Equal(t, !test.activateRegolith, infoTx.IsSystemTx())
}
......@@ -340,7 +340,7 @@ func TestMigration(t *testing.T) {
}, lgr.New("module", "batcher"))
require.NoError(t, err)
t.Cleanup(func() {
batcher.Stop()
batcher.StopIfRunning()
})
proposer, err := l2os.NewL2OutputSubmitterFromCLIConfig(l2os.CLIConfig{
......
......@@ -212,7 +212,7 @@ func (sys *System) Close() {
sys.L2OutputSubmitter.Stop()
}
if sys.BatchSubmitter != nil {
sys.BatchSubmitter.Stop()
sys.BatchSubmitter.StopIfRunning()
}
for _, node := range sys.RollupNodes {
......
......@@ -1173,6 +1173,101 @@ func TestStopStartSequencer(t *testing.T) {
require.Greater(t, blockAfter, blockBefore, "Chain did not advance after starting sequencer")
}
func TestStopStartBatcher(t *testing.T) {
parallel(t)
if !verboseGethNodes {
log.Root().SetHandler(log.DiscardHandler())
}
cfg := DefaultSystemConfig(t)
sys, err := cfg.Start()
require.Nil(t, err, "Error starting up system")
defer sys.Close()
rollupRPCClient, err := rpc.DialContext(context.Background(), sys.RollupNodes["verifier"].HTTPEndpoint())
require.Nil(t, err)
rollupClient := sources.NewRollupClient(client.NewBaseRPCClient(rollupRPCClient))
l2Seq := sys.Clients["sequencer"]
l2Verif := sys.Clients["verifier"]
// retrieve the initial sync status
seqStatus, err := rollupClient.SyncStatus(context.Background())
require.Nil(t, err)
nonce := uint64(0)
sendTx := func() *types.Receipt {
// Submit TX to L2 sequencer node
tx := types.MustSignNewTx(cfg.Secrets.Alice, types.LatestSignerForChainID(cfg.L2ChainIDBig()), &types.DynamicFeeTx{
ChainID: cfg.L2ChainIDBig(),
Nonce: nonce,
To: &common.Address{0xff, 0xff},
Value: big.NewInt(1_000_000_000),
GasTipCap: big.NewInt(10),
GasFeeCap: big.NewInt(200),
Gas: 21000,
})
nonce++
err = l2Seq.SendTransaction(context.Background(), tx)
require.Nil(t, err, "Sending L2 tx to sequencer")
// Let it show up on the unsafe chain
receipt, err := waitForTransaction(tx.Hash(), l2Seq, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on sequencer")
return receipt
}
// send a transaction
receipt := sendTx()
// wait until the block the tx was first included in shows up in the safe chain on the verifier
safeBlockInclusionDuration := time.Duration(3*cfg.DeployConfig.L1BlockTime) * time.Second
_, err = waitForBlock(receipt.BlockNumber, l2Verif, safeBlockInclusionDuration)
require.Nil(t, err, "Waiting for block on verifier")
// ensure the safe chain advances
newSeqStatus, err := rollupClient.SyncStatus(context.Background())
require.Nil(t, err)
require.Greater(t, newSeqStatus.SafeL2.Number, seqStatus.SafeL2.Number, "Safe chain did not advance")
// stop the batch submission
err = sys.BatchSubmitter.Stop()
require.Nil(t, err)
// wait for any old safe blocks being submitted / derived
time.Sleep(safeBlockInclusionDuration)
// get the initial sync status
seqStatus, err = rollupClient.SyncStatus(context.Background())
require.Nil(t, err)
// send another tx
sendTx()
time.Sleep(safeBlockInclusionDuration)
// ensure that the safe chain does not advance while the batcher is stopped
newSeqStatus, err = rollupClient.SyncStatus(context.Background())
require.Nil(t, err)
require.Equal(t, newSeqStatus.SafeL2.Number, seqStatus.SafeL2.Number, "Safe chain advanced while batcher was stopped")
// start the batch submission
err = sys.BatchSubmitter.Start()
require.Nil(t, err)
time.Sleep(safeBlockInclusionDuration)
// send a third tx
receipt = sendTx()
// wait until the block the tx was first included in shows up in the safe chain on the verifier
_, err = waitForBlock(receipt.BlockNumber, l2Verif, safeBlockInclusionDuration)
require.Nil(t, err, "Waiting for block on verifier")
// ensure that the safe chain advances after restarting the batcher
newSeqStatus, err = rollupClient.SyncStatus(context.Background())
require.Nil(t, err)
require.Greater(t, newSeqStatus.SafeL2.Number, seqStatus.SafeL2.Number, "Safe chain did not advance after batcher was restarted")
}
func safeAddBig(a *big.Int, b *big.Int) *big.Int {
return new(big.Int).Add(a, b)
}
......
......@@ -215,6 +215,7 @@ func (n *OpNode) initRPCServer(ctx context.Context, cfg *Config) error {
}
if cfg.RPC.EnableAdmin {
server.EnableAdminAPI(NewAdminAPI(n.l2Driver, n.metrics))
n.log.Info("Admin RPC enabled")
}
n.log.Info("Starting JSON-RPC server")
if err := server.Start(); err != nil {
......
......@@ -115,6 +115,7 @@ services:
ports:
- "6061:6060"
- "7301:7300"
- "6545:8545"
environment:
OP_BATCHER_L1_ETH_RPC: http://l1:8545
OP_BATCHER_L2_ETH_RPC: http://l2:8545
......@@ -130,10 +131,10 @@ services:
OP_BATCHER_RESUBMISSION_TIMEOUT: 30s
OP_BATCHER_MNEMONIC: test test test test test test test test test test test junk
OP_BATCHER_SEQUENCER_HD_PATH: "m/44'/60'/0'/0/2"
OP_BATCHER_SEQUENCER_BATCH_INBOX_ADDRESS: "${SEQUENCER_BATCH_INBOX_ADDRESS}"
OP_BATCHER_LOG_TERMINAL: "true"
OP_BATCHER_PPROF_ENABLED: "true"
OP_BATCHER_METRICS_ENABLED: "true"
OP_BATCHER_RPC_ENABLE_ADMIN: "true"
stateviz:
build:
......
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/35039927/218812217-92f0f784-cb85-43b9-9ca6-e2b9effd9eb2.png">
<img alt="wagmi logo" src="https://user-images.githubusercontent.com/35039927/218812217-92f0f784-cb85-43b9-9ca6-e2b9effd9eb2.png" width="auto" height="300">
</picture>
</p>
<div align="center">
<br />
<br />
<a href="https://optimism.io"><img alt="Optimism" src="https://raw.githubusercontent.com/ethereum-optimism/brand-kit/main/assets/svg/OPTIMISM-R.svg" width=600></a>
<br />
<h3>@eth-optimism/atst</h3> The official SDK and cli for Optimism's attestation Station
<br />
</div>
<p align="center">
I need a tagline here
<p>
<a href="https://www.npmjs.com/package/@eth-optimism/atst" target="\_parent">
<img alt="" src="https://img.shields.io/npm/dm/@eth-optimism/atst.svg" />
......@@ -14,40 +16,40 @@
# atst
atst is a typescript sdk and cli around the attestation station (TODO link to attstation station)
Maybe put a gif of the cli here?
## Visit [Docs](https://evmts-docs-fx6udvub5-evmts.vercel.app/en/getting-started) for more documentation on teh attestation station!
TODO link to oris docs here
atst is a typescript sdk and cli around the attestation station
Maybe link to cli docs and sdk docs seperately?
### Visit [Docs](https://community.optimism.io/docs/governance/attestation-station/) for general documentation on the attestation station!
## Getting started
Install
```bash
npm install @eth-optimism/atst
npm install @eth-optimism/atst wagmi @wagmi/core ethers@5.7.0
```
## ATST SDK
## atst typescript sdk
TODO
The typescript sdk provides a clean [wagmi](https://wagmi.sh/) based interface for reading and writing to the attestation station
## ATST CLI
### See [sdk docs]() for usage instructions
TODO
## atst cli
## Contributing
The cli provides a convenient cli for interacting with the attestation station contract
TODO put a gif here of using it
## React instructions
Please see our [contributing.md](/docs/contributing.md).
For react hooks we recomend using the [wagmi cli](https://wagmi.sh/cli/getting-started) with the [etherscan plugin](https://wagmi.sh/cli/plugins/etherscan) and [react plugin](https://wagmi.sh/cli/plugins/react) to automatically generate react hooks around the attestation station. See [example/react](http://todo.todo.todo) for an example.
## 🚧 WARNING: UNDER CONSTRUCTION 🚧
Use `parseAttestationBytes` and `stringifyAttestationBytes` to parse and stringify attestations before passing them into wagmi hooks.
## Contributing
**This sdk is not yet released**
Please see our [contributing.md](/docs/contributing.md). No contribution is too small.
## Check out these other awesome attestation station tools
Having your contribution denied feels bad. Please consider opening an issue before adding any new features or apis
TODO
\ No newline at end of file
## Check [Awesome ATST](https://todo.todo.todo) for awesome tools and examples around the attestation station
# @eth-optimism/atst (WIP)
TODO
WIP sdk and cli tool for the attestation station. Currently in design phase. Any code here is purely experimental proof of concepts.
## Dev setup
To be able to run the cli commands with npx you must first link the node module so npm treats it like an installed package
```
npm link
```
Alternatively `yarn dev` will run the cli
```bash
# these are the same
npx atst --help
yarn dev --help
```
Example of how to read an attestation
```bash
npx atst read --key "optimist.base-uri" --about 0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5 --creator 0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3
```
# atst cli docs
## Installation
```bash
npm install @eth-optimism/atst --global
```
## Usage
```bash
npx atst <command> [options]
```
## Commands
read read an attestation
write write an attestation
For more info, run any command with the `--help` flag:
```bash
npx atst read --help
npx atst write --help
```
## Options:
-h, --help Display this message
-v, --version Display version number
## Usage:
```bash
npx atst <command> [options]
```
Commands:
read read an attestation
write write an attestation
For more info, run any command with the `--help` flag:
```bash
npx atst read --help
npx atst write --help
```
### Read
`--creator <string> Address of the creator of the attestation`
`--about <string> Address of the subject of the attestation`
`--key <string> Key of the attestation either as string or hex number`
`--data-type [string] Zod validator for the DataType type string | bytes | number | bool | address (default: string)`
`--rpc-url [url] Rpc url to use (default: https://mainnet.optimism.io)`
`--contract [address] Contract address to read from (default: 0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77)`
`-h, --help Display this message`
Example:
```bash
npx atst read --key "optimist.base-uri" --about 0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5 --creator 0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3
```
### write
`--private-key <string> Address of the creator of the attestation`
`--data-type [string] Zod validator for the DataType type string | bytes | number | bool | address (default: string)`
`--about <string> Address of the subject of the attestation`
`--key <string> Key of the attestation either as string or hex number`
`--value <string> undefined`
`--rpc-url [url] Rpc url to use (default: https://mainnet.optimism.io)`
`--contract [address] Contract address to read from (default: 0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77) -h, --help Display this message`
```bash
atst write --key "optimist.base-uri" --about 0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5 --value "my attestation" --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545
atst/0.0.0
```
# atst sdk docs
Typescript sdk for interacting with the ATST based on [@wagmi/core](https://wagmi.sh/core/getting-started)
TODO add a table of contents like [zod](https://github.com/colinhacks/zod/blob/master/README.md)
## Installation
Install atst and it's peer dependencies.
npm
```bash
npm i @eth-optimism/atst @wagmi/core ethers@5.7.0
```
pnpm
```bash
pnpm i @eth-optimism/atst @wagmi/core ethers@5.7.0
```
yarn
```bash
yarn add @eth-optimism/atst @wagmi/core ethers@5.7.0
```
**Note** as ethers v6 is not yet stable we only support ethers v5 at this time
## Basic usage
### Basic Setup
ATST uses `@wagmi/core` under the hood. See their documentation for more information.
```typescript
import { connect, createClient } from '@wagmi/core'
import { providers, Wallet } from 'ethers'
const provider = new providers.JsonRpcProvider({
url: parsedOptions.rpcUrl,
headers: {
'User-Agent': '@eth-optimism/atst',
},
})
createClient({
provider,
})
```
### Reading an attestation
Here is an example of reading an attestation used by the optimist nft
```typescript
import { readAttestation } from '@eth-optimism/atst'
const creator = '0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3'
const about = '0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5'
const key = 'optimist.base-uri'
const dataType = 'string' // note string is default
const attestation = await readAttestation(creator, about, key, dataType)
console.log(attestation) // https://assets.optimism.io/4a609661-6774-441f-9fdb-453fdbb89931-bucket/optimist-nft/attributes
```
If reading more than one attestation you can use readAttestations to read them with multicall
### Writing an attestation
To write to an attestation you must [connect](https://wagmi.sh/core/connectors/metaMask) your wagmi client if not already connected. If using Node.js use the [mock connector](https://wagmi.sh/core/connectors/mock)
```typescript
import { prepareWriteAttestation, writeAttestation } from '@eth-optimism/sdk'
const preparedTx = await prepareWriteAttestation(about, key, 'hello world')
console.log(preparedTx.gasLimit)
await writeAttestation(preparedTx)
```
## API
### ATTESTATION_STATION_ADDRESS
The deterministic deployment address for the attestation station currently deployed with create2 on Optimism and Optimism Goerli `0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77`
```typescript
import { ATTESTATION_STATION_ADDRESS } from '@eth-optimism/atst'
```
### abi
The abi of the attestation station
```typescript
import { abi } from '@eth-optimism/atst'
```
### readAttestation
[Reads](https://wagmi.sh/core/actions/readContract) and parses an attestation based on it's data type.
```typescript
const attestation = await readAttestation(
/**
* Address: The creator of the attestation
*/
creator,
/**
* Address: The about topic of the attestation
*/
about,
/**
* string: The key of the attestation
*/
key,
/**
* 'string' | 'bytes' | 'number' | 'bool' | 'address'
* The data type of the attestation
* @defaults defaults to 'string'
*/
dataType,
/**
* Address: the contract address of the attestation station
* @defaults defaults to the create2 address
*/
contractAddress
)
```
`Return Value` The data returned from invoking the contract method.
### readAttestations
Similar to read attestation but reads multiple attestations at once. Pass in a variadic amount of attestations to read.
```typescript
const attestation = await readAttestations({
/**
* Address: The creator of the attestation
*/
creator,
/**
* Address: The about topic of the attestation
*/
about,
/**
* string: The key of the attestation
*/
key,
/**
* 'string' | 'bytes' | 'number' | 'bool' | 'address'
* The data type of the attestation
* @defaults defaults to 'string'
*/
dataType,
/**
* Address: the contract address of the attestation station
* @defaults defaults to the create2 address
*/
contractAddress,
/**
* Boolean: Whether to allow some of the calls to fail
* Defaults to false
*/
allowFailures,
})
```
### parseAttestationBytes
Parses raw bytes from the attestation station based on the type.
Note: `readAttestation` and `readAttestations` already parse the bytes so this is only necessary if reading attestations directly from chain instead of through this sdkA
```typescript
const attestation = parseAttestationBytes(
/**
* HexString: The raw bytes returned from reading an attestation
*/
bytes,
/**
* 'string' | 'bytes' | 'number' | 'bool' | 'address'
* The data type of the attestation
* @defaults defaults to 'string'
*/
dataType
)
```
### prepareWriteAttestation
[Prepares](https://wagmi.sh/core/actions/prepareWriteContract) an attestation to be written.
```typescript
const preparedTx = await prepareWriteAttestation(about, key, 'hello world')
console.log(preparedTx.gasFee)
```
### stringifyAttestationBytes
Stringifys an attestation into raw bytes.
Note: `writeAttestation` already does this for you so this is only needed if using a library other than the attestation station
```typescript
const stringAttestatoin = stringifyAttestationBytes('hello world')
const numberAttestation = stringifyAttestationBytes(500)
const hexAttestation = stringifyAttestationBytes('0x1')
const bigNumberAttestation = stringifyAttestationBytes(
BigNumber.from('9999999999999999999999999')
)
```
### writeAttestation
[Writes the prepared tx](https://wagmi.sh/core/actions/writeContract)
```typescript
const preparedTx = await prepareWriteAttestation(about, key, 'hello world')
await writeAttestation(preparedTx)
```
......@@ -13,6 +13,7 @@
"dev": "tsx src/cli.ts",
"clean": "rm -rf ./node_modules && rm -rf ./dist && rm -rf ./coverage",
"build": "yarn tsup",
"generate": "wagmi generate",
"lint": "yarn lint:fix && yarn lint:check",
"lint:check": "eslint . --max-warnings=0",
"lint:fix": "yarn lint:check --fix",
......@@ -21,16 +22,18 @@
},
"dependencies": {
"cac": "^6.7.14",
"ethers": "^5.7.0",
"picocolors": "^1.0.0",
"ora": "^6.1.2",
"zod": "^3.11.6"
},
"peerDependencies": {
"@wagmi/core": ">0.9.0"
"@wagmi/core": ">0.9.0",
"wagmi": ">0.11.0",
"ethers": "^5.0.0"
},
"devDependencies": {
"@testing-library/react-hooks": "^8.0.1",
"ethers": "^5.7.0",
"jsdom": "^21.1.0",
"react-dom": "^18.2.0",
"react": "^18.2.0",
......@@ -39,6 +42,8 @@
"tsup": "^6.5.0",
"tsx": "^3.12.2",
"typescript": "^4.9.3",
"@wagmi/core": "^0.9.2"
"@wagmi/core": "^0.9.2",
"@wagmi/cli": "~0.1.5",
"wagmi": "~0.11.0"
}
}
......@@ -5,8 +5,12 @@ import { isAddress, isHexString, toUtf8Bytes } from 'ethers/lib/utils.js'
import { WagmiBytes } from '../types/WagmiBytes'
export const stringifyAttestationBytes = (
bytes: WagmiBytes | string | Address | number | boolean
bytes: WagmiBytes | string | Address | number | boolean | BigNumber
) => {
bytes = bytes === '0x' ? '0x0' : bytes
if (BigNumber.isBigNumber(bytes)) {
return bytes.toHexString()
}
if (typeof bytes === 'number') {
return BigNumber.from(bytes).toHexString()
}
......
This diff is collapsed.
import { defineConfig } from '@wagmi/cli'
import { hardhat, react } from '@wagmi/cli/plugins'
import * as chains from 'wagmi/chains'
import {ATTESTATION_STATION_ADDRESS} from '@eth-optimism/atst'
export default defineConfig({
out: 'src/react.ts',
plugins: [
hardhat({
project: '../contracts-periphery',
include: ['AttestationStation.json'],
deployments: {
AttestationStation: {
[chains.optimism.id]: ATTESTATION_STATION_ADDRESS,
[chains.optimismGoerli.id]: ATTESTATION_STATION_ADDRESS,
[chains.foundry.id]: ATTESTATION_STATION_ADDRESS,
}
},
}),
react(),
],
})
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment