From 4ef4a83f74f9dfb93fae9136bc725d03e716b7e4 Mon Sep 17 00:00:00 2001 From: Alain Nicolas Date: Wed, 11 Oct 2023 00:07:42 +0200 Subject: [PATCH] feat: As a user, I want to create an attestation using the SDK --- pnpm-lock.yaml | 8 +- sdk/.env.example | 1 + sdk/examples/portal/portalExamples.ts | 32 +- sdk/package.json | 18 +- sdk/src/VeraxSdk.ts | 33 +- sdk/src/abi/DefaultPortal.ts | 403 ++++++++++++++++++ sdk/src/dataMapper/BaseDataMapper.ts | 11 +- sdk/src/dataMapper/PortalDataMapper.ts | 47 +- sdk/src/types/index.d.ts | 19 +- .../dataMapper/AttestationDataMapper.test.ts | 18 +- 10 files changed, 552 insertions(+), 38 deletions(-) create mode 100644 sdk/.env.example create mode 100644 sdk/src/abi/DefaultPortal.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e500158..09856b34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: '@apollo/client': specifier: ^3.8.4 version: 3.8.4(graphql@16.8.1) + dotenv: + specifier: ^16.3.1 + version: 16.3.1 graphql: specifier: ^16.8.1 version: 16.8.1 @@ -4187,7 +4190,6 @@ packages: /dotenv@16.3.1: resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} engines: {node: '>=12'} - dev: true /ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} @@ -5154,7 +5156,7 @@ packages: dependencies: inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 8.0.4 once: 1.4.0 path-is-absolute: 1.0.1 dev: true @@ -5165,7 +5167,7 @@ packages: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 8.0.4 once: 1.4.0 path-is-absolute: 1.0.1 dev: true diff --git a/sdk/.env.example b/sdk/.env.example new file mode 100644 index 00000000..606ca5e9 --- /dev/null +++ b/sdk/.env.example @@ -0,0 +1 @@ +PRIVATE_KEY=XXX diff --git a/sdk/examples/portal/portalExamples.ts b/sdk/examples/portal/portalExamples.ts index a0067f39..ec0cc9db 100644 --- a/sdk/examples/portal/portalExamples.ts +++ b/sdk/examples/portal/portalExamples.ts @@ -2,9 +2,11 @@ import VeraxSdk from "../../src/VeraxSdk"; export default class PortalExamples { private veraxSdk: VeraxSdk; + constructor(_veraxSdk: VeraxSdk) { this.veraxSdk = _veraxSdk; } + async run(methodName: string = "") { if (methodName.toLowerCase() == "findOneById".toLowerCase() || methodName == "") console.log(await this.veraxSdk.portal.findOneById("0x1495341ab1019798dd08976f4a3e5ab0e095510b")); @@ -12,7 +14,35 @@ export default class PortalExamples { if (methodName.toLowerCase() == "findBy".toLowerCase() || methodName == "") console.log(await this.veraxSdk.portal.findBy({ ownerName: "Clique" })); - if (methodName.toLowerCase() == "attest" || methodName == "") console.log(await this.veraxSdk.portal.attest()); + if (methodName.toLowerCase() == "simulateAttest".toLowerCase() || methodName == "") { + console.log( + await this.veraxSdk.portal.simulateAttest( + "0xeea25bc2ec56cae601df33b8fc676673285e12cc", + { + schemaId: "0x9ba590dd7fbd5bd1a7d06cdcb4744e20a49b3520560575cd63de17734a408738", + expirationDate: 1693583329, + subject: "0x828c9f04D1a07E3b0aBE12A9F8238a3Ff7E57b47", + attestationData: [{ isBuidler: true }], + }, + [], + ), + ); + } + + if (methodName.toLowerCase() == "attest" || methodName == "") { + console.log( + await this.veraxSdk.portal.attest( + "0xeea25bc2ec56cae601df33b8fc676673285e12cc", + { + schemaId: "0x9ba590dd7fbd5bd1a7d06cdcb4744e20a49b3520560575cd63de17734a408738", + expirationDate: 1693583329, + subject: "0x828c9f04D1a07E3b0aBE12A9F8238a3Ff7E57b47", + attestationData: [{ isBuidler: true }], + }, + [], + ), + ); + } if (methodName.toLowerCase() == "bulkAttest".toLowerCase() || methodName == "") console.log(await this.veraxSdk.portal.bulkAttest()); diff --git a/sdk/package.json b/sdk/package.json index e7b130ab..2281f09d 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -20,25 +20,11 @@ "module": "ts-node examples/module/index.ts", "portal": "ts-node examples/portal/index.ts", "schema": "ts-node examples/schema/index.ts", - "attestation:all": "ts-node examples/attestation/findBy.ts", - "attestation:one": "ts-node examples/attestation/findOneById.ts", - "module:all": "ts-node examples/module/findBy.ts", - "module:one": "ts-node examples/module/findOneById.ts", - "portal:all": "ts-node examples/portal/findBy.ts", - "portal:one": "ts-node examples/portal/findOneById.ts", - "schema:all": "ts-node examples/schema/findBy.ts", - "schema:one": "ts-node examples/schema/findOneById.ts", - "test": "jest", - "utils:attestation:counter": "ts-node examples/utils/getAttestationIdCounter.ts", - "utils:attestation:version": "ts-node examples/utils/getVersionNumber.ts", - "utils:decode": "ts-node examples/utils/decode.ts", - "utils:encode": "ts-node examples/utils/encode.ts", - "utils:module:counter": "ts-node examples/utils/getModulesNumber.ts", - "utils:portal:counter": "ts-node examples/utils/getPortalsCount.ts", - "utils:schema:counter": "ts-node examples/utils/getSchemasNumber.ts" + "test": "jest" }, "dependencies": { "@apollo/client": "^3.8.4", + "dotenv": "^16.3.1", "graphql": "^16.8.1", "viem": "^1.14.0" }, diff --git a/sdk/src/VeraxSdk.ts b/sdk/src/VeraxSdk.ts index c7397ba6..4ce14769 100644 --- a/sdk/src/VeraxSdk.ts +++ b/sdk/src/VeraxSdk.ts @@ -3,14 +3,19 @@ import AttestationDataMapper from "./dataMapper/AttestationDataMapper"; import SchemaDataMapper from "./dataMapper/SchemaDataMapper"; import ModuleDataMapper from "./dataMapper/ModuleDataMapper"; import PortalDataMapper from "./dataMapper/PortalDataMapper"; -import { createPublicClient, http, PublicClient } from "viem"; +import { createPublicClient, createWalletClient, Hex, http, PublicClient, WalletClient } from "viem"; import { ApolloClient, InMemoryCache } from "@apollo/client/core"; -import { Conf } from "./types"; import UtilsDataMapper from "./dataMapper/UtilsDataMapper"; +import { Conf } from "./types"; +import { privateKeyToAccount } from "viem/accounts"; +import dotenv from "dotenv"; + +dotenv.config({ path: "./.env" }); export default class VeraxSdk { static DEFAULT_LINEA_MAINNET: Conf = { chain: linea, + mode: 0, // TODO: use SDKMode enum subgraphUrl: "https://graph-query.linea.build/subgraphs/name/Consensys/linea-attestation-registry", portalRegistryAddress: "0xd5d61e4ECDf6d46A63BfdC262af92544DFc19083", moduleRegistryAddress: "0xf851513A732996F22542226341748f3C9978438f", @@ -20,6 +25,7 @@ export default class VeraxSdk { static DEFAULT_LINEA_TESTNET: Conf = { chain: lineaTestnet, + mode: 0, subgraphUrl: "https://graph-query.goerli.linea.build/subgraphs/name/Consensys/linea-attestation-registry", portalRegistryAddress: "0x506f88a5Ca8D5F001f2909b029738A40042e42a6", moduleRegistryAddress: "0x1a20b2CFA134686306436D2c9f778D7eC6c43A43", @@ -28,6 +34,7 @@ export default class VeraxSdk { }; private readonly web3Client: PublicClient; + private readonly walletClient: WalletClient; private readonly apolloClient: ApolloClient; public attestation: AttestationDataMapper; @@ -42,15 +49,27 @@ export default class VeraxSdk { transport: http(), }); + this.walletClient = + conf.mode === 0 + ? createWalletClient({ + chain: conf.chain, + account: privateKeyToAccount(process.env.PRIVATE_KEY as Hex), + transport: http(), + }) + : createWalletClient({ + chain: conf.chain, + transport: http(), + }); + this.apolloClient = new ApolloClient({ uri: conf.subgraphUrl, cache: new InMemoryCache(), }); - this.attestation = new AttestationDataMapper(conf, this.web3Client, this.apolloClient); - this.schema = new SchemaDataMapper(conf, this.web3Client, this.apolloClient); - this.module = new ModuleDataMapper(conf, this.web3Client, this.apolloClient); - this.portal = new PortalDataMapper(conf, this.web3Client, this.apolloClient); - this.utils = new UtilsDataMapper(conf, this.web3Client, this.apolloClient); + this.attestation = new AttestationDataMapper(conf, this.web3Client, this.walletClient, this.apolloClient); + this.schema = new SchemaDataMapper(conf, this.web3Client, this.walletClient, this.apolloClient); + this.module = new ModuleDataMapper(conf, this.web3Client, this.walletClient, this.apolloClient); + this.portal = new PortalDataMapper(conf, this.web3Client, this.walletClient, this.apolloClient); + this.utils = new UtilsDataMapper(conf, this.web3Client, this.walletClient, this.apolloClient); } } diff --git a/sdk/src/abi/DefaultPortal.ts b/sdk/src/abi/DefaultPortal.ts new file mode 100644 index 00000000..04fe0715 --- /dev/null +++ b/sdk/src/abi/DefaultPortal.ts @@ -0,0 +1,403 @@ +export const abiDefaultPortal = [ + { + inputs: [ + { + internalType: "address[]", + name: "modules", + type: "address[]", + }, + { + internalType: "address", + name: "router", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "OnlyPortalOwner", + type: "error", + }, + { + inputs: [], + name: "AlreadyRevoked", + type: "error", + }, + { + inputs: [], + name: "ArrayLengthMismatch", + type: "error", + }, + { + inputs: [], + name: "AttestationDataFieldEmpty", + type: "error", + }, + { + inputs: [], + name: "AttestationNotAttested", + type: "error", + }, + { + inputs: [], + name: "AttestationNotRevocable", + type: "error", + }, + { + inputs: [], + name: "AttestationSubjectFieldEmpty", + type: "error", + }, + { + inputs: [], + name: "OnlyAttestingPortal", + type: "error", + }, + { + inputs: [], + name: "OnlyPortal", + type: "error", + }, + { + inputs: [], + name: "RouterInvalid", + type: "error", + }, + { + inputs: [], + name: "SchemaNotRegistered", + type: "error", + }, + { + inputs: [ + { + components: [ + { + internalType: "bytes32", + name: "schemaId", + type: "bytes32", + }, + { + internalType: "uint64", + name: "expirationDate", + type: "uint64", + }, + { + internalType: "bytes", + name: "subject", + type: "bytes", + }, + { + internalType: "bytes", + name: "attestationData", + type: "bytes", + }, + ], + internalType: "struct AttestationPayload", + name: "attestationPayload", + type: "tuple", + }, + { + internalType: "bytes[]", + name: "validationPayloads", + type: "bytes[]", + }, + ], + name: "attest", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "attestationRegistry", + outputs: [ + { + internalType: "contract AttestationRegistry", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "bytes32", + name: "schemaId", + type: "bytes32", + }, + { + internalType: "uint64", + name: "expirationDate", + type: "uint64", + }, + { + internalType: "bytes", + name: "subject", + type: "bytes", + }, + { + internalType: "bytes", + name: "attestationData", + type: "bytes", + }, + ], + internalType: "struct AttestationPayload[]", + name: "attestationsPayloads", + type: "tuple[]", + }, + { + internalType: "bytes[][]", + name: "validationPayloads", + type: "bytes[][]", + }, + ], + name: "bulkAttest", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32[]", + name: "attestationIds", + type: "bytes32[]", + }, + { + components: [ + { + internalType: "bytes32", + name: "schemaId", + type: "bytes32", + }, + { + internalType: "uint64", + name: "expirationDate", + type: "uint64", + }, + { + internalType: "bytes", + name: "subject", + type: "bytes", + }, + { + internalType: "bytes", + name: "attestationData", + type: "bytes", + }, + ], + internalType: "struct AttestationPayload[]", + name: "attestationsPayloads", + type: "tuple[]", + }, + { + internalType: "bytes[][]", + name: "validationPayloads", + type: "bytes[][]", + }, + ], + name: "bulkReplace", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32[]", + name: "attestationIds", + type: "bytes32[]", + }, + ], + name: "bulkRevoke", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "getAttester", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getModules", + outputs: [ + { + internalType: "address[]", + name: "", + type: "address[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "moduleRegistry", + outputs: [ + { + internalType: "contract ModuleRegistry", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "modules", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "portalRegistry", + outputs: [ + { + internalType: "contract PortalRegistry", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "attestationId", + type: "bytes32", + }, + { + components: [ + { + internalType: "bytes32", + name: "schemaId", + type: "bytes32", + }, + { + internalType: "uint64", + name: "expirationDate", + type: "uint64", + }, + { + internalType: "bytes", + name: "subject", + type: "bytes", + }, + { + internalType: "bytes", + name: "attestationData", + type: "bytes", + }, + ], + internalType: "struct AttestationPayload", + name: "attestationPayload", + type: "tuple", + }, + { + internalType: "bytes[]", + name: "validationPayloads", + type: "bytes[]", + }, + ], + name: "replace", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "attestationId", + type: "bytes32", + }, + ], + name: "revoke", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "router", + outputs: [ + { + internalType: "contract IRouter", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "interfaceID", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "address payable", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +]; diff --git a/sdk/src/dataMapper/BaseDataMapper.ts b/sdk/src/dataMapper/BaseDataMapper.ts index 0a3d4aa1..a825bd79 100644 --- a/sdk/src/dataMapper/BaseDataMapper.ts +++ b/sdk/src/dataMapper/BaseDataMapper.ts @@ -1,4 +1,4 @@ -import { PublicClient } from "viem"; +import { PublicClient, WalletClient } from "viem"; import { ApolloClient, gql } from "@apollo/client/core"; import { Conf } from "../types"; import { stringifyWhereClause } from "../utils/apolloClientHelper"; @@ -6,13 +6,20 @@ import { stringifyWhereClause } from "../utils/apolloClientHelper"; export default abstract class BaseDataMapper { protected readonly conf: Conf; protected readonly web3Client: PublicClient; + protected readonly walletClient: WalletClient; protected readonly apolloClient: ApolloClient; protected abstract typeName: string; protected abstract gqlInterface: string; - constructor(_conf: Conf, _web3Client: PublicClient, _apolloClient: ApolloClient) { + constructor( + _conf: Conf, + _web3Client: PublicClient, + _walletClient: WalletClient, + _apolloClient: ApolloClient, + ) { this.conf = _conf; this.web3Client = _web3Client; + this.walletClient = _walletClient; this.apolloClient = _apolloClient; } diff --git a/sdk/src/dataMapper/PortalDataMapper.ts b/sdk/src/dataMapper/PortalDataMapper.ts index d76cd7da..d8194626 100644 --- a/sdk/src/dataMapper/PortalDataMapper.ts +++ b/sdk/src/dataMapper/PortalDataMapper.ts @@ -1,5 +1,8 @@ -import { Portal } from "../types"; +import { AttestationPayload, Portal } from "../types"; import BaseDataMapper from "./BaseDataMapper"; +import { abiDefaultPortal } from "../abi/DefaultPortal"; +import { Address, BaseError, ContractFunctionRevertedError, Hash } from "viem"; +import { encode } from "../utils/abiCoder"; export default class PortalDataMapper extends BaseDataMapper { typeName = "portal"; @@ -13,8 +16,46 @@ export default class PortalDataMapper extends BaseDataMapper { ownerName }`; - async attest() { - throw new Error("Not implemented"); + async simulateAttest(portalAddress: Address, attestationPayload: AttestationPayload, validationPayloads: string[]) { + // TODO: get matching Schema from the SchemaDataMapper? + //const matchingSchema = await schemaDataMapper.findOneById(attestationPayload.schemaId); + const matchingSchema = { schema: "(bool isBuidler)" }; + const attestationData = encode(matchingSchema.schema, attestationPayload.attestationData); + + try { + const { request } = await this.web3Client.simulateContract({ + address: portalAddress, + abi: abiDefaultPortal, + functionName: "attest", + account: this.walletClient.account, + args: [ + [attestationPayload.schemaId, attestationPayload.expirationDate, attestationPayload.subject, attestationData], + validationPayloads, + ], + }); + + return request; + } catch (err) { + if (err instanceof BaseError) { + const revertError = err.walk((err) => err instanceof ContractFunctionRevertedError); + if (revertError instanceof ContractFunctionRevertedError) { + const errorName = revertError.data?.errorName ?? ""; + console.error(`Failing with ${errorName}`); + } + } + console.error(err); + + throw new Error("Simulation failed"); + } + } + + async attest(portalAddress: Address, attestationPayload: AttestationPayload, validationPayloads: string[]) { + const request = await this.simulateAttest(portalAddress, attestationPayload, validationPayloads); + const hash: Hash = await this.walletClient.writeContract(request); + + console.log(`Transaction sent with hash ${hash}`); + + return hash; } async bulkAttest() { diff --git a/sdk/src/types/index.d.ts b/sdk/src/types/index.d.ts index 1de13567..feda97b0 100644 --- a/sdk/src/types/index.d.ts +++ b/sdk/src/types/index.d.ts @@ -1,7 +1,13 @@ -import { Chain, Address } from "viem"; +import { Address, Chain } from "viem"; + +export enum SDKMode { + BACKEND, + FRONTEND, +} export interface Conf { chain: Chain; + mode: SDKMode; subgraphUrl: string; portalRegistryAddress: Address; moduleRegistryAddress: Address; @@ -9,12 +15,19 @@ export interface Conf { attestationRegistryAddress: Address; } +export type AttestationPayload = { + schemaId: string; // The identifier of the schema this attestation adheres to. + expirationDate: number; // The expiration date of the attestation. + subject: string; // The ID of the attestee, EVM address, DID, URL etc. + attestationData: object[]; // The attestation data. +}; + export type Attestation = { attestationId: string; // The unique identifier of the attestation. schemaId: string; // The identifier of the schema this attestation adheres to. replacedBy: string | null; // Whether the attestation was replaced by a new one. - Address: string; // The address issuing the attestation to the subject. - Address: string; // The id of the portal that created the attestation. + attester: Address; // The address issuing the attestation to the subject. + portal: Address; // The id of the portal that created the attestation. attestedDate: number; // The date the attestation is issued. expirationDate: number; // The expiration date of the attestation. revocationDate: number | null; // The date when the attestation was revoked. diff --git a/sdk/test/dataMapper/AttestationDataMapper.test.ts b/sdk/test/dataMapper/AttestationDataMapper.test.ts index 00b9895d..c49ccca4 100644 --- a/sdk/test/dataMapper/AttestationDataMapper.test.ts +++ b/sdk/test/dataMapper/AttestationDataMapper.test.ts @@ -1,11 +1,12 @@ import AttestationDataMapper from "../../src/dataMapper/AttestationDataMapper"; import VeraxSdk from "../../src/VeraxSdk"; -import { createPublicClient, PublicClient, http } from "viem"; -import { ApolloClient, InMemoryCache, ApolloQueryResult, gql } from "@apollo/client/core"; +import { createPublicClient, createWalletClient, http, PublicClient, WalletClient } from "viem"; +import { ApolloClient, ApolloQueryResult, gql, InMemoryCache } from "@apollo/client/core"; //TODO : This is a basic test example. mock data and assertions should be more precise describe("AttestationDataMapper", () => { let mockApolloClient: ApolloClient; let web3Client: PublicClient; + let walletClient: WalletClient; let attestationDataMapper: AttestationDataMapper; const typeName: string = "attestation"; const gqlInterface: string = `{ @@ -32,12 +33,23 @@ describe("AttestationDataMapper", () => { transport: http(), }); + // Create walletClient + walletClient = createWalletClient({ + chain: VeraxSdk.DEFAULT_LINEA_TESTNET.chain, + transport: http(), + }); + // Create a mock Apollo Client with an in-memory cache mockApolloClient = new ApolloClient({ cache: new InMemoryCache(), }); - attestationDataMapper = new AttestationDataMapper(VeraxSdk.DEFAULT_LINEA_TESTNET, web3Client, mockApolloClient); + attestationDataMapper = new AttestationDataMapper( + VeraxSdk.DEFAULT_LINEA_TESTNET, + web3Client, + walletClient, + mockApolloClient, + ); }); afterEach(() => {