Commit d9d9bdbd authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

Merge pull request #5973 from ethereum-optimism/willc/nukeatst

chore(atst): Nuke it
parents 7f05e976 e11e0b72
...@@ -80,7 +80,6 @@ jobs: ...@@ -80,7 +80,6 @@ jobs:
paths: paths:
- "node_modules" - "node_modules"
- "packages/actor-tests/node_modules" - "packages/actor-tests/node_modules"
- "packages/atst/node_modules"
- "packages/balance-monitor/node_modules" - "packages/balance-monitor/node_modules"
- "packages/chain-mon/node_modules" - "packages/chain-mon/node_modules"
- "packages/common-ts/node_modules" - "packages/common-ts/node_modules"
...@@ -675,46 +674,6 @@ jobs: ...@@ -675,46 +674,6 @@ jobs:
command: npx depcheck command: npx depcheck
working_directory: packages/sdk working_directory: packages/sdk
atst-tests:
docker:
- image: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:latest
resource_class: large
steps:
- checkout
- attach_workspace: { at: '.' }
- check-changed:
patterns: atst,contracts-periphery
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-v2-{{ checksum "yarn.lock" }}
- run:
name: anvil
background: true
command: anvil --fork-url $ANVIL_L2_FORK_URL_MAINNET --fork-block-number 92093723
- run:
name: build
command: yarn build
working_directory: packages/atst
- run:
name: typecheck
command: yarn typecheck
working_directory: packages/atst
- run:
name: lint
command: yarn lint:check
working_directory: packages/atst
- run:
name: make sure anvil is up
command: npx wait-on tcp:8545 && cast block-number --rpc-url http://localhost:8545
- run:
name: test
command: yarn test
no_output_timeout: 5m
working_directory: packages/atst
environment:
CI: true
go-lint: go-lint:
parameters: parameters:
module: module:
...@@ -1157,9 +1116,6 @@ workflows: ...@@ -1157,9 +1116,6 @@ workflows:
- op-bindings-build: - op-bindings-build:
requires: requires:
- yarn-monorepo - yarn-monorepo
- atst-tests:
requires:
- yarn-monorepo
- js-lint-test: - js-lint-test:
name: actor-tests-tests name: actor-tests-tests
coverage_flag: actor-tests-tests coverage_flag: actor-tests-tests
......
{
"extends": ["../../.eslintrc.js"]
}
node_modules/
dist/
coverage/
# @eth-optimism/atst
## 0.2.0
### Minor Changes
- dcd13eec1: Update readAttestations and prepareWriteAttestation to handle keys longer than 32 bytes
- 9fd5be8e2: Remove broken allowFailures as option
- 3f4a43542: Move react api to @eth-optimism/atst/react so react isn't required to run the core sdk
- 71727eae9: Fix main and module in atst package.json
- 3d5f26c49: Deprecate parseAttestationBytes and createRawKey in favor for createKey, createValue
### Patch Changes
- 68bbe48b6: Update docs
- 6fea2f2db: Fixed bug with atst not defaulting to currently connected chain
## 0.1.0
### Minor Changes
- a312af15d: Make type parsing more intuitive
- 82a033fed: Fix string type that should be `0x${string}`
### Patch Changes
- 11bb01851: Add new atst package
- 7c37d262a: Release ATST
(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 / javascript sdk and cli around AttestationStation
**Visit [Docs](https://community.optimism.io/docs/governance/attestation-station/) for general documentation on AttestationStation.**
## Getting started
Install
```bash
npm install @eth-optimism/atst wagmi @wagmi/core ethers@5.7.0
```
## atst typescript/javascript sdk
The typescript sdk provides a clean [wagmi](https://wagmi.sh/) based interface for reading and writing to AttestationStation.
**See [sdk docs](https://github.com/ethereum-optimism/optimism/blob/develop/packages/atst/docs/sdk.md) for usage instructions.**
## atst cli
The cli provides a convenient cli for interacting with the AttestationStation contract
![preview](./assets/preview.gif)
**See [cli docs](https://github.com/ethereum-optimism/optimism/blob/develop/packages/atst/docs/cli.md) for usage instructions.**
## React API
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 AttestationStation.
Use `createKey` and `createValue` to convert your raw keys and values into bytes that can be used in AttestationStation contract calls
Use `parseString`, `parseBool`, `parseAddress` and `parseNumber` to convert values returned by AttestationStation to their correct data type.
For convenience we also [export the hooks here](https://github.com/ethereum-optimism/optimism/blob/develop/packages/atst/src/index.ts):
- `useAttestationStationAttestation` - Reads attestations with useContractRead
- `useAttestationStationVersion` - Reads attestation version
- `useAttestationStationAttest` - Wraps useContractWrite with AttestationStation abi calling attest
- `usePrepareAttestationStationAttest` - Wraps usePrepare with AttestationStation abi calling attest
- `useAttestationStationAttestationCreatedEvent` - Wraps useContractEvents for Created events
Also some more hooks exported by the cli but these are likely the only ones you need.
## Contributing
Please see our [contributing.md](https://github.com/ethereum-optimism/optimism/blob/develop/CONTRIBUTING.md). No contribution is too small.
Having your contribution denied feels bad.
Please consider [opening an issue](https://github.com/ethereum-optimism/optimism/issues) before adding any new features or apis.
## Getting help
If you have any problems, these resources could help you:
- [sdk documentation](https://github.com/ethereum-optimism/optimism/blob/develop/packages/atst/docs/sdk.md)
- [cli documentation](https://github.com/ethereum-optimism/optimism/blob/develop/packages/atst/docs/cli.md)
- [Optimism Discord](https://discord.gg/optimism)
- [Telegram group](https://t.me/+zwpJ8Ohqgl8yNjNh)
# Assets
## preview.gif
A gif preview of using the cli
## preview.tape
The script to record the preview.gif with [vhs](https://github.com/charmbracelet/vhs)
To execute:
1. [Download vhs](https://github.com/charmbracelet/vhs)
2. Install the local version of atst
```bash
npm uninstall @eth-optimism/atst -g && npm i . -g && atst --version
```
3. Start anvil
```bash
anvil --fork-url https://mainnet.optimism.io
```
4. Record tape vhs < assets/preview.tape
```bash
vhs < assets/preview.tape
```
5. The tape will be outputted to `assets/preview.gif`
# VHS File source
# https://github.com/charmbracelet/vhs
#
# Output:
# Output <path>.gif Create a GIF output at the given <path>
# Output <path>.mp4 Create an MP4 output at the given <path>
# Output <path>.webm Create a WebM output at the given <path>
#
# Settings:
# Set FontSize <number> Set the font size of the terminal
# Set FontFamily <string> Set the font family of the terminal
# Set Height <number> Set the height of the terminal
# Set Width <number> Set the width of the terminal
# Set LetterSpacing <float> Set the font letter spacing (tracking)
# Set LineHeight <float> Set the font line height
# Set Theme <string> Set the theme of the terminal (JSON)
# Set Padding <number> Set the padding of the terminal
# Set Framerate <number> Set the framerate of the recording
# Set PlaybackSpeed <float> Set the playback speed of the recording
#
# Sleep:
# Sleep <time> Sleep for a set amount of <time> in seconds
#
# Type:
# Type[@<time>] "<characters>" Type <characters> into the terminal with a
# <time> delay between each character
#
# Keys:
# Backspace[@<time>] [number] Press the Backspace key
# Down[@<time>] [number] Press the Down key
# Enter[@<time>] [number] Press the Enter key
# Space[@<time>] [number] Press the Space key
# Tab[@<time>] [number] Press the Tab key
# Left[@<time>] [number] Press the Left Arrow key
# Right[@<time>] [number] Press the Right Arrow key
# Up[@<time>] [number] Press the Up Arrow key
# Down[@<time>] [number] Press the Down Arrow key
# Ctrl+<key> Press the Control key + <key> (e.g. Ctrl+C)
#
# Display:
# Hide Hide the subsequent commands from the output
# Show Show the subsequent commands in the output
Output assets/preview.gif
Set FontSize 16
Set Width 1920
Set Height 1080
Type "atst write --key attitude --about 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --value 'feeling very optimistic' --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545"
Enter
Sleep 2000ms
Type "atst read --key attitude --about 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --creator 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://localhost:8545"
Enter
Sleep 2000ms
Type "atst write --key impress-level --about 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --value 10 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545"
Enter
Sleep 2000ms
Type "atst read --key impress-level --about 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --creator 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://localhost:8545"
Enter
Sleep 2000ms
Type "atst --help"
Enter
Sleep 2000ms
# atst cli docs
![preview](../assets/preview.gif)
## 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
```
### General options
- `-h`, `--help` Display help message
- `-v`, `--version` Display version number
### Read
- `--creator <address>` Address of the creator of the attestation
- `--about <address>` Address of the subject of the attestation
- `--key <string>` Key of the attestation either as string or hex number
- `[--data-type <string>]` 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 help message
Example:
```bash
npx atst read --key "optimist.base-uri" --about 0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5 \
--creator 0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3
```
### Write
- `--private-key <string>` Private key of the creator of the attestation
- `[--data-type <string>]` The DataType type `string` | `bytes` | `number` | `bool` | `address` (default: `string`)
- `--about <address>` Address of the subject of the attestation
- `--key <address>` 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
Example:
```bash
npx atst write --key "optimist.base-uri" \
--about 0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5 \
--value "my attestation" \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--rpc-url http://goerli.optimism.io
```
# AttestationStation sdk docs
Typescript sdk for interacting with the ATST based on [@wagmi/core](https://wagmi.sh/core/getting-started)
## Table of content
- [Installation](#installation)
- [Basic usage](#basic-usage)
- [Setup](#setup)
- [Reading an attestation](#reading-an-attestation)
- [Reading multiple attestation](#reading-multiple-attestations)
- [Writing an attestation](#writing-an-attestation)
- [API](#api)
- [High level API](#high-level-api)
- [Low level API](#low-level-api)
- [React API](#react-api)
- [Tutorial](#tutorial)
## Installation
Install atst and its 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
### Setup
atst uses `@wagmi/core` under the hood.
[See their documentation for more information](https://wagmi.sh/core/getting-started).
```javascript
import ethers from "ethers"
const wagmiCore = await import("@wagmi/core")
const wagmiAlchemy = await import("@wagmi/core/providers/alchemy")
const wagmiChains = await import("@wagmi/core/chains")
const { chains, provider, webSocketProvider } = wagmiCore.configureChains(
[wagmiChains.optimismGoerli],
[wagmiAlchemy.alchemyProvider({ apiKey: process.env.ALCHEMY_API_KEY })],
)
wagmiCore.createClient({
provider,
webSocketProvider
})
```
### Reading an attestation
Here is an example of reading an attestation used by the optimist nft
```javascript
import { readAttestation } from '@eth-optimism/atst'
const creator = '0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3'
const about = '0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5'
const key = 'optimist.base-uri'
const str = await readAttestationString(creator, about, key)
console.log(attestation)
// https://assets.optimism.io/4a609661-6774-441f-9fdb-453fdbb89931-bucket/optimist-nft/attributes
```
### Reading multiple attestations
If reading more than one attestation you can use [`readAttestations`](#readattestations) to read them with [multicall](https://www.npmjs.com/package/ethereum-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/atst'
const preparedTx = await prepareWriteAttestation(about, key, 'hello world')
console.log(preparedTx.gasLimit)
await writeAttestation(preparedTx)
```
## API
### High level API
These functions are the easiest way to interact with the AttestationStation contract.
For a more detailed explanation, [see the tutorial](https://github.com/ethereum-optimism/optimism-tutorial/tree/main/ecosystem/attestation-station/using-sdk).
#### `getEvents`
Use `getEvents` to get attestation events using a provider and filters.
```typescript
const events = await getEvents({
creator,
about,
key,
value,
provider: new ethers.providers.JsonRpcProvider('http://localhost:8545'),
fromBlockOrBlockhash,
toBlock,
})
```
Set `key`, `about`, `creator`, or `value` to `null` to not filter that value.
#### `prepareWriteAttestation`
[Prepares](https://wagmi.sh/core/actions/prepareWriteContract) an attestation to be written.
This function creates the transaction data, estimates the gas cost, etc.
```typescript
const preparedTx = await prepareWriteAttestation(about, key, 'hello world')
console.log(preparedTx.gasFee)
```
**Return Value:** A prepared transaction, ready for [`writeAttestation`](#writeattestation).
#### `readAttestation`
[Reads](https://wagmi.sh/core/actions/readContract) and parses an attestation based on its data type.
```typescript
const attestation = await readAttestation(
creator, // Address: The creator of the attestation
about, // Address: The about topic of the attestation
key, // string: The key of the attestation
dataType, // Optional, the data type of the attestation, 'string' | 'bytes' | 'number' | 'bool' | 'address'
contractAddress // Optional address: the contract address of AttestationStation
)
```
**Return Value:** The value attested by the `creator` on the `about` address concerning the `key`, when interpreted as the `dataType`.
If there is no such attestation the result is zero, `false`, or an empty string.
#### `readAttestations`
Similar to `readAttestation` but reads multiple attestations at once.
Pass in a variadic amount of attestations to read.
The parameters are all the same structure type, the one shown below.
```typescript
const attestationList = await readAttestations({
creator: <creator address>,
about: <about address>,
key: <attestation key (string)>,
[dataType: <data type - 'string' | 'bytes' | 'number' | 'bool' | 'address'>,]
[contractAddress: <contract address, if not the default>]
}, {...}, {...})
```
**Return Value:** A list of values attested by the `creator` on the `about` address concerning the `key`, when interpreted as the `dataType`.
#### `writeAttestation`
[Writes the prepared tx](https://wagmi.sh/core/actions/writeContract).
```typescript
const preparedTx = await prepareWriteAttestation(about, key, 'hello world')
await writeAttestation(preparedTx)
```
### Low level API
These definitions allow you to communicate with AttestationStation, but are not as easy to use as the high level API.
#### `ATTESTATION_STATION_ADDRESS`
The deployment address for AttestationStation currently deployed with create2 on Optimism and Optimism Goerli `0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77`.
```typescript
import { ATTESTATION_STATION_ADDRESS } from '@eth-optimism/atst'
```
#### `abi`
The abi of the AttestationStation contract
```typescript
import { abi } from '@eth-optimism/atst'
```
#### `createKey`
`createKey` hashes keys longer than 31 bytes, because the atst key size is limited to 32 bytes.
```typescript
const key = await createKey(
'i.am.a.key.much.longer.than.32.bytes.long'
)
```
createKey will keep the key as is if it is shorter than 32 bytes and otherwise run it through keccak256.
#### `parseAddress`
Turn bytes into an address.
**Note:** `readAttestation` and `readAttestations` already do this for you.
This is only needed if talking to the contracts directly, or through a different library.
#### `parseBool`
Turn bytes into a boolean value.
**Note:** `readAttestation` and `readAttestations` already do this for you.
This is only needed if talking to the contracts directly, or through a different library.
#### `parseNumber`
Turn bytes into a number.
**Note:** `readAttestation` and `readAttestations` already do this for you.
This is only needed if talking to the contracts directly, or through a different library.
#### `parseString`
Turn bytes into a string.
**Note:** `readAttestation` and `readAttestations` already do this for you.
This is only needed if talking to the contracts directly, or through a different library.
#### `stringifyAttestationBytes`
Stringifys an attestation into raw bytes.
```typescript
const stringAttestation = stringifyAttestationBytes('hello world')
const numberAttestation = stringifyAttestationBytes(500)
const hexAttestation = stringifyAttestationBytes('0x1')
const bigNumberAttestation = stringifyAttestationBytes(
BigNumber.from('9999999999999999999999999')
)
```
**Note:** `writeAttestation` already does this for you so this is only needed if using a library other than `atst`.
### React API
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 AttestationStation.
Use `createKey` and `createValue` to convert your raw keys and values into bytes that can be used in AttestationStation contract calls.
Use `parseString`, `parseBool`, `parseAddress` and `parseNumber` to convert values returned by AttestationStation to their correct data type.
For convenience we also [export the hooks here](../src/react.ts):
- `useAttestationStationAttestation` - Reads attestations with useContractRead
- `useAttestationStationVersion` - Reads attestation version
- `useAttestationStationAttest` - Wraps useContractWrite with AttestationStation abi calling attest
- `usePrepareAttestationStationAttest` - Wraps usePrepare with AttestationStation abi calling attest
- `useAttestationStationAttestationCreatedEvent` - Wraps useContractEvents for Created events
## Tutorial
- [General atst tutorial](https://github.com/ethereum-optimism/optimism-tutorial/tree/main/ecosystem/attestation-station).
- [React atst starter](https://github.com/ethereum-optimism/optimism-starter).
{
"name": "@eth-optimism/atst",
"version": "0.2.0",
"type": "module",
"main": "dist/index.cjs",
"types": "src/index.ts",
"module": "dist/index.js",
"license": "MIT",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.js",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./react": {
"types": "./src/react.ts",
"default": "./dist/react.js",
"import": "./dist/react.js",
"require": "./dist/react.cjs"
}
},
"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"
},
"keywords": [
"react",
"hooks",
"eth",
"ethereum",
"dapps",
"web3",
"optimism",
"attestation"
]
}
# 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
## constants
Internal and external constants
## contracts
The attestation station contract
## lib
All library code for the sdk lives here
## test
Test helpers
## types
Zod and typscript types
// 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(
() =>
// note: private key is just the first testing address when running anvil
`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 spinner = logger.spinner()
spinner.start('Writing attestation...')
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 })
.then((res) => {
spinner.succeed('Attestation written!')
logger.info(`Attestation hash: ${res}`)
})
.catch((e) => {
logger.error(e)
spinner.fail('Attestation failed!')
})
})
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,
dataType: 'string',
})
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'
../../../contracts-periphery/contracts/universal/op-nft/AttestationStation.sol
\ No newline at end of file
// constants
export { ATTESTATION_STATION_ADDRESS } from './constants/attestationStationAddress'
// lib
export { encodeRawKey, createKey } from './lib/createKey'
export { createValue, stringifyAttestationBytes } from './lib/createValue'
export {
readAttestation,
readAttestationAddress,
readAttestationBool,
readAttestationNumber,
readAttestationString,
} from './lib/readAttestation'
export { readAttestations } from './lib/readAttestations'
export { getEvents } from './lib/getEvents'
export { prepareWriteAttestation } from './lib/prepareWriteAttestation'
export { prepareWriteAttestations } from './lib/prepareWriteAttestations'
export { writeAttestation } from './lib/writeAttestation'
export { abi } from './lib/abi'
export {
parseAttestationBytes,
parseAddress,
parseNumber,
parseBool,
parseString,
} from './lib/parseAttestationBytes'
// types
export type { AttestationCreatedEvent } from './types/AttestationCreatedEvent'
export type { AttestationReadParams } from './types/AttestationReadParams'
export type { DataTypeOption } from './types/DataTypeOption'
export type { WagmiBytes } from './types/WagmiBytes'
// 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 { describe, expect, it } from 'vitest'
import { encodeRawKey } from './createKey'
describe(encodeRawKey.name, () => {
it('should return just the raw key if it is less than 32 bytes', () => {
const rawKey = 'I am 32'
const encodedKey = encodeRawKey(rawKey)
expect(encodedKey).toMatchInlineSnapshot(
'"0x4920616d20333200000000000000000000000000000000000000000000000000"'
)
})
it('should return the keccak256 hash of the raw key if it is more than 32 bytes', () => {
const rawKey = 'I am way more than 32 bytes long I should be hashed'
const encodedKey = encodeRawKey(rawKey)
expect(encodedKey).toMatchInlineSnapshot(
'"0xc9d5d767710cc45f74c3a9a0c53dc44391a7951604c7ea3bd9116ccff406daff"'
)
})
})
import { ethers } from 'ethers'
import { WagmiBytes } from '../types/WagmiBytes'
/**
* Creates an attesation key from a raw string
* Converts to bytes32 if key is less than 32 bytes
* Hashes key if key is greater than 32 bytes
*/
export const createKey = (rawKey: string): WagmiBytes => {
if (rawKey.length < 32) {
return ethers.utils.formatBytes32String(rawKey) as WagmiBytes
}
const hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(rawKey))
return (hash.slice(0, 64) + 'ff') as WagmiBytes
}
/**
* @deprecated use createKey instead
* Will be removed in v1.0.0
*/
export const encodeRawKey: typeof createKey = (rawKey) => {
console.warn('encodeRawKey is deprecated, use createKey instead')
return createKey(rawKey)
}
import { Address } from '@wagmi/core'
import { BigNumber } from 'ethers'
import {
hexlify,
isAddress,
isHexString,
toUtf8Bytes,
} from 'ethers/lib/utils.js'
import { WagmiBytes } from '../types/WagmiBytes'
/**
* Turns a value into bytes to make an attestation
*
* @example
* createValue('hello world') // '0x68656c6c6f20776f726c64'
* createValue(123) // '0x7b'
* createValue(true) // '0x1'
* createValue(BigNumber.from(10)) // '0xa'
*/
export const createValue = (
bytes: WagmiBytes | string | Address | number | boolean | BigNumber
): WagmiBytes => {
bytes = bytes === '0x' ? '0x0' : bytes
if (BigNumber.isBigNumber(bytes)) {
return bytes.toHexString() as WagmiBytes
}
if (typeof bytes === 'number') {
return BigNumber.from(bytes).toHexString() as WagmiBytes
}
if (typeof bytes === 'boolean') {
return bytes ? '0x1' : '0x0'
}
if (isAddress(bytes)) {
return bytes
}
if (isHexString(bytes)) {
return bytes as WagmiBytes
}
if (typeof bytes === 'string') {
return hexlify(toUtf8Bytes(bytes)) as WagmiBytes
}
throw new Error(`unrecognized bytes type ${bytes satisfies never}`)
}
/**
* @deprecated use createValue instead
* Will be removed in v1.0.0
*/
export const stringifyAttestationBytes: typeof createValue = (bytes) => {
console.warn(
'stringifyAttestationBytes is deprecated, use createValue instead'
)
return createValue(bytes)
}
This diff is collapsed.
import { ethers } from 'ethers'
import type { Address } from '@wagmi/core'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { abi } from '../lib/abi'
import { AttestationCreatedEvent } from '../types/AttestationCreatedEvent'
import { encodeRawKey } from './createKey'
export const getEvents = async ({
creator = null,
about = null,
key = null,
value = null,
provider,
fromBlockOrBlockhash,
toBlock,
}: {
creator?: Address | null
about?: Address | null
key?: string | null
value?: string | null
provider: ethers.providers.JsonRpcProvider
fromBlockOrBlockhash?: ethers.providers.BlockTag | undefined
toBlock?: ethers.providers.BlockTag | undefined
}) => {
const contract = new ethers.Contract(
ATTESTATION_STATION_ADDRESS,
abi,
provider
)
return contract.queryFilter(
contract.filters.AttestationCreated(
creator,
about,
key && encodeRawKey(key),
value
),
fromBlockOrBlockhash,
toBlock
) as Promise<AttestationCreatedEvent[]>
}
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 {
parseNumber,
parseAddress,
parseBool,
parseString,
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')).toMatchInlineSnapshot(`
{
"hex": "0x7b",
"type": "BigNumber",
}
`)
})
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', () => {
expect(parseAttestationBytes('0x420', 'bytes')).toMatchInlineSnapshot(
'"0x420"'
)
expect(parseAttestationBytes('0x', 'string')).toMatchInlineSnapshot('""')
expect(parseAttestationBytes('0x0', 'string')).toMatchInlineSnapshot('""')
})
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)
})
})
describe('parseFoo', () => {
it('works for strings', () => {
const str = 'Hello World'
const bytes = BigNumber.from(toUtf8Bytes(str)).toHexString() as WagmiBytes
expect(parseString(bytes)).toBe(str)
expect(parseString('0x')).toMatchInlineSnapshot('""')
expect(parseString('0x0')).toMatchInlineSnapshot('""')
expect(parseString('0x0')).toMatchInlineSnapshot('""')
})
it('works for numbers', () => {
const num = 123
const bytes = BigNumber.from(num).toHexString() as WagmiBytes
expect(parseNumber(bytes)).toEqual(BigNumber.from(num))
expect(parseNumber('0x')).toEqual(BigNumber.from(0))
})
it('works for addresses', () => {
const addr = '0x1234567890123456789012345678901234567890'
const bytes = BigNumber.from(addr).toHexString() as WagmiBytes
expect(parseAddress(bytes)).toBe(addr)
})
it('works for booleans', () => {
const bytes = BigNumber.from(1).toHexString() as WagmiBytes
expect(parseBool(bytes)).toBe(true)
expect(parseBool('0x')).toBe(false)
expect(parseBool('0x0')).toBe(false)
expect(parseBool('0x00000')).toBe(false)
})
})
import { BigNumber } from 'ethers'
import { toUtf8String } from 'ethers/lib/utils.js'
import type { Address } from '@wagmi/core'
import type { DataTypeOption } from '../types/DataTypeOption'
import type { WagmiBytes } from '../types/WagmiBytes'
import { ParseBytesReturn } from '../types/ParseBytesReturn'
/**
* Parses a string attestion
*/
export const parseString = (rawAttestation: WagmiBytes): string => {
rawAttestation = rawAttestation === '0x0' ? '0x' : rawAttestation
return rawAttestation ? toUtf8String(rawAttestation) : ''
}
/**
* Parses a boolean attestion
*/
export const parseBool = (rawAttestation: WagmiBytes): boolean => {
rawAttestation = rawAttestation === '0x' ? '0x0' : rawAttestation
return rawAttestation ? BigNumber.from(rawAttestation).gt(0) : false
}
/**
* Parses a number attestion
*/
export const parseNumber = (rawAttestation: WagmiBytes): BigNumber => {
rawAttestation = rawAttestation === '0x' ? '0x0' : rawAttestation
return rawAttestation ? BigNumber.from(rawAttestation) : BigNumber.from(0)
}
/**
* Parses a address attestion
*/
export const parseAddress = (rawAttestation: WagmiBytes): Address => {
rawAttestation = rawAttestation === '0x' ? '0x0' : rawAttestation
return rawAttestation
? (BigNumber.from(rawAttestation).toHexString() as Address)
: '0x0000000000000000000000000000000000000000'
}
/**
* @deprecated use parseString, parseBool, parseNumber, or parseAddress instead
* Will be removed in v1.0.0
* @internal
* Parses a raw attestation
*/
export const parseAttestationBytes = <TDataType extends DataTypeOption>(
attestationBytes: WagmiBytes,
dataType: TDataType
): ParseBytesReturn<TDataType> => {
if (dataType === 'bytes') {
return attestationBytes as ParseBytesReturn<TDataType>
}
if (dataType === 'number') {
return parseNumber(attestationBytes) as ParseBytesReturn<TDataType>
}
if (dataType === 'address') {
return parseAddress(attestationBytes) as ParseBytesReturn<TDataType>
}
if (dataType === 'bool') {
return parseBool(attestationBytes) as ParseBytesReturn<TDataType>
}
if (dataType === 'string') {
return parseString(attestationBytes) as ParseBytesReturn<TDataType>
}
console.warn(`unrecognized dataType ${dataType satisfies never}`)
return attestationBytes as never
}
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('undefined')
expect(result.functionName).toMatchInlineSnapshot('"attest"')
expect(result.mode).toMatchInlineSnapshot('"prepared"')
expect(result.request.gasLimit).toMatchInlineSnapshot(`
{
"hex": "0xd6c9",
"type": "BigNumber",
}
`)
})
it('should work for key longer than 32 bytes', async () => {
const dataType = 'string'
expect(
await 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
)
).toMatchInlineSnapshot('""')
})
})
import { Address, prepareWriteContract } from '@wagmi/core'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { WagmiBytes } from '../types/WagmiBytes'
import { abi } from './abi'
import { createKey } from './createKey'
import { createValue } from './createValue'
export const prepareWriteAttestation = async (
about: Address,
key: string,
value: string | WagmiBytes | number | boolean,
chainId: number | undefined = undefined,
contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => {
const formattedKey = createKey(key) as WagmiBytes
return prepareWriteContract({
address: contractAddress,
abi,
functionName: 'attest',
chainId,
args: [about, formattedKey, createValue(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('undefined')
expect(result.functionName).toMatchInlineSnapshot('"attest"')
expect(result.mode).toMatchInlineSnapshot('"prepared"')
expect(result.request.gasLimit).toMatchInlineSnapshot(`
{
"hex": "0xd9ce",
"type": "BigNumber",
}
`)
})
it('should work if key is longer than 32 bytes', async () => {
const dataType = 'string'
expect(
await 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
)
).toMatchInlineSnapshot('""')
})
})
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 { createValue } from './createValue'
type Attestation = {
about: Address
key: string
value: string | WagmiBytes | number | boolean
}
export const prepareWriteAttestations = async (
attestations: Attestation[],
chainId: number | undefined = undefined,
contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => {
const formattedAttestations = attestations.map((attestation) => {
let formattedKey: WagmiBytes
try {
formattedKey = formatBytes32String(attestation.key) as WagmiBytes
} catch (e) {
console.error(e)
throw new Error(
`key is longer than 32 bytes: ${attestation.key}. Try using a shorter key or using 'encodeRawKey' to encode the key into 32 bytes first`
)
}
const formattedValue = createValue(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 work if key is longer than 32 bytes', async () => {
expect(
await 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
)
).toMatchInlineSnapshot('""')
})
})
import type { Address } from '@wagmi/core'
import { BigNumber } from 'ethers'
import { DataTypeOption } from '../types/DataTypeOption'
import { ParseBytesReturn } from '../types/ParseBytesReturn'
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 <TDataType extends DataTypeOption>(
/**
* Creator of the attestation
*/
creator: Address,
/**
* Address the attestation is about
*/
about: Address,
/**
* Key of the attestation
*/
key: string,
/**
* Data type of the attestation
* string | bool | number | address | bytes
*
* @defaults 'string'
*/
dataType: TDataType,
/**
* Attestation address
* defaults to the official Optimism attestation station determistic deploy address
*
* @defaults '0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
*/
contractAddress: Address = '0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
): Promise<ParseBytesReturn<TDataType>> => {
const [result] = await readAttestations({
creator,
about,
key,
contractAddress,
dataType,
})
return result as ParseBytesReturn<TDataType>
}
/**
* Reads a string attestation
*/
export const readAttestationString = (
/**
* Creator of the attestation
*/
creator: Address,
/**
* Address the attestation is about
*/
about: Address,
/**
* Key of the attestation
*/
key: string,
/**
* Attestation address
* defaults to the official Optimism attestation station determistic deploy address
*
* @defaults '0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
*/
contractAddress: Address = '0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
) => {
return readAttestation(
creator,
about,
key,
'string',
contractAddress
) as Promise<string>
}
export const readAttestationBool = (
/**
* Creator of the attestation
*/
creator: Address,
/**
* Address the attestation is about
*/
about: Address,
/**
* Key of the attestation
*/
key: string,
/**
* Attestation address
* defaults to the official Optimism attestation station determistic deploy address
*
* @defaults '0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
*/
contractAddress: Address = '0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
) => {
return readAttestation(
/**
* Creator of the attestation
*/
creator,
about,
key,
'bool',
contractAddress
) as Promise<boolean>
}
export const readAttestationNumber = (
/**
* Creator of the attestation
*/
creator: Address,
/**
* Address the attestation is about
*/
about: Address,
/**
* Key of the attestation
*/
key: string,
/**
* Attestation address
* defaults to the official Optimism attestation station determistic deploy address
*
* @defaults '0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
*/
contractAddress: Address = '0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
) => {
return readAttestation(
creator,
about,
key,
'number',
contractAddress
) as Promise<BigNumber>
}
export const readAttestationAddress = (
/**
* Creator of the attestation
*/
creator: Address,
/**
* Address the attestation is about
*/
about: Address,
/**
* Key of the attestation
*/
key: string,
/**
* Attestation address
* defaults to the official Optimism attestation station determistic deploy address
*
* @defaults '0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
*/
contractAddress: Address = '0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
) => {
return readAttestation(
creator,
about,
key,
'address',
contractAddress
) as Promise<Address>
}
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",
{
"hex": "0x68747470733a2f2f6173736574732e6f7074696d69736d2e696f2f34613630393636312d363737342d343431662d396664622d3435336664626238393933312d6275636b65742f6f7074696d6973742d6e66742f61747472696275746573",
"type": "BigNumber",
},
]
`
)
})
})
import { readContracts } from '@wagmi/core'
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 { createKey } from './createKey'
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',
* },
* {
* creator: creatorAddress2,
* about: aboutAddress2,
* key: 'my_key',
* dataType: 'number',
* contractAddress: '0x1234',
* },
* )
*/
export const readAttestations = async (
...attestationReads: Array<AttestationReadParams>
) => {
const calls = attestationReads.map((attestation) => {
const {
creator,
about,
key,
contractAddress = ATTESTATION_STATION_ADDRESS,
} = attestation
return {
address: contractAddress,
abi,
functionName: 'attestations',
args: [creator, about, createKey(key) as WagmiBytes],
} 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 { 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 type { Event } from 'ethers'
interface TypedEvent<TArgsArray extends Array<any> = any, TArgsObject = any>
extends Event {
args: TArgsArray & TArgsObject
}
export interface AttestationCreatedEventObject {
creator: string
about: string
key: string
val: string
}
export type AttestationCreatedEvent = TypedEvent<
[string, string, string, string],
AttestationCreatedEventObject
>
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
chainId?: number
}
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>
import { BigNumber } from 'ethers'
import type { Address } from '@wagmi/core'
import { DataTypeOption } from './DataTypeOption'
import { WagmiBytes } from './WagmiBytes'
/**
* @internal
* Returns the correct typescript type of a DataOption
*/
export type ParseBytesReturn<T extends DataTypeOption> = T extends 'bytes'
? WagmiBytes
: T extends 'number'
? BigNumber
: T extends 'address'
? Address
: T extends 'bool'
? boolean
: T extends 'string'
? string
: never
/**
* @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', 'src/react.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'
export const ATTESTATION_STATION_ADDRESS =
'0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77'
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