From 37a5b68665c1365668ed85b99c90ff6b7a87532d Mon Sep 17 00:00:00 2001 From: Will Cory Date: Thu, 23 Feb 2023 02:23:39 -0800 Subject: [PATCH] :sparkles: Add atst SDK :sparkles: add everything else :art: move the casting to the sdk :recycle: clean up Build sdk with tsup Implement the package remove the comment Promise.all it use readContracts start finishing everyithing finish sdk untested add vite tests for the sdk add all the reading tests add rest of tests and functionality last test implementation is done start on readmes add todos fix package versions fix circleci changeset revert sdk change --- .../atst/src/constants/defaultDataType.ts | 5 ++ packages/atst/src/constants/defaultRpcUrl.ts | 5 ++ packages/atst/src/index.ts | 11 +++ .../src/lib/__snapshots__/logger.spec.ts.snap | 11 +++ .../src/lib/parseAttestationBytes.spec.ts | 42 +++++++++++ .../atst/src/lib/parseAttestationBytes.ts | 29 ++++++++ .../src/lib/prepareWriteAttestation.spec.ts | 71 +++++++++++++++++++ .../atst/src/lib/prepareWriteAttestation.ts | 24 +++++++ packages/atst/src/lib/readAttestation.spec.ts | 41 +++++++++++ packages/atst/src/lib/readAttestation.ts | 36 ++++++++++ .../atst/src/lib/readAttestations.spec.ts | 62 ++++++++++++++++ packages/atst/src/lib/readAttestations.ts | 66 +++++++++++++++++ .../atst/src/lib/stringifyAttestationBytes.ts | 26 +++++++ .../atst/src/lib/writeAttestation.spec.ts | 10 +++ packages/atst/src/lib/writeAttestation.ts | 15 ++++ .../atst/src/types/AttestationReadParams.ts | 15 ++++ packages/atst/src/types/DataTypeOption.ts | 22 ++++++ packages/atst/src/types/WagmiBytes.ts | 5 ++ 18 files changed, 496 insertions(+) create mode 100644 packages/atst/src/constants/defaultDataType.ts create mode 100644 packages/atst/src/constants/defaultRpcUrl.ts create mode 100644 packages/atst/src/lib/__snapshots__/logger.spec.ts.snap create mode 100644 packages/atst/src/lib/parseAttestationBytes.spec.ts create mode 100644 packages/atst/src/lib/parseAttestationBytes.ts create mode 100644 packages/atst/src/lib/prepareWriteAttestation.spec.ts create mode 100644 packages/atst/src/lib/prepareWriteAttestation.ts create mode 100644 packages/atst/src/lib/readAttestation.spec.ts create mode 100644 packages/atst/src/lib/readAttestation.ts create mode 100644 packages/atst/src/lib/readAttestations.spec.ts create mode 100644 packages/atst/src/lib/readAttestations.ts create mode 100644 packages/atst/src/lib/stringifyAttestationBytes.ts create mode 100644 packages/atst/src/lib/writeAttestation.spec.ts create mode 100644 packages/atst/src/lib/writeAttestation.ts create mode 100644 packages/atst/src/types/AttestationReadParams.ts create mode 100644 packages/atst/src/types/DataTypeOption.ts create mode 100644 packages/atst/src/types/WagmiBytes.ts diff --git a/packages/atst/src/constants/defaultDataType.ts b/packages/atst/src/constants/defaultDataType.ts new file mode 100644 index 000000000000..583c8497a35c --- /dev/null +++ b/packages/atst/src/constants/defaultDataType.ts @@ -0,0 +1,5 @@ +/** + * @internal + * Default data type for attestations + */ +export const DEFAULT_DATA_TYPE = 'string' as const diff --git a/packages/atst/src/constants/defaultRpcUrl.ts b/packages/atst/src/constants/defaultRpcUrl.ts new file mode 100644 index 000000000000..e4c15dc4f091 --- /dev/null +++ b/packages/atst/src/constants/defaultRpcUrl.ts @@ -0,0 +1,5 @@ +/** + * @internal + * Default RPC URL for Optimism + */ +export const DEFAULT_RPC_URL = 'https://mainnet.optimism.io' diff --git a/packages/atst/src/index.ts b/packages/atst/src/index.ts index 533c9a8bde83..4c7772b8199b 100644 --- a/packages/atst/src/index.ts +++ b/packages/atst/src/index.ts @@ -1,2 +1,13 @@ // 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 { writeAttestation } from './lib/writeAttestation' +export { abi } from './lib/abi' +export { parseAttestationBytes } from './lib/parseAttestationBytes' +// types +export type { AttestationReadParams } from './types/AttestationReadParams' +export type { WagmiBytes } from './types/WagmiBytes' +export type { DataTypeOption } from './types/DataTypeOption' diff --git a/packages/atst/src/lib/__snapshots__/logger.spec.ts.snap b/packages/atst/src/lib/__snapshots__/logger.spec.ts.snap new file mode 100644 index 000000000000..7f2679594359 --- /dev/null +++ b/packages/atst/src/lib/__snapshots__/logger.spec.ts.snap @@ -0,0 +1,11 @@ +// 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"`; diff --git a/packages/atst/src/lib/parseAttestationBytes.spec.ts b/packages/atst/src/lib/parseAttestationBytes.spec.ts new file mode 100644 index 000000000000..738a64b946e9 --- /dev/null +++ b/packages/atst/src/lib/parseAttestationBytes.spec.ts @@ -0,0 +1,42 @@ +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) + }) +}) diff --git a/packages/atst/src/lib/parseAttestationBytes.ts b/packages/atst/src/lib/parseAttestationBytes.ts new file mode 100644 index 000000000000..d616bd0daebd --- /dev/null +++ b/packages/atst/src/lib/parseAttestationBytes.ts @@ -0,0 +1,29 @@ +import { BigNumber } from 'ethers' +import { toUtf8String } from 'ethers/lib/utils.js' + +import type { DataTypeOption } from '../types/DataTypeOption' +import * as logger from './logger' +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) + } + logger.warn(`unrecognized dataType ${dataType satisfies never}`) + return attestationBytes +} diff --git a/packages/atst/src/lib/prepareWriteAttestation.spec.ts b/packages/atst/src/lib/prepareWriteAttestation.spec.ts new file mode 100644 index 000000000000..750348fd6c85 --- /dev/null +++ b/packages/atst/src/lib/prepareWriteAttestation.spec.ts @@ -0,0 +1,71 @@ +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"' + ) + }) +}) diff --git a/packages/atst/src/lib/prepareWriteAttestation.ts b/packages/atst/src/lib/prepareWriteAttestation.ts new file mode 100644 index 000000000000..c3e9eebbdda1 --- /dev/null +++ b/packages/atst/src/lib/prepareWriteAttestation.ts @@ -0,0 +1,24 @@ +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], + }) +} diff --git a/packages/atst/src/lib/readAttestation.spec.ts b/packages/atst/src/lib/readAttestation.spec.ts new file mode 100644 index 000000000000..5df88a885216 --- /dev/null +++ b/packages/atst/src/lib/readAttestation.spec.ts @@ -0,0 +1,41 @@ +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"' + ) + }) +}) diff --git a/packages/atst/src/lib/readAttestation.ts b/packages/atst/src/lib/readAttestation.ts new file mode 100644 index 000000000000..2f85589c62c4 --- /dev/null +++ b/packages/atst/src/lib/readAttestation.ts @@ -0,0 +1,36 @@ +import type { Address } from '@wagmi/core' + +import { DEFAULT_DATA_TYPE } from '../constants/defaultDataType' +import type { DataTypeOption } 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 +} diff --git a/packages/atst/src/lib/readAttestations.spec.ts b/packages/atst/src/lib/readAttestations.spec.ts new file mode 100644 index 000000000000..6fd5ba2f5805 --- /dev/null +++ b/packages/atst/src/lib/readAttestations.spec.ts @@ -0,0 +1,62 @@ +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", + ] + ` + ) + }) +}) diff --git a/packages/atst/src/lib/readAttestations.ts b/packages/atst/src/lib/readAttestations.ts new file mode 100644 index 000000000000..945cac4982b2 --- /dev/null +++ b/packages/atst/src/lib/readAttestations.ts @@ -0,0 +1,66 @@ +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 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 +) => { + 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 ?? 'string' + return parseAttestationBytes(dataBytes, dataType) + }) +} diff --git a/packages/atst/src/lib/stringifyAttestationBytes.ts b/packages/atst/src/lib/stringifyAttestationBytes.ts new file mode 100644 index 000000000000..424958d76aa4 --- /dev/null +++ b/packages/atst/src/lib/stringifyAttestationBytes.ts @@ -0,0 +1,26 @@ +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 +) => { + 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}`) +} diff --git a/packages/atst/src/lib/writeAttestation.spec.ts b/packages/atst/src/lib/writeAttestation.spec.ts new file mode 100644 index 000000000000..46bd7d363712 --- /dev/null +++ b/packages/atst/src/lib/writeAttestation.spec.ts @@ -0,0 +1,10 @@ +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) + }) +}) diff --git a/packages/atst/src/lib/writeAttestation.ts b/packages/atst/src/lib/writeAttestation.ts new file mode 100644 index 000000000000..14db0d35d6c3 --- /dev/null +++ b/packages/atst/src/lib/writeAttestation.ts @@ -0,0 +1,15 @@ +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 diff --git a/packages/atst/src/types/AttestationReadParams.ts b/packages/atst/src/types/AttestationReadParams.ts new file mode 100644 index 000000000000..71bb6c12baad --- /dev/null +++ b/packages/atst/src/types/AttestationReadParams.ts @@ -0,0 +1,15 @@ +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 +} diff --git a/packages/atst/src/types/DataTypeOption.ts b/packages/atst/src/types/DataTypeOption.ts new file mode 100644 index 000000000000..848fa34358d2 --- /dev/null +++ b/packages/atst/src/types/DataTypeOption.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' + +/** + * 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 diff --git a/packages/atst/src/types/WagmiBytes.ts b/packages/atst/src/types/WagmiBytes.ts new file mode 100644 index 000000000000..c4d127df5755 --- /dev/null +++ b/packages/atst/src/types/WagmiBytes.ts @@ -0,0 +1,5 @@ +/** + * @internal + * WagmiBytes is a type that represents a hex string with a length of 32 bytes. + */ +export type WagmiBytes = `0x${string}`