import { EAS as EASContract, EAS__factory as EASFactory } from '@ethereum-attestation-service/eas-contracts'; import { ContractTransaction, hexlify, Interface, keccak256, Overrides, solidityPackedKeccak256, toUtf8Bytes, TransactionReceipt, TransactionResponse } from 'ethers'; import semver from 'semver'; import { EIP712Proxy } from './eip712-proxy'; import { EAS as EASLegacyContract, EAS__factory as EASLegacyFactory } from './legacy/typechain'; import { legacyVersion } from './legacy/version'; import { Delegated, Offchain, OffchainAttestationVersion } from './offchain'; import { AttestationRequest, DelegatedAttestationRequest, DelegatedProxyAttestationRequest, DelegatedProxyRevocationRequest, DelegatedRevocationRequest, MultiAttestationRequest, MultiDelegatedAttestationRequest, MultiDelegatedProxyAttestationRequest, MultiDelegatedProxyRevocationRequest, MultiDelegatedRevocationRequest, MultiRevocationRequest, NO_EXPIRATION, RevocationRequest } from './request'; import { Base, RequireSigner, Transaction, TransactionProvider, TransactionSigner } from './transaction'; import { ZERO_ADDRESS, ZERO_BYTES32 } from './utils'; const LEGACY_VERSION = '1.1.0'; enum Event { Attested = 'Attested', Timestamped = 'Timestamped', RevokedOffchain = 'RevokedOffchain' } const TOPICS = { [Event.Attested]: keccak256(toUtf8Bytes('Attested(address,address,bytes32,bytes32)')), [Event.Timestamped]: keccak256(toUtf8Bytes('Timestamped(bytes32,uint64)')), [Event.RevokedOffchain]: keccak256(toUtf8Bytes('RevokedOffchain(address,bytes32,uint64)')) }; export { Overrides } from 'ethers'; export * from './request'; export interface Attestation { uid: string; schema: string; refUID: string; time: bigint; expirationTime: bigint; revocationTime: bigint; recipient: string; revocable: boolean; attester: string; data: string; } export interface EASOptions { signer?: TransactionSigner | TransactionProvider; proxy?: EIP712Proxy; } // Overloads to support both legacy (experimental) and standard (TC39) decorators export function RequireProxy( _target: unknown, _propertyKey: string, descriptor: PropertyDescriptor ): PropertyDescriptor; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function RequireProxy any>( value: TFn, _context: ClassMethodDecoratorContext ): TFn; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function RequireProxy(...args: any[]): any { // Standard decorator: (value, context) if (args.length === 2) { const [value] = args as [ // eslint-disable-next-line @typescript-eslint/no-explicit-any (this: unknown, ...fnArgs: any[]) => any, ClassMethodDecoratorContext ]; const wrapped = function ( this: unknown, // eslint-disable-next-line @typescript-eslint/no-explicit-any ...fnArgs: any[] ) { // eslint-disable-next-line @typescript-eslint/no-explicit-any if (!(this as any).proxy) { throw new Error('Invalid proxy'); } return value.apply(this as unknown, fnArgs); }; return wrapped; } // Legacy decorator: (target, propertyKey, descriptor) const [_target, _propertyKey, descriptor] = args as [unknown, string, PropertyDescriptor]; // eslint-disable-next-line @typescript-eslint/no-explicit-any const original = descriptor.value as unknown as (this: unknown, ...fnArgs: any[]) => unknown; descriptor.value = function ( this: unknown, // eslint-disable-next-line @typescript-eslint/no-explicit-any ...fnArgs: any[] ) { // eslint-disable-next-line @typescript-eslint/no-explicit-any if (!(this as any).proxy) { throw new Error('Invalid proxy'); } return original.apply(this as unknown, fnArgs); }; return descriptor; } export class EAS extends Base { private proxy?: EIP712Proxy; private delegated?: Delegated; private offchain?: Offchain; private version?: string; private legacyEAS: Base; constructor(address: string, options?: EASOptions) { const { signer, proxy } = options || {}; super(new EASFactory(), address, signer); // Check for ethers v6 compatibility if (!this.contract.getAddress) { throw new Error('Incompatible ethers version detect. Make sure to use the SDK with ethers v6 or later'); } this.signer = signer; if (proxy) { this.proxy = proxy; } this.legacyEAS = new Base(new EASLegacyFactory(), address, signer); } // Connects the API to a specific signer public connect(signer: TransactionSigner | TransactionProvider) { delete this.delegated; delete this.offchain; super.connect(signer); if (this.legacyEAS) { this.legacyEAS.connect(signer); } return this; } // Returns the version of the contract public async getVersion(): Promise { if (this.version) { return this.version; } return (this.version = (await legacyVersion(this.contract)) ?? (await this.contract.version())); } // Returns an existing schema by attestation UID public getAttestation(uid: string): Promise { return this.contract.getAttestation(uid); } // Returns whether an attestation is valid public isAttestationValid(uid: string): Promise { return this.contract.isAttestationValid(uid); } // Returns whether an attestation has been revoked public async isAttestationRevoked(uid: string): Promise { const attestation = await this.contract.getAttestation(uid); if (attestation.uid === ZERO_BYTES32) { throw new Error('Invalid attestation'); } return attestation.revocationTime != NO_EXPIRATION; } // Returns the timestamp that the specified data was timestamped with public getTimestamp(data: string): Promise { return this.contract.getTimestamp(data); } // Returns the timestamp that the specified data was timestamped with public getRevocationOffchain(user: string, uid: string): Promise { return this.contract.getRevokeOffchain(user, uid); } // Returns the EIP712 proxy public getEIP712Proxy(): EIP712Proxy | undefined { return this.proxy; } // Returns the delegated attestations helper public getDelegated(): Promise | Delegated { if (this.delegated) { return this.delegated; } return this.setDelegated(); } // Returns the offchain attestations helper public getOffchain(): Promise | Offchain { if (this.offchain) { return this.offchain; } return this.setOffchain(); } // Attests to a specific schema @RequireSigner public async attest( { schema, data: { recipient = ZERO_ADDRESS, data, expirationTime = NO_EXPIRATION, revocable = true, refUID = ZERO_BYTES32, value = 0n } }: AttestationRequest, overrides?: Overrides ): Promise> { return new Transaction( await this.contract.attest.populateTransaction( { schema, data: { recipient, expirationTime, revocable, refUID, data, value } }, { value, ...overrides } ), this.signer!, // eslint-disable-next-line require-await async (receipt: TransactionReceipt) => this.getUIDsFromAttestReceipt(receipt)[0] ); } // Attests to a specific schema via an EIP712 delegation request @RequireSigner public async attestByDelegation( { schema, data: { recipient = ZERO_ADDRESS, data, expirationTime = NO_EXPIRATION, revocable = true, refUID = ZERO_BYTES32, value = 0n }, signature, attester, deadline = NO_EXPIRATION }: DelegatedAttestationRequest, overrides?: Overrides ): Promise> { let tx: ContractTransaction; if (await this.isLegacyContract()) { tx = await this.legacyEAS.contract.attestByDelegation.populateTransaction( { schema, data: { recipient, expirationTime, revocable, refUID, data, value }, signature, attester }, { value, ...overrides } ); } else { tx = await this.contract.attestByDelegation.populateTransaction( { schema, data: { recipient, expirationTime, revocable, refUID, data, value }, signature, attester, deadline }, { value, ...overrides } ); } return new Transaction( tx, this.signer!, // eslint-disable-next-line require-await async (receipt: TransactionReceipt) => this.getUIDsFromAttestReceipt(receipt)[0] ); } // Multi-attests to multiple schemas @RequireSigner public async multiAttest(requests: MultiAttestationRequest[], overrides?: Overrides): Promise> { const multiAttestationRequests = requests.map((r) => ({ schema: r.schema, data: r.data.map((d) => ({ recipient: d.recipient ?? ZERO_ADDRESS, expirationTime: d.expirationTime ?? NO_EXPIRATION, revocable: d.revocable ?? true, refUID: d.refUID ?? ZERO_BYTES32, data: d.data ?? ZERO_BYTES32, value: d.value ?? 0n })) })); const requestedValue = multiAttestationRequests.reduce((res, { data }) => { const total = data.reduce((res, r) => res + r.value, 0n); return res + total; }, 0n); return new Transaction( await this.contract.multiAttest.populateTransaction(multiAttestationRequests, { value: requestedValue, ...overrides }), this.signer!, // eslint-disable-next-line require-await async (receipt: TransactionReceipt) => this.getUIDsFromAttestReceipt(receipt) ); } // Multi-attests to multiple schemas via an EIP712 delegation requests @RequireSigner public async multiAttestByDelegation( requests: MultiDelegatedAttestationRequest[], overrides?: Overrides ): Promise> { let tx: ContractTransaction; if (await this.isLegacyContract()) { const multiAttestationRequests = requests.map((r) => ({ schema: r.schema, data: r.data.map((d) => ({ recipient: d.recipient ?? ZERO_ADDRESS, expirationTime: d.expirationTime ?? NO_EXPIRATION, revocable: d.revocable ?? true, refUID: d.refUID ?? ZERO_BYTES32, data: d.data ?? ZERO_BYTES32, value: d.value ?? 0n })), signatures: r.signatures, attester: r.attester, deadline: r.deadline ?? NO_EXPIRATION })); const requestedValue = multiAttestationRequests.reduce((res, { data }) => { const total = data.reduce((res, r) => res + r.value, 0n); return res + total; }, 0n); tx = await this.legacyEAS.contract.multiAttestByDelegation.populateTransaction(multiAttestationRequests, { value: requestedValue, ...overrides }); } else { const multiAttestationRequests = requests.map((r) => ({ schema: r.schema, data: r.data.map((d) => ({ recipient: d.recipient ?? ZERO_ADDRESS, expirationTime: d.expirationTime ?? NO_EXPIRATION, revocable: d.revocable ?? true, refUID: d.refUID ?? ZERO_BYTES32, data: d.data ?? ZERO_BYTES32, value: d.value ?? 0n })), signatures: r.signatures, attester: r.attester, deadline: r.deadline ?? NO_EXPIRATION })); const requestedValue = multiAttestationRequests.reduce((res, { data }) => { const total = data.reduce((res, r) => res + r.value, 0n); return res + total; }, 0n); tx = await this.contract.multiAttestByDelegation.populateTransaction(multiAttestationRequests, { value: requestedValue, ...overrides }); } return new Transaction( tx, this.signer!, // eslint-disable-next-line require-await async (receipt: TransactionReceipt) => this.getUIDsFromAttestReceipt(receipt) ); } // Revokes an existing attestation @RequireSigner public async revoke( { schema, data: { uid, value = 0n } }: RevocationRequest, overrides?: Overrides ): Promise> { return new Transaction( await this.contract.revoke.populateTransaction({ schema, data: { uid, value } }, { value, ...overrides }), this.signer!, async () => {} ); } // Revokes an existing attestation an EIP712 delegation request @RequireSigner public async revokeByDelegation( { schema, data: { uid, value = 0n }, signature, revoker, deadline = NO_EXPIRATION }: DelegatedRevocationRequest, overrides?: Overrides ): Promise> { let tx: ContractTransaction; if (await this.isLegacyContract()) { tx = await this.legacyEAS.contract.revokeByDelegation.populateTransaction( { schema, data: { uid, value }, signature, revoker }, { value, ...overrides } ); } else { tx = await this.contract.revokeByDelegation.populateTransaction( { schema, data: { uid, value }, signature, revoker, deadline }, { value, ...overrides } ); } return new Transaction(tx, this.signer!, async () => {}); } // Multi-revokes multiple attestations @RequireSigner public async multiRevoke(requests: MultiRevocationRequest[], overrides?: Overrides): Promise> { const multiRevocationRequests = requests.map((r) => ({ schema: r.schema, data: r.data.map((d) => ({ uid: d.uid, value: d.value ?? 0n })) })); const requestedValue = multiRevocationRequests.reduce((res, { data }) => { const total = data.reduce((res, r) => res + r.value, 0n); return res + total; }, 0n); return new Transaction( await this.contract.multiRevoke.populateTransaction(multiRevocationRequests, { value: requestedValue, ...overrides }), this.signer!, async () => {} ); } // Multi-revokes multiple attestations via an EIP712 delegation requests @RequireSigner public async multiRevokeByDelegation( requests: MultiDelegatedRevocationRequest[], overrides?: Overrides ): Promise> { let tx: ContractTransaction; if (await this.isLegacyContract()) { const multiRevocationRequests = requests.map((r) => ({ schema: r.schema, data: r.data.map((d) => ({ uid: d.uid, value: d.value ?? 0n })), signatures: r.signatures, revoker: r.revoker })); const requestedValue = multiRevocationRequests.reduce((res, { data }) => { const total = data.reduce((res, r) => res + r.value, 0n); return res + total; }, 0n); tx = await this.legacyEAS.contract.multiRevokeByDelegation.populateTransaction(multiRevocationRequests, { value: requestedValue, ...overrides }); } else { const multiRevocationRequests = requests.map((r) => ({ schema: r.schema, data: r.data.map((d) => ({ uid: d.uid, value: d.value ?? 0n })), signatures: r.signatures, revoker: r.revoker, deadline: r.deadline ?? NO_EXPIRATION })); const requestedValue = multiRevocationRequests.reduce((res, { data }) => { const total = data.reduce((res, r) => res + r.value, 0n); return res + total; }, 0n); tx = await this.contract.multiRevokeByDelegation.populateTransaction(multiRevocationRequests, { value: requestedValue, ...overrides }); } return new Transaction(tx, this.signer!, async () => {}); } // Attests to a specific schema via an EIP712 delegation request using an external EIP712 proxy @RequireSigner @RequireProxy public attestByDelegationProxy( request: DelegatedProxyAttestationRequest, overrides?: Overrides ): Promise> { return this.proxy!.attestByDelegationProxy(request, overrides); } // Multi-attests to multiple schemas via an EIP712 delegation requests using an external EIP712 proxy @RequireSigner @RequireProxy public multiAttestByDelegationProxy( requests: MultiDelegatedProxyAttestationRequest[], overrides?: Overrides ): Promise> { return this.proxy!.multiAttestByDelegationProxy(requests, overrides); } // Revokes an existing attestation an EIP712 delegation request using an external EIP712 proxy @RequireSigner @RequireProxy public revokeByDelegationProxy( request: DelegatedProxyRevocationRequest, overrides?: Overrides ): Promise> { return this.proxy!.revokeByDelegationProxy(request, overrides); } // Multi-revokes multiple attestations via an EIP712 delegation requests using an external EIP712 proxy @RequireSigner @RequireProxy public multiRevokeByDelegationProxy( requests: MultiDelegatedProxyRevocationRequest[], overrides?: Overrides ): Promise> { return this.proxy!.multiRevokeByDelegationProxy(requests, overrides); } // Timestamps the specified bytes32 data @RequireSigner public async timestamp(data: string, overrides?: Overrides): Promise> { return new Transaction( await this.contract.timestamp.populateTransaction(data, overrides ?? {}), this.signer!, // eslint-disable-next-line require-await async (receipt: TransactionReceipt) => this.getTimestampFromTimestampReceipt(receipt)[0] ); } // Timestamps the specified multiple bytes32 data @RequireSigner public async multiTimestamp(data: string[], overrides?: Overrides): Promise> { return new Transaction( await this.contract.multiTimestamp.populateTransaction(data, overrides ?? {}), this.signer!, // eslint-disable-next-line require-await async (receipt: TransactionReceipt) => this.getTimestampFromTimestampReceipt(receipt) ); } // Revokes the specified offchain attestation UID @RequireSigner public async revokeOffchain(uid: string, overrides?: Overrides): Promise> { return new Transaction( await this.contract.revokeOffchain.populateTransaction(uid, overrides ?? {}), this.signer!, // eslint-disable-next-line require-await async (receipt: TransactionReceipt) => this.getTimestampFromOffchainRevocationReceipt(receipt)[0] ); } // Revokes the specified multiple offchain attestation UIDs @RequireSigner public async multiRevokeOffchain(uids: string[], overrides?: Overrides): Promise> { return new Transaction( await this.contract.multiRevokeOffchain.populateTransaction(uids, overrides ?? {}), this.signer!, // eslint-disable-next-line require-await async (receipt: TransactionReceipt) => this.getTimestampFromOffchainRevocationReceipt(receipt) ); } // Returns the domain separator used in the encoding of the signatures for attest, and revoke public getDomainSeparator(): Promise { return this.contract.getDomainSeparator(); } // Returns the current nonce per-account. public getNonce(address: string): Promise { return this.contract.getNonce(address); } // Returns the EIP712 type hash for the attest function public getAttestTypeHash(): Promise { return this.contract.getAttestTypeHash(); } // Returns the EIP712 type hash for the revoke function public getRevokeTypeHash(): Promise { return this.contract.getRevokeTypeHash(); } // Return attestation UID public static getAttestationUID = ( schema: string, recipient: string, attester: string, time: bigint, expirationTime: bigint, revocable: boolean, refUID: string, data: string, bump: number ) => solidityPackedKeccak256( ['bytes', 'address', 'address', 'uint64', 'uint64', 'bool', 'bytes32', 'bytes', 'uint32'], [hexlify(toUtf8Bytes(schema)), recipient, attester, time, expirationTime, revocable, refUID, data, bump] ); public async getUIDFromAttestTx(res: Promise | TransactionResponse): Promise { return (await this.getUIDsFromMultiAttestTx(res))[0]; } public async getUIDsFromMultiAttestTx(res: Promise | TransactionResponse): Promise { const tx = await res; const receipt = await tx.wait(); if (!receipt) { throw new Error(`Unable to confirm: ${tx}`); } return this.getUIDsFromAttestReceipt(receipt); } public getUIDsFromAttestReceipt(receipt: TransactionReceipt): string[] { return this.getDataFromReceipt(receipt, Event.Attested, 'uid'); } public getTimestampFromTimestampReceipt(receipt: TransactionReceipt): bigint[] { return this.getDataFromReceipt(receipt, Event.Timestamped, 'timestamp').map((s) => BigInt(s)); } public getTimestampFromOffchainRevocationReceipt(receipt: TransactionReceipt): bigint[] { return this.getDataFromReceipt(receipt, Event.RevokedOffchain, 'timestamp').map((s) => BigInt(s)); } // Sets the delegated attestations helper private async setDelegated(): Promise { this.delegated = new Delegated( { address: await this.contract.getAddress(), domainSeparator: await this.getDomainSeparator(), chainId: await this.getChainId() }, this ); return this.delegated; } // Sets the offchain attestations helper private async setOffchain(): Promise { this.offchain = new Offchain( { address: await this.contract.getAddress(), version: await this.getVersion(), chainId: await this.getChainId() }, OffchainAttestationVersion.Version2, this ); return this.offchain; } private async isLegacyContract() { const version = await this.getVersion(); const fullVersion = semver.coerce(version); if (!fullVersion) { throw new Error(`Invalid version: ${version}`); } return semver.lte(fullVersion, LEGACY_VERSION); } private getDataFromReceipt(receipt: TransactionReceipt, event: Event, attribute: string): string[] { const eas = new Interface(EASFactory.abi); const easAddress = this.contract.target as string; const logs = []; for (const log of receipt.logs.filter( (l) => l.topics[0] === TOPICS[event] && l.address.toLowerCase() === easAddress.toLowerCase() ) || []) { logs.push({ ...log, log: event, fragment: { name: event }, args: eas.decodeEventLog(event, log.data, log.topics) }); } if (!logs) { return []; } const filteredLogs = logs.filter((l) => l.fragment?.name === event); if (filteredLogs.length === 0) { throw new Error(`Unable to process ${event} events`); } return filteredLogs.map( // eslint-disable-next-line @typescript-eslint/no-explicit-any (log: any) => eas.decodeEventLog(event, log.data, log.topics)[attribute] ); } }