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

Merge branch 'develop' into 02-25-documentation

parents 4feb0824 628324b8
---
'@eth-optimism/atst': minor
---
Make type parsing more intuitive
---
'@eth-optimism/chain-mon': minor
---
Added a withdrawal monitoring service
......@@ -32,6 +32,7 @@ require (
github.com/urfave/cli v1.22.9
github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa
golang.org/x/crypto v0.6.0
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
golang.org/x/term v0.5.0
)
......@@ -174,7 +175,6 @@ require (
go.uber.org/fx v1.19.1 // indirect
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sync v0.1.0 // indirect
......
......@@ -19,6 +19,7 @@ import (
mocknet "github.com/libp2p/go-libp2p/p2p/net/mock"
tswarm "github.com/libp2p/go-libp2p/p2p/net/swarm/testing"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/enode"
......@@ -314,19 +315,22 @@ func TestDiscovery(t *testing.T) {
// B and C don't know each other yet, but both have A as a bootnode.
// It should only be a matter of time for them to connect, if they discover each other via A.
var firstPeersOfB []peer.ID
for i := 0; i < 2; i++ {
timeout := time.After(time.Second * 10)
var peersOfB []peer.ID
// B should be connected to the bootnode (A) it used (it's a valid optimism node to connect to here)
// C should also be connected, although this one might take more time to discover
for !slices.Contains(peersOfB, hostA.ID()) || !slices.Contains(peersOfB, hostC.ID()) {
select {
case <-time.After(time.Second * 30):
t.Fatal("failed to get connection to B in time")
case <-timeout:
var peers []string
for _, id := range peersOfB {
peers = append(peers, id.String())
}
t.Fatalf("timeout reached - expected host A: %v and host C: %v to be in %v", hostA.ID().String(), hostC.ID().String(), peers)
case c := <-connsB:
firstPeersOfB = append(firstPeersOfB, c.RemotePeer())
peersOfB = append(peersOfB, c.RemotePeer())
}
}
// B should be connected to the bootnode it used (it's a valid optimism node to connect to here)
require.Contains(t, firstPeersOfB, hostA.ID())
// C should be connected, although this one might take more time to discover
require.Contains(t, firstPeersOfB, hostC.ID())
}
// Most tests should use mocknets instead of using the actual local host network
......
......@@ -203,6 +203,21 @@ const attestation = parseAttestationBytes(
)
```
### attestation keys
Attestation keys are limited to 32 bytes. To support keys longer than 32 bytes, you can use the `encodeRawKey` function
```typescript
const key = await encodeRawKey(
about,
key,
'i.am.a.key.much.longer.than.32.bytes.long'
)
await writeAttestation(preparedTx)
```
encodeRawKey will keep the key as is if it is shorter than 32 bytes and otherwise run it through kekkak256
### prepareWriteAttestation
[Prepares](https://wagmi.sh/core/actions/prepareWriteContract) an attestation to be written.
......@@ -235,3 +250,7 @@ const bigNumberAttestation = stringifyAttestationBytes(
const preparedTx = await prepareWriteAttestation(about, key, 'hello world')
await writeAttestation(preparedTx)
```
## Tutorial
For a tutorial on using the attestation station in general, see out tutorial as well as other Optimism related tutorials in our [optimism-tutorial](https://github.com/ethereum-optimism/optimism-tutorial/tree/main/ecosystem/attestation-station#key-values) repo
......@@ -25,6 +25,7 @@ describe(`cli:${write.name}`, () => {
value,
contract: ATTESTATION_STATION_ADDRESS,
rpcUrl,
dataType: 'string',
})
expect(txHash.startsWith('0x')).toBe(true)
......
// constants
export { ATTESTATION_STATION_ADDRESS } from './constants/attestationStationAddress'
// lib
export { readAttestation } from './lib/readAttestation'
export { encodeRawKey } from './lib/encodeRawKey'
export {
readAttestation,
readAttestationAddress,
readAttestationBool,
readAttestationNumber,
readAttestationString,
} 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'
export {
parseAttestationBytes,
parseAddress,
parseNumber,
parseBool,
parseString,
} from './lib/parseAttestationBytes'
// types
export type { AttestationReadParams } from './types/AttestationReadParams'
export type { WagmiBytes } from './types/WagmiBytes'
export type { DataTypeOption } from './types/DataTypeOption'
export type { WagmiBytes } from './types/WagmiBytes'
import { describe, expect, it } from 'vitest'
import { encodeRawKey } from './encodeRawKey'
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'
export const encodeRawKey = (rawKey: string) => {
if (rawKey.length < 32) {
return ethers.utils.formatBytes32String(rawKey)
}
const hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(rawKey))
return hash.slice(0, 64) + 'ff'
}
......@@ -3,7 +3,13 @@ import { toUtf8Bytes } from 'ethers/lib/utils.js'
import { expect, describe, it } from 'vitest'
import { WagmiBytes } from '../types/WagmiBytes'
import { parseAttestationBytes } from './parseAttestationBytes'
import {
parseNumber,
parseAddress,
parseBool,
parseString,
parseAttestationBytes,
} from './parseAttestationBytes'
describe(parseAttestationBytes.name, () => {
it('works for strings', () => {
......@@ -15,7 +21,12 @@ describe(parseAttestationBytes.name, () => {
it('works for numbers', () => {
const num = 123
const bytes = BigNumber.from(num).toHexString() as WagmiBytes
expect(parseAttestationBytes(bytes, 'number')).toBe(num.toString())
expect(parseAttestationBytes(bytes, 'number')).toMatchInlineSnapshot(`
{
"hex": "0x7b",
"type": "BigNumber",
}
`)
})
it('works for addresses', () => {
......@@ -26,7 +37,7 @@ describe(parseAttestationBytes.name, () => {
it('works for booleans', () => {
const bytes = BigNumber.from(1).toHexString() as WagmiBytes
expect(parseAttestationBytes(bytes, 'bool')).toBe('true')
expect(parseAttestationBytes(bytes, 'bool')).toBe(true)
})
it('should work for raw bytes', () => {
......@@ -40,3 +51,28 @@ describe(parseAttestationBytes.name, () => {
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)
})
it('works for numbers', () => {
const num = 123
const bytes = BigNumber.from(num).toHexString() as WagmiBytes
expect(parseNumber(bytes)).toEqual(BigNumber.from(num))
})
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)
})
})
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'
export const parseAttestationBytes = (
/**
* Parses a string attestion
*/
export const parseString = (rawAttestation: WagmiBytes): string => {
rawAttestation = rawAttestation === '0x' ? '0x0' : 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'
}
/**
* Parses a raw attestation
*/
export const parseAttestationBytes = <TDataType extends DataTypeOption>(
attestationBytes: WagmiBytes,
dataType: DataTypeOption
) => {
dataType: TDataType
): ParseBytesReturn<TDataType> => {
if (dataType === 'bytes') {
return attestationBytes
return attestationBytes as ParseBytesReturn<TDataType>
}
if (dataType === 'number') {
return BigNumber.from(attestationBytes).toString()
return parseNumber(attestationBytes) as ParseBytesReturn<TDataType>
}
if (dataType === 'address') {
return BigNumber.from(attestationBytes).toHexString()
return parseAddress(attestationBytes) as ParseBytesReturn<TDataType>
}
if (dataType === 'bool') {
return BigNumber.from(attestationBytes).gt(0) ? 'true' : 'false'
return parseBool(attestationBytes) as ParseBytesReturn<TDataType>
}
if (dataType === 'string') {
return attestationBytes && toUtf8String(attestationBytes)
return parseString(attestationBytes) as ParseBytesReturn<TDataType>
}
console.warn(`unrecognized dataType ${dataType satisfies never}`)
return attestationBytes
return attestationBytes as never
}
......@@ -13,7 +13,15 @@ export const prepareWriteAttestation = async (
chainId = 10,
contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => {
const formattedKey = formatBytes32String(key) as WagmiBytes
let formattedKey: WagmiBytes
try {
formattedKey = formatBytes32String(key) as WagmiBytes
} catch (e) {
console.error(e)
throw new Error(
`key is longer than 32 bytes: ${key}. Try using a shorter key or using 'encodeRawKey' to encode the key into 32 bytes first`
)
}
return prepareWriteContract({
address: contractAddress,
abi,
......
......@@ -18,7 +18,15 @@ export const prepareWriteAttestations = async (
contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => {
const formattedAttestations = attestations.map((attestation) => {
const formattedKey = formatBytes32String(attestation.key) as WagmiBytes
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 = stringifyAttestationBytes(
attestation.value
) as WagmiBytes
......
......@@ -39,3 +39,4 @@ describe(readAttestation.name, () => {
)
})
})
import type { Address } from '@wagmi/core'
import { BigNumber } from 'ethers'
import { DataTypeOption, DEFAULT_DATA_TYPE } from '../types/DataTypeOption'
import { DataTypeOption } from '../types/DataTypeOption'
import { ParseBytesReturn } from '../types/ParseBytesReturn'
import { readAttestations } from './readAttestations'
/**
......@@ -17,19 +19,166 @@ import { readAttestations } from './readAttestations'
* key: 'my_key',
* },
*/
export const readAttestation = async (
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,
dataType: DataTypeOption = DEFAULT_DATA_TYPE,
/**
* 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,
dataType,
contractAddress,
dataType,
})
return result
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>
}
......@@ -52,9 +52,12 @@ describe(readAttestation.name, () => {
`
[
"https://assets.optimism.io/4a609661-6774-441f-9fdb-453fdbb89931-bucket/optimist-nft/attributes",
"true",
true,
"0x68747470733a2f2f6173736574732e6f7074696d69736d2e696f2f34613630393636312d363737342d343431662d396664622d3435336664626238393933312d6275636b65742f6f7074696d6973742d6e66742f61747472696275746573",
"9665973469795080068873111198635018086067645613429821071805084917303478255842407465257371959707311987533859075426222329066766033171696373249109388415320911537042272090516917683029511016473045453921068327933733922308146003731827",
{
"hex": "0x68747470733a2f2f6173736574732e6f7074696d69736d2e696f2f34613630393636312d363737342d343431662d396664622d3435336664626238393933312d6275636b65742f6f7074696d6973742d6e66742f61747472696275746573",
"type": "BigNumber",
},
]
`
)
......
import { BigNumber } from 'ethers'
import { Address } from 'wagmi'
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
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