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

Merge branch 'develop' into michael/stop-start-batcher

parents c6ea1e2b e8e0c0b4
---
'@eth-optimism/atst': patch
---
Add new atst package
---
'@eth-optimism/atst': patch
---
Release ATST
...@@ -191,6 +191,6 @@ require ( ...@@ -191,6 +191,6 @@ require (
nhooyr.io/websocket v1.8.7 // indirect nhooyr.io/websocket v1.8.7 // indirect
) )
replace github.com/ethereum/go-ethereum v1.11.2 => github.com/ethereum-optimism/op-geth v0.0.0-20230222154945-12d211246519 replace github.com/ethereum/go-ethereum v1.11.2 => github.com/ethereum-optimism/op-geth v1.11.2-aea0402.0.20230227230209-0705cf1b7df9
//replace github.com/ethereum/go-ethereum v1.11.2 => ../go-ethereum //replace github.com/ethereum/go-ethereum v1.11.2 => ../go-ethereum
...@@ -217,8 +217,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 ...@@ -217,8 +217,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 h1:RWHKLhCrQThMfch+QJ1Z8veEq5ZO3DfIhZ7xgRP9WTc= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 h1:RWHKLhCrQThMfch+QJ1Z8veEq5ZO3DfIhZ7xgRP9WTc=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3/go.mod h1:QziizLAiF0KqyLdNJYD7O5cpDlaFMNZzlxYNcWsJUxs= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3/go.mod h1:QziizLAiF0KqyLdNJYD7O5cpDlaFMNZzlxYNcWsJUxs=
github.com/ethereum-optimism/op-geth v0.0.0-20230222154945-12d211246519 h1:6ha+V+b2sHGSgJHLvrrrGjJM27Vr51g381Vq4Jj0Qfc= github.com/ethereum-optimism/op-geth v1.11.2-aea0402.0.20230227230209-0705cf1b7df9 h1:O13fqCZYW+HiGVs+UFKtMUHnCMpWR7XcyTPijm9IAiY=
github.com/ethereum-optimism/op-geth v0.0.0-20230222154945-12d211246519/go.mod h1:/tjlXxOaovIyuF0l6+wCzr6AtDb3lYWTymmpQAQcqu8= github.com/ethereum-optimism/op-geth v1.11.2-aea0402.0.20230227230209-0705cf1b7df9/go.mod h1:/tjlXxOaovIyuF0l6+wCzr6AtDb3lYWTymmpQAQcqu8=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
......
...@@ -2,7 +2,10 @@ module github.com/ethereum-optimism/optimism/indexer ...@@ -2,7 +2,10 @@ module github.com/ethereum-optimism/optimism/indexer
go 1.17 go 1.17
replace github.com/ethereum/go-ethereum v1.10.26 => github.com/ethereum-optimism/op-geth v0.0.0-20230214215134-401b7fd3309b replace (
github.com/ethereum/go-ethereum v1.10.26 => github.com/ethereum-optimism/op-geth v0.0.0-20230214215134-401b7fd3309b
github.com/ethereum/go-ethereum v1.11.2 => github.com/ethereum-optimism/op-geth v1.11.2-aea0402.0.20230227230209-0705cf1b7df9
)
require ( require (
github.com/ethereum-optimism/optimism/op-bindings v0.10.14 github.com/ethereum-optimism/optimism/op-bindings v0.10.14
......
...@@ -69,6 +69,9 @@ type DeployConfig struct { ...@@ -69,6 +69,9 @@ type DeployConfig struct {
L2GenesisBlockParentHash common.Hash `json:"l2GenesisBlockParentHash"` L2GenesisBlockParentHash common.Hash `json:"l2GenesisBlockParentHash"`
L2GenesisBlockBaseFeePerGas *hexutil.Big `json:"l2GenesisBlockBaseFeePerGas"` L2GenesisBlockBaseFeePerGas *hexutil.Big `json:"l2GenesisBlockBaseFeePerGas"`
// Seconds after genesis block that Regolith hard fork activates. 0 to activate at genesis. Nil to disable regolith
L2GenesisRegolithTimeOffset *hexutil.Uint64 `json:"l2GenesisRegolithTimeOffset,omitempty"`
// Owner of the ProxyAdmin predeploy // Owner of the ProxyAdmin predeploy
ProxyAdminOwner common.Address `json:"proxyAdminOwner"` ProxyAdminOwner common.Address `json:"proxyAdminOwner"`
// Owner of the system on L1 // Owner of the system on L1
...@@ -284,6 +287,17 @@ func (d *DeployConfig) InitDeveloperDeployedAddresses() error { ...@@ -284,6 +287,17 @@ func (d *DeployConfig) InitDeveloperDeployedAddresses() error {
return nil return nil
} }
func (d *DeployConfig) RegolithTime(genesisTime uint64) *uint64 {
if d.L2GenesisRegolithTimeOffset == nil {
return nil
}
v := uint64(0)
if offset := *d.L2GenesisRegolithTimeOffset; offset > 0 {
v = genesisTime + uint64(offset)
}
return &v
}
// RollupConfig converts a DeployConfig to a rollup.Config // RollupConfig converts a DeployConfig to a rollup.Config
func (d *DeployConfig) RollupConfig(l1StartBlock *types.Block, l2GenesisBlockHash common.Hash, l2GenesisBlockNumber uint64) (*rollup.Config, error) { func (d *DeployConfig) RollupConfig(l1StartBlock *types.Block, l2GenesisBlockHash common.Hash, l2GenesisBlockNumber uint64) (*rollup.Config, error) {
if d.OptimismPortalProxy == (common.Address{}) { if d.OptimismPortalProxy == (common.Address{}) {
...@@ -320,6 +334,7 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *types.Block, l2GenesisBlockHas ...@@ -320,6 +334,7 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *types.Block, l2GenesisBlockHas
BatchInboxAddress: d.BatchInboxAddress, BatchInboxAddress: d.BatchInboxAddress,
DepositContractAddress: d.OptimismPortalProxy, DepositContractAddress: d.OptimismPortalProxy,
L1SystemConfigAddress: d.SystemConfigProxy, L1SystemConfigAddress: d.SystemConfigProxy,
RegolithTime: d.RegolithTime(l1StartBlock.Time()),
}, nil }, nil
} }
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"testing" "testing"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
...@@ -33,3 +34,15 @@ func TestUnmarshalL1StartingBlockTag(t *testing.T) { ...@@ -33,3 +34,15 @@ func TestUnmarshalL1StartingBlockTag(t *testing.T) {
require.NoError(t, json.Unmarshal([]byte(fmt.Sprintf(`{"l1StartingBlockTag": "%s"}`, h)), decoded)) require.NoError(t, json.Unmarshal([]byte(fmt.Sprintf(`{"l1StartingBlockTag": "%s"}`, h)), decoded))
require.EqualValues(t, common.HexToHash(h), *decoded.L1StartingBlockTag.BlockHash) require.EqualValues(t, common.HexToHash(h), *decoded.L1StartingBlockTag.BlockHash)
} }
func TestRegolithTimeZero(t *testing.T) {
regolithOffset := hexutil.Uint64(0)
config := &DeployConfig{L2GenesisRegolithTimeOffset: &regolithOffset}
require.Equal(t, uint64(0), *config.RegolithTime(1234))
}
func TestRegolithTimeAsOffset(t *testing.T) {
regolithOffset := hexutil.Uint64(1500)
config := &DeployConfig{L2GenesisRegolithTimeOffset: &regolithOffset}
require.Equal(t, uint64(1500+5000), *config.RegolithTime(5000))
}
...@@ -289,6 +289,8 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m ...@@ -289,6 +289,8 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m
// Set the Optimism options. // Set the Optimism options.
cfg.BedrockBlock = bedrockBlock.Number() cfg.BedrockBlock = bedrockBlock.Number()
// Enable Regolith from the start of Bedrock
cfg.RegolithTime = new(uint64)
cfg.Optimism = &params.OptimismConfig{ cfg.Optimism = &params.OptimismConfig{
EIP1559Denominator: config.EIP1559Denominator, EIP1559Denominator: config.EIP1559Denominator,
EIP1559Elasticity: config.EIP1559Elasticity, EIP1559Elasticity: config.EIP1559Elasticity,
......
...@@ -54,6 +54,7 @@ func NewL2Genesis(config *DeployConfig, block *types.Block) (*core.Genesis, erro ...@@ -54,6 +54,7 @@ func NewL2Genesis(config *DeployConfig, block *types.Block) (*core.Genesis, erro
TerminalTotalDifficulty: big.NewInt(0), TerminalTotalDifficulty: big.NewInt(0),
TerminalTotalDifficultyPassed: true, TerminalTotalDifficultyPassed: true,
BedrockBlock: new(big.Int).SetUint64(uint64(config.L2GenesisBlockNumber)), BedrockBlock: new(big.Int).SetUint64(uint64(config.L2GenesisBlockNumber)),
RegolithTime: config.RegolithTime(block.Time()),
Optimism: &params.OptimismConfig{ Optimism: &params.OptimismConfig{
EIP1559Denominator: eip1559Denom, EIP1559Denominator: eip1559Denom,
EIP1559Elasticity: eip1559Elasticity, EIP1559Elasticity: eip1559Elasticity,
......
...@@ -214,6 +214,7 @@ func Setup(t require.TestingT, deployParams *DeployParams, alloc *AllocParams) * ...@@ -214,6 +214,7 @@ func Setup(t require.TestingT, deployParams *DeployParams, alloc *AllocParams) *
BatchInboxAddress: deployConf.BatchInboxAddress, BatchInboxAddress: deployConf.BatchInboxAddress,
DepositContractAddress: predeploys.DevOptimismPortalAddr, DepositContractAddress: predeploys.DevOptimismPortalAddr,
L1SystemConfigAddress: predeploys.DevSystemConfigAddr, L1SystemConfigAddress: predeploys.DevSystemConfigAddr,
RegolithTime: deployConf.RegolithTime(uint64(deployConf.L1GenesisBlockTimestamp)),
} }
deploymentsL1 := DeploymentsL1{ deploymentsL1 := DeploymentsL1{
......
...@@ -179,7 +179,8 @@ func (d *OpGeth) StartBlockBuilding(ctx context.Context, attrs *eth.PayloadAttri ...@@ -179,7 +179,8 @@ func (d *OpGeth) StartBlockBuilding(ctx context.Context, attrs *eth.PayloadAttri
// CreatePayloadAttributes creates a valid PayloadAttributes containing a L1Info deposit transaction followed by the supplied transactions. // CreatePayloadAttributes creates a valid PayloadAttributes containing a L1Info deposit transaction followed by the supplied transactions.
func (d *OpGeth) CreatePayloadAttributes(txs ...*types.Transaction) (*eth.PayloadAttributes, error) { func (d *OpGeth) CreatePayloadAttributes(txs ...*types.Transaction) (*eth.PayloadAttributes, error) {
timestamp := d.L2Head.Timestamp + 2 timestamp := d.L2Head.Timestamp + 2
l1Info, err := derive.L1InfoDepositBytes(d.sequenceNum, d.L1Head, d.SystemConfig, false) regolith := d.L2ChainConfig.IsRegolith(uint64(timestamp))
l1Info, err := derive.L1InfoDepositBytes(d.sequenceNum, d.L1Head, d.SystemConfig, regolith)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
This diff is collapsed.
...@@ -309,6 +309,7 @@ func (cfg SystemConfig) Start() (*System, error) { ...@@ -309,6 +309,7 @@ func (cfg SystemConfig) Start() (*System, error) {
BatchInboxAddress: cfg.DeployConfig.BatchInboxAddress, BatchInboxAddress: cfg.DeployConfig.BatchInboxAddress,
DepositContractAddress: predeploys.DevOptimismPortalAddr, DepositContractAddress: predeploys.DevOptimismPortalAddr,
L1SystemConfigAddress: predeploys.DevSystemConfigAddr, L1SystemConfigAddress: predeploys.DevSystemConfigAddr,
RegolithTime: cfg.DeployConfig.RegolithTime(uint64(cfg.DeployConfig.L1GenesisBlockTimestamp)),
} }
} }
defaultConfig := makeRollupConfig() defaultConfig := makeRollupConfig()
......
...@@ -6,11 +6,11 @@ import subprocess ...@@ -6,11 +6,11 @@ import subprocess
import os import os
GETH_VERSION='v1.10.26' GETH_VERSION='v1.11.2'
def main(): def main():
for project in ('.', 'op-wheel', 'indexer'): for project in ('.', 'indexer'):
print(f'Updating {project}...') print(f'Updating {project}...')
update_mod(project) update_mod(project)
...@@ -22,7 +22,7 @@ def update_mod(project): ...@@ -22,7 +22,7 @@ def update_mod(project):
'mod', 'mod',
'edit', 'edit',
'-replace', '-replace',
f'github.com/ethereum/go-ethereum@{GETH_VERSION}=github.com/ethereum-optimism/op-geth@optimism-history' f'github.com/ethereum/go-ethereum@{GETH_VERSION}=github.com/ethereum-optimism/op-geth@optimism'
], cwd=os.path.join(project), check=True) ], cwd=os.path.join(project), check=True)
print('Tidying...') print('Tidying...')
subprocess.run([ subprocess.run([
......
{
"extends": ["../../.eslintrc.js"]
}
node_modules/
dist/
coverage/
(The MIT License)
Copyright 2020-2022 Optimism
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
<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">
<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" />
</a>
# atst
atst is a typescript sdk and cli around the attestation station
### 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 wagmi @wagmi/core ethers@5.7.0
```
## atst typescript sdk
The typescript sdk provides a clean [wagmi](https://wagmi.sh/) based interface for reading and writing to the attestation station
### See [sdk docs]() for usage instructions
## atst cli
The cli provides a convenient cli for interacting with the attestation station contract
TODO put a gif here of using it
## React instructions
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.
Use `parseAttestationBytes` and `stringifyAttestationBytes` to parse and stringify attestations before passing them into wagmi hooks.
## Contributing
Please see our [contributing.md](/docs/contributing.md). No contribution is too small.
Having your contribution denied feels bad. Please consider opening an issue before adding any new features or apis
## Check [Awesome ATST](https://todo.todo.todo) for awesome tools and examples around the attestation station
# 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)
```
{
"name": "@eth-optimism/atst",
"version": "0.0.0",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",
"module": "dist/index.cjs",
"license": "MIT",
"bin": {
"atst": "./dist/cli.js"
},
"scripts": {
"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",
"test": "vitest",
"typecheck": "yarn tsc --noEmit"
},
"dependencies": {
"cac": "^6.7.14",
"picocolors": "^1.0.0",
"ora": "^6.1.2",
"zod": "^3.11.6"
},
"peerDependencies": {
"@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",
"execa": "^1.0.0",
"vitest": "^0.28.3",
"tsup": "^6.5.0",
"tsx": "^3.12.2",
"typescript": "^4.9.3",
"@wagmi/core": "^0.9.2",
"@wagmi/cli": "~0.1.5",
"wagmi": "~0.11.0"
}
}
# src
Source code for atst
## index.ts
Entrypoint for sdk
## cli.ts
Entrypoint for the cli
## __snapshots__
Vitest snapshots for the vitest tests
## commands
CLI implementations of atst read and write
## contants
Internal and external constants
## lib
All library code for the sdk lives here
## test
Test helpers
## types
Zod and typscript types
\ No newline at end of file
// Vitest Snapshot v1
exports[`logger > \${level}() > logs message "error" 1`] = `"error"`;
exports[`logger > \${level}() > logs message "info" 1`] = `"info"`;
exports[`logger > \${level}() > logs message "log" 1`] = `"log"`;
exports[`logger > \${level}() > logs message "success" 1`] = `"success"`;
exports[`logger > \${level}() > logs message "warn" 1`] = `"warn"`;
#!/usr/bin/env node
import { cac } from 'cac'
import type { Address } from '@wagmi/core'
import { readOptionsValidators, ReadOptions } from './commands/read'
import * as logger from './lib/logger'
// @ts-ignore it's mad about me importing something not in tsconfig.includes
import packageJson from '../package.json'
import { WriteOptions, writeOptionsValidators } from './commands/write'
const cli = cac('atst')
cli
.command('read', 'read an attestation')
.option('--creator <string>', readOptionsValidators.creator.description!)
.option('--about <string>', readOptionsValidators.about.description!)
.option('--key <string>', readOptionsValidators.key.description!)
.option('--data-type [string]', readOptionsValidators.dataType.description!, {
default: readOptionsValidators.dataType.parse(undefined),
})
.option('--rpc-url [url]', readOptionsValidators.rpcUrl.description!, {
default: readOptionsValidators.rpcUrl.parse(undefined),
})
.option('--contract [address]', readOptionsValidators.contract.description!, {
default: readOptionsValidators.contract.parse(undefined),
})
.example(
() =>
`atst read --key "optimist.base-uri" --about 0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5 --creator 0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3`
)
.action(async (options: ReadOptions) => {
const { read } = await import('./commands/read')
// TODO use the native api to do this instead of parsing the raw args
// by default options parses addresses as numbers without precision
// we should use the args parsing library to do this directly
// but for now I didn't bother to figure out how to do that
const { rawArgs } = cli
const about = rawArgs[rawArgs.indexOf('--about') + 1] as Address
const creator = rawArgs[rawArgs.indexOf('--creator') + 1] as Address
const contract = rawArgs.includes('--contract')
? (rawArgs[rawArgs.indexOf('--contract') + 1] as Address)
: options.contract
await read({ ...options, about, creator, contract })
})
cli
.command('write', 'write an attestation')
.option(
'--private-key <string>',
writeOptionsValidators.privateKey.description!
)
.option('--data-type [string]', readOptionsValidators.dataType.description!, {
default: writeOptionsValidators.dataType.parse(undefined),
})
.option('--about <string>', writeOptionsValidators.about.description!)
.option('--key <string>', writeOptionsValidators.key.description!)
.option('--value <string>', writeOptionsValidators.value.description!)
.option('--rpc-url [url]', writeOptionsValidators.rpcUrl.description!, {
default: writeOptionsValidators.rpcUrl.parse(undefined),
})
.option(
'--contract [address]',
writeOptionsValidators.contract.description!,
{
default: writeOptionsValidators.contract.parse(undefined),
}
)
.example(
() =>
`atst write --key "optimist.base-uri" --about 0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5 --value "my attestation" --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545`
)
.action(async (options: WriteOptions) => {
const { write } = await import('./commands/write')
// TODO use the native api to do this instead of parsing the raw args
// by default options parses addresses as numbers without precision
// we should use the args parsing library to do this directly
// but for now I didn't bother to figure out how to do that
const { rawArgs } = cli
const privateKey = rawArgs[rawArgs.indexOf('--private-key') + 1] as Address
const about = rawArgs[rawArgs.indexOf('--about') + 1] as Address
const contract = rawArgs.includes('--contract')
? (rawArgs[rawArgs.indexOf('--contract') + 1] as Address)
: options.contract
await write({ ...options, about, privateKey, contract })
})
cli.help()
cli.version(packageJson.version)
void (async () => {
try {
// Parse CLI args without running command
cli.parse(process.argv, { run: false })
if (!cli.matchedCommand && cli.args.length === 0) {
cli.outputHelp()
}
await cli.runMatchedCommand()
} catch (error) {
logger.error(`\n${(error as Error).message}`)
process.exit(1)
}
})()
import { describe, expect, it } from 'vitest'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { watchConsole } from '../test/watchConsole'
import { read } from './read'
describe(`cli:${read.name}`, () => {
it('should read attestation', async () => {
const creator = '0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3'
const about = '0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5'
const key = 'optimist.base-uri'
const dataType = 'string'
const consoleUtil = watchConsole()
await read({
creator,
about,
key,
dataType,
contract: ATTESTATION_STATION_ADDRESS,
rpcUrl: 'http://localhost:8545',
})
expect(consoleUtil.formatted).toMatchInlineSnapshot(
'"https://assets.optimism.io/4a609661-6774-441f-9fdb-453fdbb89931-bucket/optimist-nft/attributes"'
)
})
})
import { Address, createClient } from '@wagmi/core'
import { isAddress } from 'ethers/lib/utils.js'
import { z } from 'zod'
import { providers } from 'ethers'
import * as logger from '../lib/logger'
import { dataTypeOptionValidator } from '../types/DataTypeOption'
import type { WagmiBytes } from '../types/WagmiBytes'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { DEFAULT_RPC_URL } from '../constants/defaultRpcUrl'
import { readAttestation } from '../lib/readAttestation'
const zodAddress = () =>
z
.string()
.transform((addr) => addr as Address)
.refine(isAddress, { message: 'Invalid address' })
export const readOptionsValidators = {
creator: zodAddress().describe('Address of the creator of the attestation'),
about: zodAddress().describe('Address of the subject of the attestation'),
key: z
.string()
.describe('Key of the attestation either as string or hex number'),
dataType: dataTypeOptionValidator,
rpcUrl: z
.string()
.url()
.optional()
.default(DEFAULT_RPC_URL)
.describe('Rpc url to use'),
contract: zodAddress()
.optional()
.default(ATTESTATION_STATION_ADDRESS)
.describe('Contract address to read from'),
}
const validators = z.object(readOptionsValidators)
export type ReadOptions = z.infer<typeof validators>
export const read = async (options: ReadOptions) => {
// TODO make these errors more user friendly
const parsedOptions = await validators.parseAsync(options).catch((e) => {
logger.error(e)
process.exit(1)
})
const provider = new providers.JsonRpcProvider({
url: parsedOptions.rpcUrl,
headers: {
'User-Agent': '@eth-optimism/atst',
},
})
createClient({
provider,
})
try {
const result = await readAttestation(
parsedOptions.creator,
parsedOptions.about,
parsedOptions.key as WagmiBytes,
parsedOptions.dataType,
parsedOptions.contract
)
logger.log(result?.toString())
return result?.toString()
} catch (e) {
logger.error('Unable to read attestation', e)
process.exit(1)
}
}
import { Address } from '@wagmi/core'
import { Wallet } from 'ethers'
import { describe, expect, it } from 'vitest'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { read } from './read'
import { write } from './write'
describe(`cli:${write.name}`, () => {
it('should write attestation', async () => {
// Anvil account[0]
const privateKey =
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
const publicKey = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
const about = Wallet.createRandom().address as Address
const key = 'key'
const value = 'value'
const rpcUrl = 'http://localhost:8545'
const txHash = await write({
privateKey,
about,
key,
value,
contract: ATTESTATION_STATION_ADDRESS,
rpcUrl,
})
expect(txHash.startsWith('0x')).toBe(true)
// check that attestation was written
const attestation = await read({
creator: publicKey,
about,
key,
dataType: 'string',
contract: ATTESTATION_STATION_ADDRESS,
rpcUrl,
})
expect(attestation).toBe(value)
})
})
import { Address, connect, createClient } from '@wagmi/core'
import { isAddress } from 'ethers/lib/utils.js'
import { z } from 'zod'
import { providers, Wallet } from 'ethers'
import { MockConnector } from '@wagmi/core/connectors/mock'
import * as logger from '../lib/logger'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { DEFAULT_RPC_URL } from '../constants/defaultRpcUrl'
import { prepareWriteAttestation } from '../lib/prepareWriteAttestation'
import { writeAttestation } from '../lib/writeAttestation'
import { castAsDataType } from '../lib/castAsDataType'
import { dataTypeOptionValidator } from '../types/DataTypeOption'
const zodAddress = () =>
z
.string()
.transform((addr) => addr as Address)
.refine(isAddress, { message: 'Invalid address' })
const zodWallet = () => z.string().refine((key) => new Wallet(key))
const zodAttestation = () => z.union([z.string(), z.number(), z.boolean()])
export const writeOptionsValidators = {
privateKey: zodWallet().describe('Address of the creator of the attestation'),
about: zodAddress().describe('Address of the subject of the attestation'),
key: z
.string()
.describe('Key of the attestation either as string or hex number'),
value: zodAttestation().describe('Attestation value').default(''),
dataType: dataTypeOptionValidator,
rpcUrl: z
.string()
.url()
.optional()
.default(DEFAULT_RPC_URL)
.describe('Rpc url to use'),
contract: zodAddress()
.optional()
.default(ATTESTATION_STATION_ADDRESS)
.describe('Contract address to read from'),
}
const validators = z.object(writeOptionsValidators)
export type WriteOptions = z.infer<typeof validators>
export const write = async (options: WriteOptions) => {
// TODO make these errors more user friendly
const parsedOptions = await validators.parseAsync(options).catch((e) => {
logger.error(e)
process.exit(1)
})
const provider = new providers.JsonRpcProvider({
url: parsedOptions.rpcUrl,
headers: {
'User-Agent': '@eth-optimism/atst',
},
})
createClient({
provider,
})
const network = await provider.getNetwork()
if (!network) {
logger.error('Unable to detect chainId')
process.exit(1)
}
await connect({
// MockConnector is actually a vanilla connector
// it's called mockConnector because normally they
// expect us to connect with metamask or something
// but we're just using a private key
connector: new MockConnector({
options: {
chainId: network.chainId,
signer: new Wallet(parsedOptions.privateKey, provider),
},
}),
})
try {
const preparedTx = await prepareWriteAttestation(
parsedOptions.about,
parsedOptions.key,
castAsDataType(parsedOptions.value, parsedOptions.dataType),
network.chainId
)
const result = await writeAttestation(preparedTx)
await result.wait()
logger.log(`txHash: ${result.hash}`)
return result.hash
} catch (e) {
logger.error('Unable to read attestation', e)
process.exit(1)
}
}
/**
* The attestation station contract address
* The attestation station contract is deterministically deployed
* to 0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77
*/
export const ATTESTATION_STATION_ADDRESS =
'0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
/**
* @internal
* Default RPC URL for Optimism
*/
export const DEFAULT_RPC_URL = 'https://mainnet.optimism.io'
// constants
export { ATTESTATION_STATION_ADDRESS } from './constants/attestationStationAddress'
// lib
export { readAttestation } from './lib/readAttestation'
export { readAttestations } from './lib/readAttestations'
export { prepareWriteAttestation } from './lib/prepareWriteAttestation'
export { prepareWriteAttestations } from './lib/prepareWriteAttestations'
export { writeAttestation } from './lib/writeAttestation'
export { abi } from './lib/abi'
export { parseAttestationBytes } from './lib/parseAttestationBytes'
export { stringifyAttestationBytes } from './lib/stringifyAttestationBytes'
// types
export type { AttestationReadParams } from './types/AttestationReadParams'
export type { WagmiBytes } from './types/WagmiBytes'
export type { DataTypeOption } from './types/DataTypeOption'
// Vitest Snapshot v1
exports[`logger > \${level}() > logs message "error" 1`] = `"error"`;
exports[`logger > \${level}() > logs message "info" 1`] = `"info"`;
exports[`logger > \${level}() > logs message "log" 1`] = `"log"`;
exports[`logger > \${level}() > logs message "success" 1`] = `"success"`;
exports[`logger > \${level}() > logs message "warn" 1`] = `"warn"`;
import { describe, expect, it } from 'vitest'
import { abi } from './abi'
/**
* This is a low value test that I made only because
* it makes for a good final check that indeed are
* exporting the correct abi
*/
describe('abi', () => {
it('is the correct abi', () => {
const methodNames = abi.map((obj) => (obj as { name: string }).name)
expect(methodNames).toMatchInlineSnapshot(`
[
undefined,
"AttestationCreated",
"attest",
"attest",
"attestations",
"version",
]
`)
})
})
/**
* The attestation station abi
*/
export const abi = [
{
inputs: [],
stateMutability: 'nonpayable',
type: 'constructor',
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'address',
name: 'creator',
type: 'address',
},
{
indexed: true,
internalType: 'address',
name: 'about',
type: 'address',
},
{
indexed: true,
internalType: 'bytes32',
name: 'key',
type: 'bytes32',
},
{
indexed: false,
internalType: 'bytes',
name: 'val',
type: 'bytes',
},
],
name: 'AttestationCreated',
type: 'event',
},
{
inputs: [
{
components: [
{
internalType: 'address',
name: 'about',
type: 'address',
},
{
internalType: 'bytes32',
name: 'key',
type: 'bytes32',
},
{
internalType: 'bytes',
name: 'val',
type: 'bytes',
},
],
internalType: 'struct AttestationStation.AttestationData[]',
name: '_attestations',
type: 'tuple[]',
},
],
name: 'attest',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: '_about',
type: 'address',
},
{
internalType: 'bytes32',
name: '_key',
type: 'bytes32',
},
{
internalType: 'bytes',
name: '_val',
type: 'bytes',
},
],
name: 'attest',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
{
internalType: 'address',
name: '',
type: 'address',
},
{
internalType: 'bytes32',
name: '',
type: 'bytes32',
},
],
name: 'attestations',
outputs: [
{
internalType: 'bytes',
name: '',
type: 'bytes',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'version',
outputs: [
{
internalType: 'string',
name: '',
type: 'string',
},
],
stateMutability: 'view',
type: 'function',
},
] as const
import { DataTypeOption } from '../types/DataTypeOption'
/**
* @internal
* Takes a datatype and returns the value casted to that type
*/
export const castAsDataType = (value: any, dataType: DataTypeOption) => {
if (dataType === 'string') {
return value
} else if (dataType === 'number') {
return Number(value)
} else if (dataType === 'bool') {
return Boolean(value)
} else if (dataType === 'bytes') {
return value
} else if (dataType === 'address') {
return value
} else {
throw new Error(`Unrecognized data type ${dataType satisfies never}`)
}
}
import { afterEach, describe, expect, it, vi } from 'vitest'
import * as logger from './logger'
import { watchConsole } from '../test/watchConsole'
describe('logger', () => {
afterEach(() => {
vi.restoreAllMocks()
})
describe.each([
{ level: 'success' },
{ level: 'info' },
{ level: 'log' },
{ level: 'warn' },
{ level: 'error' },
// eslint-disable-next-line no-template-curly-in-string
])('${level}()', ({ level }) => {
it(`logs message "${level}"`, () => {
const spy = vi.spyOn(logger, level as 'info')
const consoleUtil = watchConsole()
const loggerFn = logger[level]
loggerFn(level)
expect(spy).toHaveBeenCalledWith(level)
expect(consoleUtil.formatted).toMatchSnapshot()
})
})
})
import util from 'util'
import ora from 'ora'
import pc from 'picocolors'
const format = (args: any[]) => {
return util
.format(...args)
.split('\n')
.join('\n')
}
export const success = (...args: Array<any>) => {
console.log(pc.green(format(args)))
}
export const info = (...args: Array<any>) => {
console.info(pc.blue(format(args)))
}
export const log = (...args: Array<any>) => {
console.log(pc.white(format(args)))
}
export const warn = (...args: Array<any>) => {
console.warn(pc.yellow(format(args)))
}
export const error = (...args: Array<any>) => {
console.error(pc.red(format(args)))
}
export const spinner = () => {
return ora({
color: 'gray',
spinner: 'dots8Bit',
})
}
import { BigNumber } from 'ethers'
import { toUtf8Bytes } from 'ethers/lib/utils.js'
import { expect, describe, it } from 'vitest'
import { WagmiBytes } from '../types/WagmiBytes'
import { parseAttestationBytes } from './parseAttestationBytes'
describe(parseAttestationBytes.name, () => {
it('works for strings', () => {
const str = 'Hello World'
const bytes = BigNumber.from(toUtf8Bytes(str)).toHexString() as WagmiBytes
expect(parseAttestationBytes(bytes, 'string')).toBe(str)
})
it('works for numbers', () => {
const num = 123
const bytes = BigNumber.from(num).toHexString() as WagmiBytes
expect(parseAttestationBytes(bytes, 'number')).toBe(num.toString())
})
it('works for addresses', () => {
const addr = '0x1234567890123456789012345678901234567890'
const bytes = BigNumber.from(addr).toHexString() as WagmiBytes
expect(parseAttestationBytes(bytes, 'address')).toBe(addr)
})
it('works for booleans', () => {
const bytes = BigNumber.from(1).toHexString() as WagmiBytes
expect(parseAttestationBytes(bytes, 'bool')).toBe('true')
})
it('should work for raw bytes', () => {
const bytes = '0x420'
expect(parseAttestationBytes(bytes, 'bytes')).toBe(bytes)
})
it('should return raw bytes for invalid type', () => {
const bytes = '0x420'
// @ts-expect-error - this is a test for an error case
expect(parseAttestationBytes(bytes, 'foo')).toBe(bytes)
})
})
import { BigNumber } from 'ethers'
import { toUtf8String } from 'ethers/lib/utils.js'
import type { DataTypeOption } from '../types/DataTypeOption'
import type { WagmiBytes } from '../types/WagmiBytes'
export const parseAttestationBytes = (
attestationBytes: WagmiBytes,
dataType: DataTypeOption
) => {
if (dataType === 'bytes') {
return attestationBytes
}
if (dataType === 'number') {
return BigNumber.from(attestationBytes).toString()
}
if (dataType === 'address') {
return BigNumber.from(attestationBytes).toHexString()
}
if (dataType === 'bool') {
return BigNumber.from(attestationBytes).gt(0) ? 'true' : 'false'
}
if (dataType === 'string') {
return attestationBytes && toUtf8String(attestationBytes)
}
console.warn(`unrecognized dataType ${dataType satisfies never}`)
return attestationBytes
}
import { connect, createClient } from '@wagmi/core'
import { providers, Wallet } from 'ethers'
import { expect, describe, it, beforeAll } from 'vitest'
import { MockConnector } from '@wagmi/core/connectors/mock'
import { prepareWriteAttestation } from './prepareWriteAttestation'
import { readAttestation } from './readAttestation'
const creator = '0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3'
const about = '0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5'
const key = 'optimist.base-uri'
const chainId = 10
const provider = new providers.JsonRpcProvider(
{
url: 'http://localhost:8545',
},
chainId
)
const wallet = Wallet.createRandom({ provider })
createClient({
provider,
})
beforeAll(async () => {
await connect({
connector: new MockConnector({
options: {
chainId,
signer: new Wallet(wallet.privateKey, provider),
},
}),
})
})
describe(prepareWriteAttestation.name, () => {
it('Should correctly prepare an attestation', async () => {
const result = await prepareWriteAttestation(about, key, 'hello world')
expect(result.address).toMatchInlineSnapshot(
'"0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77"'
)
expect(result.chainId).toMatchInlineSnapshot('10')
expect(result.functionName).toMatchInlineSnapshot('"attest"')
expect(result.mode).toMatchInlineSnapshot('"prepared"')
expect(result.request.gasLimit).toMatchInlineSnapshot(`
{
"hex": "0xd6c9",
"type": "BigNumber",
}
`)
})
it('should throw an error if key is longer than 32 bytes', async () => {
const dataType = 'string'
await expect(
readAttestation(
creator,
about,
'this is a key that is way longer than 32 bytes so this key should throw an error matching the inline snapshot',
dataType
)
).rejects.toThrowErrorMatchingInlineSnapshot(
'"Key is longer than the max length of 32 for attestation keys"'
)
})
})
import { Address, prepareWriteContract } from '@wagmi/core'
import { formatBytes32String } from 'ethers/lib/utils.js'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { WagmiBytes } from '../types/WagmiBytes'
import { abi } from './abi'
import { stringifyAttestationBytes } from './stringifyAttestationBytes'
export const prepareWriteAttestation = async (
about: Address,
key: string,
value: string | WagmiBytes | number | boolean,
chainId = 10,
contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => {
const formattedKey = formatBytes32String(key) as WagmiBytes
return prepareWriteContract({
address: contractAddress,
abi,
functionName: 'attest',
chainId,
args: [about, formattedKey, stringifyAttestationBytes(value) as WagmiBytes],
})
}
import { connect, createClient } from '@wagmi/core'
import { providers, Wallet } from 'ethers'
import { expect, describe, it, beforeAll } from 'vitest'
import { MockConnector } from '@wagmi/core/connectors/mock'
import { readAttestation } from './readAttestation'
import { prepareWriteAttestations } from './prepareWriteAttestations'
const creator = '0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3'
const about = '0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5'
const key = 'optimist.base-uri'
const chainId = 10
const provider = new providers.JsonRpcProvider(
{
url: 'http://localhost:8545',
},
chainId
)
const wallet = Wallet.createRandom({ provider })
createClient({
provider,
})
beforeAll(async () => {
await connect({
connector: new MockConnector({
options: {
chainId,
signer: new Wallet(wallet.privateKey, provider),
},
}),
})
})
describe(prepareWriteAttestations.name, () => {
it('Should correctly prepare an attestation', async () => {
const result = await prepareWriteAttestations([
{
about,
key,
value: 'hello world',
},
])
expect(result.address).toMatchInlineSnapshot(
'"0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77"'
)
expect(result.chainId).toMatchInlineSnapshot('10')
expect(result.functionName).toMatchInlineSnapshot('"attest"')
expect(result.mode).toMatchInlineSnapshot('"prepared"')
expect(result.request.gasLimit).toMatchInlineSnapshot(`
{
"hex": "0xd9ce",
"type": "BigNumber",
}
`)
})
it('should throw an error if key is longer than 32 bytes', async () => {
const dataType = 'string'
await expect(
readAttestation(
creator,
about,
'this is a key that is way longer than 32 bytes so this key should throw an error matching the inline snapshot',
dataType
)
).rejects.toThrowErrorMatchingInlineSnapshot(
'"Key is longer than the max length of 32 for attestation keys"'
)
})
})
import { Address, prepareWriteContract } from '@wagmi/core'
import { formatBytes32String } from 'ethers/lib/utils.js'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { WagmiBytes } from '../types/WagmiBytes'
import { abi } from './abi'
import { stringifyAttestationBytes } from './stringifyAttestationBytes'
type Attestation = {
about: Address
key: string
value: string | WagmiBytes | number | boolean
}
export const prepareWriteAttestations = async (
attestations: Attestation[],
chainId = 10,
contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => {
const formattedAttestations = attestations.map((attestation) => {
const formattedKey = formatBytes32String(attestation.key) as WagmiBytes
const formattedValue = stringifyAttestationBytes(
attestation.value
) as WagmiBytes
return {
about: attestation.about,
key: formattedKey,
val: formattedValue,
} as const
})
return prepareWriteContract({
address: contractAddress,
abi,
functionName: 'attest',
chainId,
args: [formattedAttestations],
})
}
import { createClient } from '@wagmi/core'
import { providers } from 'ethers'
import { expect, describe, it } from 'vitest'
import { readAttestation } from './readAttestation'
const creator = '0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3'
const about = '0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5'
const key = 'optimist.base-uri'
const dataType = 'string'
const provider = new providers.JsonRpcProvider({
url: 'http://localhost:8545',
})
createClient({
provider,
})
describe(readAttestation.name, () => {
it('should return the attestation from attestation station', async () => {
const result = await readAttestation(creator, about, key, dataType)
expect(result).toMatchInlineSnapshot(
'"https://assets.optimism.io/4a609661-6774-441f-9fdb-453fdbb89931-bucket/optimist-nft/attributes"'
)
})
it('should throw an error if key is longer than 32 bytes', async () => {
await expect(
readAttestation(
creator,
about,
'this is a key that is way longer than 32 bytes so this key should throw an error matching the inline snapshot',
dataType
)
).rejects.toThrowErrorMatchingInlineSnapshot(
'"Key is longer than the max length of 32 for attestation keys"'
)
})
})
import type { Address } from '@wagmi/core'
import { DataTypeOption, DEFAULT_DATA_TYPE } from '../types/DataTypeOption'
import { readAttestations } from './readAttestations'
/**
* reads attestation from the attestation station contract
*
* @param attestationRead - the parameters for reading an attestation
* @returns attestation result
* @throws Error if key is longer than 32 bytes
* @example
* const attestation = await readAttestation(
* {
* creator: creatorAddress,
* about: aboutAddress,
* key: 'my_key',
* },
*/
export const readAttestation = async (
creator: Address,
about: Address,
key: string,
dataType: DataTypeOption = DEFAULT_DATA_TYPE,
contractAddress: Address = '0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
) => {
const [result] = await readAttestations({
creator,
about,
key,
dataType,
contractAddress,
})
return result
}
import { createClient } from '@wagmi/core'
import { providers } from 'ethers'
import { expect, describe, it } from 'vitest'
import { readAttestation } from './readAttestation'
import { readAttestations } from './readAttestations'
const creator = '0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3'
const about = '0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5'
const key = 'optimist.base-uri'
const provider = new providers.JsonRpcProvider({
url: 'http://localhost:8545',
})
createClient({
provider,
})
describe(readAttestation.name, () => {
it('should return attestations from attestation station', async () => {
const dataType = 'string'
const result = await readAttestations(
{
creator,
about,
key,
dataType,
},
{
creator,
about,
key,
dataType: 'bool',
},
{
creator,
about,
key,
dataType: 'bytes',
},
{
creator,
about,
key,
dataType: 'number',
}
)
expect(result).toMatchInlineSnapshot(
`
[
"https://assets.optimism.io/4a609661-6774-441f-9fdb-453fdbb89931-bucket/optimist-nft/attributes",
"true",
"0x68747470733a2f2f6173736574732e6f7074696d69736d2e696f2f34613630393636312d363737342d343431662d396664622d3435336664626238393933312d6275636b65742f6f7074696d6973742d6e66742f61747472696275746573",
"9665973469795080068873111198635018086067645613429821071805084917303478255842407465257371959707311987533859075426222329066766033171696373249109388415320911537042272090516917683029511016473045453921068327933733922308146003731827",
]
`
)
})
})
import { readContracts } from '@wagmi/core'
import { formatBytes32String } from 'ethers/lib/utils.js'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import type { AttestationReadParams } from '../types/AttestationReadParams'
import { DEFAULT_DATA_TYPE } from '../types/DataTypeOption'
import type { WagmiBytes } from '../types/WagmiBytes'
import { abi } from './abi'
import { parseAttestationBytes } from './parseAttestationBytes'
/**
* reads attestations from the attestation station contract
*
* @returns an array of attestation values
* @throws Error if key is longer than 32 bytes
* @example
* const attestations = await readAttestations(
* {
* creator: creatorAddress,
* about: aboutAddress,
* key: 'my_key',
* allowFailure: false,
* },
* {
* creator: creatorAddress2,
* about: aboutAddress2,
* key: 'my_key',
* dataType: 'number',
* contractAddress: '0x1234',
* allowFailure: false,
* },
* )
*/
export const readAttestations = async (
...attestationReads: Array<AttestationReadParams>
) => {
const calls = attestationReads.map((attestation) => {
const {
creator,
about,
key,
contractAddress = ATTESTATION_STATION_ADDRESS,
allowFailure = false,
} = attestation
if (key.length > 32) {
throw new Error(
'Key is longer than the max length of 32 for attestation keys'
)
}
return {
address: contractAddress,
abi,
functionName: 'attestations',
args: [creator, about, formatBytes32String(key) as WagmiBytes],
allowFailure,
} as const
})
const results = await readContracts({
contracts: calls,
})
return results.map((dataBytes, i) => {
const dataType = attestationReads[i].dataType ?? DEFAULT_DATA_TYPE
return parseAttestationBytes(dataBytes, dataType)
})
}
import { Address } from '@wagmi/core'
import { BigNumber } from 'ethers'
import { isAddress, isHexString, toUtf8Bytes } from 'ethers/lib/utils.js'
import { WagmiBytes } from '../types/WagmiBytes'
export const stringifyAttestationBytes = (
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()
}
if (typeof bytes === 'boolean') {
return bytes ? '0x1' : '0x0'
}
if (isAddress(bytes)) {
return bytes
}
if (isHexString(bytes)) {
return bytes
}
if (typeof bytes === 'string') {
return toUtf8Bytes(bytes)
}
throw new Error(`unrecognized bytes type ${bytes satisfies never}`)
}
import { writeContract } from '@wagmi/core'
import { describe, expect, it } from 'vitest'
import { writeAttestation } from './writeAttestation'
describe(writeAttestation.name, () => {
it('rexports writeContract from @wagmi/core', () => {
expect(writeAttestation).toBe(writeContract)
})
})
import { writeContract } from '@wagmi/core'
export { prepareWriteAttestation } from './prepareWriteAttestation'
export { abi } from './abi'
/**
* Writes an attestation to the blockchain
* Same function as `writeContract` from @wagmi/core
* To use first use prepareWriteContract
*
* @example
* const config = await prepareAttestation(about, key, value)
* const tx = await writeAttestation(config)
*/
export const writeAttestation = writeContract
This diff is collapsed.
import { vi } from 'vitest'
/**
* A test util for watching console output
*/
export const watchConsole = () => {
type Console = 'info' | 'log' | 'warn' | 'error'
const output: { [_ in Console | 'all']: string[] } = {
info: [],
log: [],
warn: [],
error: [],
all: [],
}
const handleOutput = (method: Console) => {
return (message: string) => {
output[method].push(message)
output.all.push(message)
}
}
return {
debug: console.debug,
info: vi.spyOn(console, 'info').mockImplementation(handleOutput('info')),
log: vi.spyOn(console, 'log').mockImplementation(handleOutput('log')),
warn: vi.spyOn(console, 'warn').mockImplementation(handleOutput('warn')),
error: vi.spyOn(console, 'error').mockImplementation(handleOutput('error')),
output,
get formatted() {
return output.all.join('\n')
},
}
}
import { Address } from '@wagmi/core'
import { DataTypeOption } from './DataTypeOption'
/**
* The parameters for reading bulk attestations
*/
export interface AttestationReadParams {
creator: Address
about: Address
key: string
dataType?: DataTypeOption
contractAddress?: Address
allowFailure?: boolean
}
import { z } from 'zod'
/**
* @internal
* Default data type for attestations
*/
export const DEFAULT_DATA_TYPE = 'string' as const
/**
* Zod validator for the DataType type
* string | bytes | number | bool | address
*/
export const dataTypeOptionValidator = z
.union([
z.literal('string'),
z.literal('bytes'),
z.literal('number'),
z.literal('bool'),
z.literal('address'),
])
.optional()
.default('string').describe(`Zod validator for the DataType type
string | bytes | number | bool | address`)
/**
* Options for attestation data type
*/
export type DataTypeOption = z.infer<typeof dataTypeOptionValidator>
/**
* @internal
* WagmiBytes is a type that represents a hex string with a length of 32 bytes.
*/
export type WagmiBytes = `0x${string}`
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"composite": true,
"target": "es2017",
"lib": ["esnext"],
"skipLibCheck": true,
"strict": true,
"module": "commonjs",
"outDir": "build"
},
"include": ["src", "src/**/*.json"]
}
import { defineConfig } from 'tsup'
/**
* @see https://tsup.egoist.dev/
*/
export default defineConfig({
name: '@eth-optimism/atst',
/**
* This is also a cli app and tsup will automatically make the cli entrypoint executable
*
* @see https://tsup.egoist.dev/#building-cli-app
*/
entry: ['src/index.ts', 'src/cli.ts'],
outDir: 'dist',
target: 'es2021',
// will create a .js file for commonjs and a .cjs file for esm
format: ['esm', 'cjs'],
// don't generate .d.ts files. This is default but being explicit
dts: false,
splitting: false,
sourcemap: true,
// remove dist folder before building
clean: true,
})
import { defineConfig } from 'vitest/config'
/**
* @see https://vitejs.dev/config/
*/
export default defineConfig({
test: {
environment: 'jsdom',
testTimeout: 10000,
},
})
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.
...@@ -96,11 +96,11 @@ abstract contract ResourceMetering is Initializable { ...@@ -96,11 +96,11 @@ abstract contract ResourceMetering is Initializable {
// Update base fee by adding the base fee delta and clamp the resulting value between // Update base fee by adding the base fee delta and clamp the resulting value between
// min and max. // min and max.
int256 newBaseFee = Arithmetic.clamp( int256 newBaseFee = Arithmetic.clamp({
int256(uint256(params.prevBaseFee)) + baseFeeDelta, _value: int256(uint256(params.prevBaseFee)) + baseFeeDelta,
MINIMUM_BASE_FEE, _min: MINIMUM_BASE_FEE,
MAXIMUM_BASE_FEE _max: MAXIMUM_BASE_FEE
); });
// If we skipped more than one block, we also need to account for every empty block. // If we skipped more than one block, we also need to account for every empty block.
// Empty block means there was no demand for deposits in that block, so we should // Empty block means there was no demand for deposits in that block, so we should
...@@ -109,15 +109,15 @@ abstract contract ResourceMetering is Initializable { ...@@ -109,15 +109,15 @@ abstract contract ResourceMetering is Initializable {
// Update the base fee by repeatedly applying the exponent 1-(1/change_denominator) // Update the base fee by repeatedly applying the exponent 1-(1/change_denominator)
// blockDiff - 1 times. Simulates multiple empty blocks. Clamp the resulting value // blockDiff - 1 times. Simulates multiple empty blocks. Clamp the resulting value
// between min and max. // between min and max.
newBaseFee = Arithmetic.clamp( newBaseFee = Arithmetic.clamp({
Arithmetic.cdexp( _value: Arithmetic.cdexp({
newBaseFee, _coefficient: newBaseFee,
BASE_FEE_MAX_CHANGE_DENOMINATOR, _denominator: BASE_FEE_MAX_CHANGE_DENOMINATOR,
int256(blockDiff - 1) _exponent: int256(blockDiff - 1)
), }),
MINIMUM_BASE_FEE, _min: MINIMUM_BASE_FEE,
MAXIMUM_BASE_FEE _max: MAXIMUM_BASE_FEE
); });
} }
// Update new base fee, reset bought gas, and update block number. // Update new base fee, reset bought gas, and update block number.
...@@ -141,7 +141,7 @@ abstract contract ResourceMetering is Initializable { ...@@ -141,7 +141,7 @@ abstract contract ResourceMetering is Initializable {
// division by zero for L1s that don't support 1559 or to avoid excessive gas burns during // division by zero for L1s that don't support 1559 or to avoid excessive gas burns during
// periods of extremely low L1 demand. One-day average gas fee hasn't dipped below 1 gwei // periods of extremely low L1 demand. One-day average gas fee hasn't dipped below 1 gwei
// during any 1 day period in the last 5 years, so should be fine. // during any 1 day period in the last 5 years, so should be fine.
uint256 gasCost = resourceCost / Math.max(block.basefee, 1000000000); uint256 gasCost = resourceCost / Math.max(block.basefee, 1 gwei);
// Give the user a refund based on the amount of gas they used to do all of the work up to // Give the user a refund based on the amount of gas they used to do all of the work up to
// this point. Since we're at the end of the modifier, this should be pretty accurate. Acts // this point. Since we're at the end of the modifier, this should be pretty accurate. Acts
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol";
import { AddressManager } from "../legacy/AddressManager.sol";
import { ResolvedDelegateProxy } from "../legacy/ResolvedDelegateProxy.sol";
contract ResolvedDelegateProxy_Test is Test {
AddressManager internal addressManager;
SimpleImplementation internal impl;
SimpleImplementation internal proxy;
function setUp() public {
// Set up the address manager.
addressManager = new AddressManager();
impl = new SimpleImplementation();
addressManager.setAddress("SimpleImplementation", address(impl));
// Set up the proxy.
proxy = SimpleImplementation(
address(new ResolvedDelegateProxy(addressManager, "SimpleImplementation"))
);
}
// Tests that the proxy properly bubbles up returndata when the delegatecall succeeds.
function testFuzz_fallback_delegateCallFoo_succeeds(uint256 x) public {
vm.expectCall(address(impl), abi.encodeWithSelector(impl.foo.selector, x));
assertEq(proxy.foo(x), x);
}
// Tests that the proxy properly bubbles up returndata when the delegatecall reverts.
function test_fallback_delegateCallBar_reverts() public {
vm.expectRevert("SimpleImplementation: revert");
vm.expectCall(address(impl), abi.encodeWithSelector(impl.bar.selector));
proxy.bar();
}
// Tests that the proxy fallback reverts as expected if the implementation within the
// address manager is not set.
function test_fallback_addressManagerNotSet_reverts() public {
AddressManager am = new AddressManager();
SimpleImplementation p = SimpleImplementation(
address(new ResolvedDelegateProxy(am, "SimpleImplementation"))
);
vm.expectRevert("ResolvedDelegateProxy: target address must be initialized");
p.foo(0);
}
}
contract SimpleImplementation {
function foo(uint256 _x) public pure returns (uint256) {
return _x;
}
function bar() public pure {
revert("SimpleImplementation: revert");
}
}
...@@ -68,7 +68,7 @@ contract AttestationStation is Semver { ...@@ -68,7 +68,7 @@ contract AttestationStation is Semver {
/** /**
* @notice Allows anyone to create attestations. * @notice Allows anyone to create attestations.
* *
* @param _attestations An array of attestation data. * @param _attestations An array of AttestationData structs.
*/ */
function attest(AttestationData[] calldata _attestations) external { function attest(AttestationData[] calldata _attestations) external {
uint256 length = _attestations.length; uint256 length = _attestations.length;
......
This diff is collapsed.
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