diff --git a/packages/backend/codegen.yaml b/packages/backend/codegen.yaml index 1e2d44d32..d80c4d345 100644 --- a/packages/backend/codegen.yaml +++ b/packages/backend/codegen.yaml @@ -51,3 +51,7 @@ generates: Vote: ../governance#VoteModel DelegateStatement: ../delegateStatement#DelegateStatementModel + + RetroPGF: ../retroPGF#RetroPGFModel + Application: ../retroPGF#ApplicationModel + List: ../retroPGF#ListModel diff --git a/packages/backend/package.json b/packages/backend/package.json index 8ef5377f9..3aa61cd40 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -9,8 +9,8 @@ "build-dump-namespace": "esbuild --bundle --platform=node src/bin/scripts/dumpNamespace.ts --outfile=dist/dump.js", "dump-namespace": "node dist/dump.js", "check": "tsc --noEmit", - "fetch": "ts-node src/indexer/bin/fetch.ts OptimismGovernorTest && ts-node src/indexer/bin/fetch.ts GovernanceToken", - "fetch-prod": "ts-node src/indexer/bin/fetch.ts OptimismGovernor && ts-node src/indexer/bin/fetch.ts GovernanceToken", + "fetch": "ts-node src/indexer/bin/fetch.ts OptimismGovernorTest && ts-node src/indexer/bin/fetch.ts GovernanceToken && ts-node src/indexer/bin/fetch.ts EAS", + "fetch-prod": "ts-node src/indexer/bin/fetch.ts OptimismGovernor && ts-node src/indexer/bin/fetch.ts GovernanceToken && ts-node src/indexer/bin/fetch.ts EAS", "backfill": "ts-node src/indexer/bin/backfill.ts dev", "backfill-prod": "ts-node src/indexer/bin/backfill.ts prod", "tail": "ts-node src/indexer/bin/tail.ts", @@ -21,7 +21,8 @@ "server": "ts-node --require ./scripts/registerGraphQL.ts src/bin/server.ts", "generate-typechain": "typechain --target ethers-v5 --out-dir src/contracts/generated src/contracts/abis/*.json", "generate-schema-types": "graphql-codegen", - "generate": "yarn run generate-typechain && yarn run generate-schema-types", + "generate-prisma-types": "prisma generate", + "generate": "yarn run generate-typechain && yarn run generate-schema-types && yarn run generate-prisma-types", "build-worker": "node scripts/buildWorker.js", "visualize-worker-bundle": "esbuild-visualizer ---metadata ./dist/meta.json", "stream": "ts-node src/indexer/ops/bin/stream.ts", @@ -48,6 +49,7 @@ "@graphql-tools/merge": "^8.3.4", "@graphql-tools/stitch": "^8.7.7", "@graphql-yoga/node": "^2.13.12", + "@prisma/client": "^5.3.1", "@sentry/integrations": "7.28.1", "@types/seedrandom": "^3.0.5", "alchemy-sdk": "^2.2.5", @@ -58,6 +60,7 @@ "get-graphql-schema": "^2.1.2", "graphql": "^16.6.0", "heap": "^0.2.7", + "jose": "^4.14.6", "level": "^8.0.0", "limiter": "^2.1.0", "lmdb": "^2.7.7", @@ -68,6 +71,7 @@ "toucan-js": "^3.1.0", "uuid": "^9.0.0", "web3": "^1.8.1", + "siwe": "^2.1.4", "wrangler": "^2.9.0", "ws": "^8.12.0", "zod": "3.14.2" @@ -96,6 +100,7 @@ "graphql-request": "^5.0.0", "jest": "^29.0.3", "jest-junit": "^14.0.1", + "prisma": "^5.3.1", "ts-jest": "^29.0.0", "ts-node": "^10.9.1", "typechain": "^8.1.0", diff --git a/packages/backend/prisma/migrations/20230920145206_init/migration.sql b/packages/backend/prisma/migrations/20230920145206_init/migration.sql new file mode 100644 index 000000000..7965a5444 --- /dev/null +++ b/packages/backend/prisma/migrations/20230920145206_init/migration.sql @@ -0,0 +1,25 @@ +-- CreateTable +CREATE TABLE "ballots" ( + "address" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3), + "published_at" TIMESTAMP(3), + "votes" JSONB NOT NULL DEFAULT '[]', + + CONSTRAINT "ballots_pkey" PRIMARY KEY ("address") +); + +-- CreateTable +CREATE TABLE "likes" ( + "id" TEXT NOT NULL, + "addresses" TEXT[] DEFAULT ARRAY[]::TEXT[], + "last_updated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "likes_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ballots_address_key" ON "ballots"("address"); + +-- CreateIndex +CREATE UNIQUE INDEX "likes_id_key" ON "likes"("id"); diff --git a/packages/backend/prisma/migrations/20230921192003_added_signature_to_ballots/migration.sql b/packages/backend/prisma/migrations/20230921192003_added_signature_to_ballots/migration.sql new file mode 100644 index 000000000..f75d00304 --- /dev/null +++ b/packages/backend/prisma/migrations/20230921192003_added_signature_to_ballots/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "ballots" ADD COLUMN "signature" TEXT; diff --git a/packages/backend/prisma/migrations/migration_lock.toml b/packages/backend/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..fbffa92c2 --- /dev/null +++ b/packages/backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/packages/backend/prisma/schema.prisma b/packages/backend/prisma/schema.prisma new file mode 100644 index 000000000..88a5e4a96 --- /dev/null +++ b/packages/backend/prisma/schema.prisma @@ -0,0 +1,27 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model Ballot { + address String @id @unique + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime? @map("updated_at") + publishedAt DateTime? @map("published_at") + votes Json @default("[]") @db.JsonB + signature String? @map("signature") + + @@map("ballots") +} + +model Like { + id String @id @unique + addresses String[] @default([]) + lastUpdated DateTime @default(now()) @map("last_updated") + + @@map("likes") +} \ No newline at end of file diff --git a/packages/backend/src/bin/proxyServer.ts b/packages/backend/src/bin/proxyServer.ts new file mode 100644 index 000000000..e1e542cb7 --- /dev/null +++ b/packages/backend/src/bin/proxyServer.ts @@ -0,0 +1,23 @@ +import express from "express"; +import { createProxyMiddleware } from "http-proxy-middleware"; + +export function startProxyServer() { + const app = express(); + + app.use( + "/graphql", + createProxyMiddleware({ + target: "http://0.0.0.0:4002/", + changeOrigin: true, + }) + ); + app.use( + "/api", + createProxyMiddleware({ + target: "http://0.0.0.0:4003/", + changeOrigin: true, + }) + ); + + app.listen(4001); +} diff --git a/packages/backend/src/bin/restServer.ts b/packages/backend/src/bin/restServer.ts new file mode 100644 index 000000000..cd01dad39 --- /dev/null +++ b/packages/backend/src/bin/restServer.ts @@ -0,0 +1,260 @@ +import { ethers } from "ethers"; +import express from "express"; +import bodyParser from "body-parser"; +import { + makeSIWENonce, + verifySIWEMessage, + verifySIWESession, +} from "../services/auth"; +import { makeBallotService, votesSchema } from "../services/ballot"; +import { makeLikesService } from "../services/likes"; +import PrismaSingleton from "../store/prisma/client"; + +const app = express(); +const PORT = 4003; + +export function startRestServer(provider: ethers.providers.BaseProvider) { + PrismaSingleton.setConnectionUrl(process.env.DATABASE_URL!); + + app.use(bodyParser.json()); + app.use("/api/auth", authRoutes()); + app.use("/api/ballot", ballotRoutes(provider)); + app.use("/api/likes", likesRoutes()); + + app.listen(PORT, () => { + console.log(`REST server is running on http://localhost:${PORT}`); + }); +} + +// ---------------- +// Auth +// ---------------- + +function authRoutes() { + const router = express.Router(); + + router.get("/nonce", (_, res) => { + try { + const newNonce = makeSIWENonce(); + + // Setting the cookie + res.cookie("nonce", newNonce, { + path: "/", + httpOnly: true, + secure: true, + maxAge: 300 * 1000, // 5 minutes + }); + + res.json({ nonce: newNonce }); + } catch (error) { + res.status(500).json({ error }); + } + }); + + router.post("/verify", async (req, res) => { + try { + const { message, signature } = req.body; + + if (!message || !signature) { + return res.status(400).json({ error: "Invalid request" }); + } + + const cookies = req.headers.cookie; + + if (cookies && cookies.includes("nonce")) { + const nonce = cookies + .split("; ") + .find((row) => row.startsWith("nonce="))! + .split("=")[1]; + + const { success, jwt } = await verifySIWEMessage( + message, + signature, + nonce, + "secret" + ); + + if (success) { + res.cookie("access-token", jwt, { + path: "/", + httpOnly: true, + sameSite: "strict", + maxAge: 3600 * 1000, + }); + + return res.json({ success }); + } else { + return res.status(401).json({ error: "Invalid nonce or signature" }); + } + } else { + return res.status(401).json({ error: "Missing nonce cookie" }); + } + } catch (error) { + return res.status(500).json({ error }); + } + }); + + router.get("/session", async (req, res) => { + try { + const cookies = req.headers.cookie; + + if (cookies && cookies.includes("access-token")) { + const jwt = cookies + .split("; ") + .find((row) => row.startsWith("access-token="))! + .split("=")[1]; + + const session = await verifySIWESession(jwt, "secret"); + if (session) { + return res.json({ ...session.payload, chainId: 10 }); + } else { + return res + .status(401) + .json({ error: "Invalid or expired access token" }); + } + } else { + return res.status(401).json({ error: "Missing access token" }); + } + } catch (error) { + return res.status(500).json({ error }); + } + }); + + router.get("/signout", (_, res) => { + res.cookie("access-token", "", { + path: "/", + httpOnly: true, + secure: true, + maxAge: 0, + }); + + return res.json({ success: true }); + }); + + return router; +} + +// ---------------- +// Ballots +// ---------------- + +function ballotRoutes(provider: ethers.providers.BaseProvider) { + const router = express.Router(); + const ballotService = makeBallotService(); + + router.get("/:address", async (req, res) => { + const address = req.params.address; + try { + const ballot = await ballotService.getBallot(address); + return res.json({ ballot }); + } catch (error) { + return res.status(500).json({ error }); + } + }); + + router.post("/save", async (req, res) => { + const { address, votes } = req.body; + + const validationResult = votesSchema.safeParse(votes); + + if (!validationResult.success || !address) { + return res + .status(400) + .json({ error: "Bad request: incorrect ballot schema" }); + } + + try { + const savedBallot = await ballotService.saveBallot(address, votes); + return res.json({ ballot: savedBallot }); + } catch (error) { + return res.status(500).json({ error }); + } + }); + + router.post("/submit", async (req, res) => { + const { address, votes, signature } = req.body; + + const validationResult = votesSchema.safeParse(votes); + + if (!validationResult.success || !address || !signature) { + return res + .status(400) + .json({ error: "Bad request: incorrect ballot schema" }); + } + + try { + const submission = await ballotService.submitBallot( + address, + votes, + signature, + provider + ); + if (submission.error) { + return res + .status(submission.error.code) + .json({ error: submission.error.message }); + } + return res.json({ submission }); + } catch (error) { + return res.status(500).json({ error }); + } + }); + + return router; +} + +// ---------------- +// Likes +// ---------------- + +function likesRoutes() { + const router = express.Router(); + const likesService = makeLikesService(); + + router.post("/:listId/like", async (req, res) => { + const listId = req.params.listId; + + if (!listId) { + return res + .status(400) + .json({ error: "Bad request: address of listId is missing" }); + } + + // TODO: verify that listId exists + + const address = req.body.address; // TODO: get address from session + + try { + const like = await likesService.like({ listId, address }); + return res.json(like); + } catch (error) { + return res.status(500).json({ error }); + } + }); + + router.get("/:listId", async (req, res) => { + const listId = req.params.listId; + + if (!listId) { + return res.status(400).json({ error: "Bad request: listId is missing" }); + } + + try { + const likes = await likesService.getLikes(listId); + return res.json(likes); + } catch (error) { + return res.status(500).json({ error }); + } + }); + + router.get("/", async (_, res) => { + try { + const likes = await likesService.getAllLikes(); + return res.json(likes); + } catch (error) { + return res.status(500).json({ error }); + } + }); + + return router; +} diff --git a/packages/backend/src/bin/server.ts b/packages/backend/src/bin/server.ts index 28b70c7ba..7eeba0e51 100644 --- a/packages/backend/src/bin/server.ts +++ b/packages/backend/src/bin/server.ts @@ -20,18 +20,8 @@ import { timeout } from "../indexer/utils/asyncUtils"; import { EthersBlockProvider } from "../indexer/blockProvider/blockProvider"; import { EthersLogProvider } from "../indexer/logProvider/logProvider"; import { makeLatestBlockFetcher } from "../schema/latestBlockFetcher"; - -// p0 -// todo: where are delegate statements going to be stored? -// todo: replicate and deploy -// todo: snapshot votes, delegate statements, cached ens name lookups - -// todo: to load up a replica, have the durable object pull from an initial file from r2 using streams - -// p1 -// todo: derived state -// todo: joins -// todo: some cleanup in the governance folder +import { startRestServer } from "./restServer"; +import { startProxyServer } from "./proxyServer"; async function main() { const schema = makeGatewaySchema(); @@ -43,6 +33,8 @@ async function main() { ); const storageArea = await makeInitialStorageArea(store); + + // Start Indexer const blockProvider = new EthersBlockProvider(baseProvider); const logProvider = new EthersLogProvider(baseProvider); const iter = followChain( @@ -51,6 +43,7 @@ async function main() { entityDefinitions, blockProvider, logProvider, + baseProvider, storageArea, process.argv[2] || "dev" ); @@ -70,6 +63,7 @@ async function main() { const dynamoDb = new DynamoDB({}); + // Start GraphQL Server (Port 4002) const server = createServer({ schema, async context(): Promise { @@ -99,11 +93,17 @@ async function main() { tracingContext: makeEmptyTracingContext(), }; }, - port: 4001, + port: 4002, maskedErrors: false, plugins: [useTiming(), useApolloTracing(), useErrorInspection()], }); await server.start(); + + // Start REST Server (Port 4003) + startRestServer(baseProvider); + + // Start Proxy Server (Port 4001) + startProxyServer(); } main(); diff --git a/packages/backend/src/contracts.ts b/packages/backend/src/contracts.ts index b33a43a4d..9da08c3f7 100644 --- a/packages/backend/src/contracts.ts +++ b/packages/backend/src/contracts.ts @@ -24,11 +24,12 @@ export type Signatures = { export function filterForEventHandlers( instance: ContractInstance, - signatures: Signatures + signatures: Signatures, + topicFilters: (string[] | null)[] = [] ): TopicFilter { return { address: [instance.address], - topics: [topicsForSignatures(instance.iface, signatures)], + topics: [topicsForSignatures(instance.iface, signatures), ...topicFilters], }; } diff --git a/packages/backend/src/contracts/abis/EAS.json b/packages/backend/src/contracts/abis/EAS.json new file mode 100644 index 000000000..896f0b83f --- /dev/null +++ b/packages/backend/src/contracts/abis/EAS.json @@ -0,0 +1,593 @@ +[ + { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, + { "inputs": [], "name": "AccessDenied", "type": "error" }, + { "inputs": [], "name": "AlreadyRevoked", "type": "error" }, + { "inputs": [], "name": "AlreadyRevokedOffchain", "type": "error" }, + { "inputs": [], "name": "AlreadyTimestamped", "type": "error" }, + { "inputs": [], "name": "InsufficientValue", "type": "error" }, + { "inputs": [], "name": "InvalidAttestation", "type": "error" }, + { "inputs": [], "name": "InvalidAttestations", "type": "error" }, + { "inputs": [], "name": "InvalidExpirationTime", "type": "error" }, + { "inputs": [], "name": "InvalidLength", "type": "error" }, + { "inputs": [], "name": "InvalidOffset", "type": "error" }, + { "inputs": [], "name": "InvalidRegistry", "type": "error" }, + { "inputs": [], "name": "InvalidRevocation", "type": "error" }, + { "inputs": [], "name": "InvalidRevocations", "type": "error" }, + { "inputs": [], "name": "InvalidSchema", "type": "error" }, + { "inputs": [], "name": "InvalidSignature", "type": "error" }, + { "inputs": [], "name": "InvalidVerifier", "type": "error" }, + { "inputs": [], "name": "Irrevocable", "type": "error" }, + { "inputs": [], "name": "NotFound", "type": "error" }, + { "inputs": [], "name": "NotPayable", "type": "error" }, + { "inputs": [], "name": "WrongSchema", "type": "error" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "attester", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "uid", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "schema", + "type": "bytes32" + } + ], + "name": "Attested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "attester", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "uid", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "schema", + "type": "bytes32" + } + ], + "name": "Revoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "revoker", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "data", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + } + ], + "name": "RevokedOffchain", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "data", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + } + ], + "name": "Timestamped", + "type": "event" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "schema", "type": "bytes32" }, + { + "components": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint64", + "name": "expirationTime", + "type": "uint64" + }, + { "internalType": "bool", "name": "revocable", "type": "bool" }, + { + "internalType": "bytes32", + "name": "refUID", + "type": "bytes32" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "internalType": "struct AttestationRequestData", + "name": "data", + "type": "tuple" + } + ], + "internalType": "struct AttestationRequest", + "name": "request", + "type": "tuple" + } + ], + "name": "attest", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "schema", "type": "bytes32" }, + { + "components": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint64", + "name": "expirationTime", + "type": "uint64" + }, + { "internalType": "bool", "name": "revocable", "type": "bool" }, + { + "internalType": "bytes32", + "name": "refUID", + "type": "bytes32" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "internalType": "struct AttestationRequestData", + "name": "data", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "internalType": "struct EIP712Signature", + "name": "signature", + "type": "tuple" + }, + { "internalType": "address", "name": "attester", "type": "address" } + ], + "internalType": "struct DelegatedAttestationRequest", + "name": "delegatedRequest", + "type": "tuple" + } + ], + "name": "attestByDelegation", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "getAttestTypeHash", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "uid", "type": "bytes32" }], + "name": "getAttestation", + "outputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "uid", "type": "bytes32" }, + { "internalType": "bytes32", "name": "schema", "type": "bytes32" }, + { "internalType": "uint64", "name": "time", "type": "uint64" }, + { + "internalType": "uint64", + "name": "expirationTime", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "revocationTime", + "type": "uint64" + }, + { "internalType": "bytes32", "name": "refUID", "type": "bytes32" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "address", "name": "attester", "type": "address" }, + { "internalType": "bool", "name": "revocable", "type": "bool" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "internalType": "struct Attestation", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getDomainSeparator", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getName", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "getNonce", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "revoker", "type": "address" }, + { "internalType": "bytes32", "name": "data", "type": "bytes32" } + ], + "name": "getRevokeOffchain", + "outputs": [{ "internalType": "uint64", "name": "", "type": "uint64" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getRevokeTypeHash", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getSchemaRegistry", + "outputs": [ + { + "internalType": "contract ISchemaRegistry", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "data", "type": "bytes32" } + ], + "name": "getTimestamp", + "outputs": [{ "internalType": "uint64", "name": "", "type": "uint64" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "uid", "type": "bytes32" }], + "name": "isAttestationValid", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "schema", "type": "bytes32" }, + { + "components": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint64", + "name": "expirationTime", + "type": "uint64" + }, + { "internalType": "bool", "name": "revocable", "type": "bool" }, + { + "internalType": "bytes32", + "name": "refUID", + "type": "bytes32" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "internalType": "struct AttestationRequestData[]", + "name": "data", + "type": "tuple[]" + } + ], + "internalType": "struct MultiAttestationRequest[]", + "name": "multiRequests", + "type": "tuple[]" + } + ], + "name": "multiAttest", + "outputs": [ + { "internalType": "bytes32[]", "name": "", "type": "bytes32[]" } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "schema", "type": "bytes32" }, + { + "components": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint64", + "name": "expirationTime", + "type": "uint64" + }, + { "internalType": "bool", "name": "revocable", "type": "bool" }, + { + "internalType": "bytes32", + "name": "refUID", + "type": "bytes32" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "internalType": "struct AttestationRequestData[]", + "name": "data", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "internalType": "struct EIP712Signature[]", + "name": "signatures", + "type": "tuple[]" + }, + { "internalType": "address", "name": "attester", "type": "address" } + ], + "internalType": "struct MultiDelegatedAttestationRequest[]", + "name": "multiDelegatedRequests", + "type": "tuple[]" + } + ], + "name": "multiAttestByDelegation", + "outputs": [ + { "internalType": "bytes32[]", "name": "", "type": "bytes32[]" } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "schema", "type": "bytes32" }, + { + "components": [ + { "internalType": "bytes32", "name": "uid", "type": "bytes32" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "internalType": "struct RevocationRequestData[]", + "name": "data", + "type": "tuple[]" + } + ], + "internalType": "struct MultiRevocationRequest[]", + "name": "multiRequests", + "type": "tuple[]" + } + ], + "name": "multiRevoke", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "schema", "type": "bytes32" }, + { + "components": [ + { "internalType": "bytes32", "name": "uid", "type": "bytes32" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "internalType": "struct RevocationRequestData[]", + "name": "data", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "internalType": "struct EIP712Signature[]", + "name": "signatures", + "type": "tuple[]" + }, + { "internalType": "address", "name": "revoker", "type": "address" } + ], + "internalType": "struct MultiDelegatedRevocationRequest[]", + "name": "multiDelegatedRequests", + "type": "tuple[]" + } + ], + "name": "multiRevokeByDelegation", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32[]", "name": "data", "type": "bytes32[]" } + ], + "name": "multiRevokeOffchain", + "outputs": [{ "internalType": "uint64", "name": "", "type": "uint64" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32[]", "name": "data", "type": "bytes32[]" } + ], + "name": "multiTimestamp", + "outputs": [{ "internalType": "uint64", "name": "", "type": "uint64" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "schema", "type": "bytes32" }, + { + "components": [ + { "internalType": "bytes32", "name": "uid", "type": "bytes32" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "internalType": "struct RevocationRequestData", + "name": "data", + "type": "tuple" + } + ], + "internalType": "struct RevocationRequest", + "name": "request", + "type": "tuple" + } + ], + "name": "revoke", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "schema", "type": "bytes32" }, + { + "components": [ + { "internalType": "bytes32", "name": "uid", "type": "bytes32" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "internalType": "struct RevocationRequestData", + "name": "data", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "internalType": "struct EIP712Signature", + "name": "signature", + "type": "tuple" + }, + { "internalType": "address", "name": "revoker", "type": "address" } + ], + "internalType": "struct DelegatedRevocationRequest", + "name": "delegatedRequest", + "type": "tuple" + } + ], + "name": "revokeByDelegation", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "data", "type": "bytes32" } + ], + "name": "revokeOffchain", + "outputs": [{ "internalType": "uint64", "name": "", "type": "uint64" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "data", "type": "bytes32" } + ], + "name": "timestamp", + "outputs": [{ "internalType": "uint64", "name": "", "type": "uint64" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + } +] diff --git a/packages/backend/src/indexer/bin/backfill.ts b/packages/backend/src/indexer/bin/backfill.ts index 830400acc..fcec65994 100644 --- a/packages/backend/src/indexer/bin/backfill.ts +++ b/packages/backend/src/indexer/bin/backfill.ts @@ -11,6 +11,7 @@ import { StructuredError } from "../utils/errorUtils"; import { ethers } from "ethers"; import { LevelEntityStore } from "../storage/level/levelEntityStore"; import { makeProgressBar } from "../utils/progressBarUtils"; +import { makeDataFetcher } from "../dataFetcher/dataFetcher"; /** * Backfills updates from fetched logs starting from the last finalized block @@ -18,6 +19,13 @@ import { makeProgressBar } from "../utils/progressBarUtils"; * to the network. */ async function main() { + const provider = new ethers.providers.AlchemyProvider( + "optimism", + process.env.ALCHEMY_API_KEY + ); + + const dataFetcher = makeDataFetcher(provider); + const store = await LevelEntityStore.open(); const envIndexers = indexers.flatMap((it) => { @@ -84,7 +92,12 @@ async function main() { ); try { - await eventHandler.handle(storageHandle, event as any, log); + await eventHandler.handle( + storageHandle, + event as any, + log, + dataFetcher + ); } catch (e) { throw new StructuredError( { diff --git a/packages/backend/src/indexer/bin/fetch.ts b/packages/backend/src/indexer/bin/fetch.ts index 0d3639bc2..50886ca4d 100644 --- a/packages/backend/src/indexer/bin/fetch.ts +++ b/packages/backend/src/indexer/bin/fetch.ts @@ -38,7 +38,8 @@ async function main() { const filter = filterForEventHandlers( indexer, - indexer.eventHandlers.map((handler) => handler.signature) + indexer.eventHandlers.map((handler) => handler.signature), + indexer.topicsFilter ); const latestBlock = await provider.getBlock("latest"); diff --git a/packages/backend/src/indexer/bin/tail.ts b/packages/backend/src/indexer/bin/tail.ts index 3d7f9d026..d8ad8cc3c 100644 --- a/packages/backend/src/indexer/bin/tail.ts +++ b/packages/backend/src/indexer/bin/tail.ts @@ -26,6 +26,7 @@ async function main() { entityDefinitions, blockProvider, logProvider, + provider, storageArea, process.argv[2] || "dev" ); diff --git a/packages/backend/src/indexer/contracts/EAS.ts b/packages/backend/src/indexer/contracts/EAS.ts new file mode 100644 index 000000000..aed1d8412 --- /dev/null +++ b/packages/backend/src/indexer/contracts/EAS.ts @@ -0,0 +1,360 @@ +import { ethers, BigNumber } from "ethers"; +import { makeContractInstance } from "../../contracts"; +import { EAS__factory } from "../../contracts/generated"; +import { makeEntityDefinition, makeIndexerDefinition } from "../process"; +import * as serde from "../serde"; + +const EASContract = makeContractInstance({ + iface: EAS__factory.createInterface(), + address: "0x4200000000000000000000000000000000000021", + startingBlock: 6490467, +}); + +const rpgfSchemas: { [key: string]: string } = { + "0xfdcfdad2dbe7489e0ce56b260348b7f14e8365a8a325aef9834818c00d46b31b": + "BADGEHOLDERS", + "0x76e98cce95f3ba992c2ee25cef25f756495147608a3da3aa2e5ca43109fe77cc": + "APPLICATION", + "0x3e3e2172aebb902cf7aa6e1820809c5b469af139e7a4265442b1c22b97c6b2a5": "LISTS", +}; + +const badgeholderAttester = "0x621477dBA416E12df7FF0d48E14c4D20DC85D7D9"; + +export const EASIndexer = makeIndexerDefinition(EASContract, { + name: "EAS", + + entities: { + Badgeholder: makeEntityDefinition({ + serde: serde.object({ + uid: serde.string, + recipient: serde.string, + attester: serde.string, + schema: serde.string, + revokedAtBlock: serde.nullable(serde.bigNumber), + + rpgfRound: serde.number, + referredBy: serde.string, + referredMethod: serde.string, + }), + indexes: [ + { + indexName: "byRecipient", + indexKey({ recipient }) { + return recipient; + }, + }, + ], + }), + + Application: makeEntityDefinition({ + serde: serde.object({ + uid: serde.string, + recipient: serde.string, + attester: serde.string, + schema: serde.string, + revokedAtBlock: serde.nullable(serde.bigNumber), + + displayName: serde.string, + applicationMetadataPtrType: serde.number, + applicationMetadataPtr: serde.string, + applicantType: serde.string, + websiteUrl: serde.string, + bio: serde.string, + contributionDescription: serde.string, + contributionLinks: serde.array( + serde.object({ + type: serde.string, + url: serde.string, + description: serde.string, + }) + ), + impactCategory: serde.array(serde.string), + impactDescription: serde.string, + impactMetrics: serde.array( + serde.object({ + description: serde.string, + number: serde.number, + url: serde.string, + }) + ), + fundingSources: serde.array( + serde.object({ + type: serde.string, + currency: serde.string, + amount: serde.number, + description: serde.string, + }) + ), + payoutAddress: serde.string, + understoodKYCRequirements: serde.boolean, + understoodFundClaimPeriod: serde.boolean, + certifiedNotDesignatedOrSanctionedOrBlocked: serde.boolean, + certifiedNotSponsoredByPoliticalFigureOrGovernmentEntity: serde.boolean, + certifiedNotBarredFromParticipating: serde.boolean, + }), + indexes: [ + { + indexName: "byDisplayName", + indexKey({ displayName }) { + return displayName; + }, + }, + { + indexName: "byApplicantType", + indexKey({ applicantType }) { + return applicantType; + }, + }, + { + indexName: "byImpactCategory", + indexKey({ impactCategory }) { + return impactCategory[0]; // TODO: support multiple categories + }, + }, + ], + }), + + List: makeEntityDefinition({ + serde: serde.object({ + uid: serde.string, + recipient: serde.string, + attester: serde.string, + schema: serde.string, + revokedAtBlock: serde.nullable(serde.bigNumber), + + listName: serde.string, + listDescription: serde.string, + impactEvaluationDescription: serde.string, + impactEvaluationLink: serde.string, + listContent: serde.array( + serde.object({ + RPGF3_Application_UID: serde.string, + OPAmount: serde.number, + }) + ), + }), + indexes: [ + { + indexName: "byListName", + indexKey({ listName }) { + return listName; + }, + }, + ], + }), + }, + + // This filter is applied to all events. If addition events are added that do not require filtering, the filter should be removed + topicsFilter: [null, null, Object.keys(rpgfSchemas)], + + eventHandlers: [ + { + signature: "Attested(address,address,bytes32,bytes32)", + async handle( + handle, + { args: { recipient, attester, uid, schema } }, + _, + { easDataFetcher, asyncDataFetcher } + ) { + const schemaName = rpgfSchemas[schema]; + + if (schemaName) { + const schemaData = await easDataFetcher.getEASData(uid); + + switch (schemaName) { + case "BADGEHOLDERS": { + const badgeholderSchema = decodeBadgeholderSchema(schemaData); + + if (attester == badgeholderAttester) { + handle.saveEntity("Badgeholder", uid, { + uid, + recipient, + attester, + schema, + revokedAtBlock: null, + + rpgfRound: badgeholderSchema.rpgfRound, + referredBy: badgeholderSchema.referredBy, + referredMethod: badgeholderSchema.referredMethod, + }); + } + + break; + } + case "APPLICATION": { + const applicationSchema = decodeApplicationSchema(schemaData); + const applicaitonData = await asyncDataFetcher.getApplicationData( + applicationSchema.applicationMetadataPtr + ); + + if (!applicaitonData) { + throw new Error( + `Application data not found for ${applicationSchema.displayName}` + ); + } + + handle.saveEntity("Application", uid, { + uid, + recipient, + attester, + schema, + revokedAtBlock: null, + + displayName: applicationSchema.displayName, + applicationMetadataPtrType: + applicationSchema.applicationMetadataPtrType, + applicationMetadataPtr: + applicationSchema.applicationMetadataPtr, + applicantType: applicaitonData.applicantType, + websiteUrl: applicaitonData.websiteUrl, + bio: applicaitonData.bio, + contributionDescription: + applicaitonData.contributionDescription, + contributionLinks: applicaitonData.contributionLinks, + impactCategory: applicaitonData.impactCategory, + impactDescription: applicaitonData.impactDescription, + impactMetrics: applicaitonData.impactMetrics, + fundingSources: applicaitonData.fundingSources, + payoutAddress: applicaitonData.payoutAddress, + understoodKYCRequirements: + applicaitonData.understoodKYCRequirements, + understoodFundClaimPeriod: + applicaitonData.understoodFundClaimPeriod, + certifiedNotDesignatedOrSanctionedOrBlocked: + applicaitonData.certifiedNotDesignatedOrSanctionedOrBlocked, + certifiedNotSponsoredByPoliticalFigureOrGovernmentEntity: + applicaitonData.certifiedNotSponsoredByPoliticalFigureOrGovernmentEntity, + certifiedNotBarredFromParticipating: + applicaitonData.certifiedNotBarredFromParticipating, + }); + + break; + } + case "LISTS": { + const listSchema = decodeListSchema(schemaData); + const listData = await asyncDataFetcher.getListData( + listSchema.listMetadataPtr + ); + + if (!listData) { + throw new Error( + `Application data not found for ${listSchema.listName}` + ); + } + + handle.saveEntity("List", uid, { + uid, + recipient, + attester, + schema, + revokedAtBlock: null, + + listName: listData.listName, + listDescription: listData.listDescription, + impactEvaluationDescription: + listData.impactEvaluationDescription, + impactEvaluationLink: listData.impactEvaluationLink, + listContent: listData.listContent, + }); + + break; + } + default: { + break; + } + } + } + }, + }, + + { + signature: "Revoked(address,address,bytes32,bytes32)", + async handle(handle, { args: { uid } }, log) { + const badgeholder = await handle.loadEntity("Badgeholder", uid); + const application = await handle.loadEntity("Application", uid); + const list = await handle.loadEntity("List", uid); + + if (badgeholder) { + handle.saveEntity("Badgeholder", uid, { + ...badgeholder, + revokedAtBlock: BigNumber.from(log.blockNumber), + }); + } + + if (application) { + handle.saveEntity("Application", uid, { + ...application, + revokedAtBlock: BigNumber.from(log.blockNumber), + }); + } + + if (list) { + handle.saveEntity("List", uid, { + ...list, + revokedAtBlock: BigNumber.from(log.blockNumber), + }); + } + }, + }, + ], +}); + +export function decodeBadgeholderSchema(schemaData: string): BadgeholderSchema { + const signature = + "SCHEMA_ENCODING(string rpgfRound,address referredBy,string referredMethod)"; + + const dataArgs = ethers.utils.FunctionFragment.fromString(signature); + + const decodedData = ethers.utils.defaultAbiCoder.decode( + dataArgs.inputs, + schemaData + ) as ethers.utils.Result; + + return decodedData; +} + +interface BadgeholderSchema { + rpgfRound: number; + referredBy: string; + referredMethod: string; +} + +export function decodeApplicationSchema(schemaData: string): ApplicaitonSchema { + const signature = + "SCHEMA_ENCODING(string displayName,uint256 applicationMetadataPtrType,string applicationMetadataPtr)"; + + const dataArgs = ethers.utils.FunctionFragment.fromString(signature); + + const decodedData = ethers.utils.defaultAbiCoder.decode( + dataArgs.inputs, + schemaData + ) as ethers.utils.Result; + + return decodedData; +} + +interface ApplicaitonSchema { + displayName: string; + applicationMetadataPtrType: number; + applicationMetadataPtr: string; +} + +export function decodeListSchema(schemaData: string): ListSchema { + const signature = + "SCHEMA_ENCODING(string listName,uint256 listMetadataPtrType,string listMetadataPtr)"; + + const dataArgs = ethers.utils.FunctionFragment.fromString(signature); + + const decodedData = ethers.utils.defaultAbiCoder.decode( + dataArgs.inputs, + schemaData + ) as ethers.utils.Result; + + return decodedData; +} + +interface ListSchema { + listName: string; + listMetadataPtrType: number; + listMetadataPtr: string; +} diff --git a/packages/backend/src/indexer/contracts/decode.test.ts b/packages/backend/src/indexer/contracts/decode.test.ts index 14febdd10..64abd41ff 100644 --- a/packages/backend/src/indexer/contracts/decode.test.ts +++ b/packages/backend/src/indexer/contracts/decode.test.ts @@ -1,5 +1,10 @@ import { ethers } from "ethers"; import { decodeProposalData, decodeVoteParams } from "./OptimismGovernor"; +import { + decodeApplicationSchema, + decodeBadgeholderSchema, + decodeListSchema, +} from "./EAS"; describe("Decode Proposal Data", () => { it("decodes ProposalData", () => { @@ -53,4 +58,34 @@ describe("Decode Proposal Data", () => { expect(typeof option).toBe("number"); }); }); + + it("decodes BadgeholderSchema", () => { + const data = + "0x00000000000000000000000000000000000000000000000000000000000000600000000000000000000000005e349eca2dc61abcd9dd99ce94d04136151a09ee00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000124261646765686f6c6465722043686f6963650000000000000000000000000000"; + const decodedData = decodeBadgeholderSchema(data); + + expect(decodedData).toHaveProperty("rpgfRound"); + expect(decodedData).toHaveProperty("referredBy"); + expect(decodedData).toHaveProperty("referredMethod"); + }); + + it("decodes ApplicationSchema", () => { + const data = + "0x0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000d43727970746f5a6f6d6269657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b68747470733a2f2f636f6e74656e742e6f7074696d69736d2e696f2f72706766334170706c69636174696f6e2f76302f6d657461646174612f31302f3078626231303545336336443634303641334630614142393863443844304533613132376244363433362e6a736f6e000000000000000000000000000000000000000000"; + const decodedData = decodeApplicationSchema(data); + + expect(decodedData).toHaveProperty("displayName"); + expect(decodedData).toHaveProperty("applicationMetadataPtrType"); + expect(decodedData).toHaveProperty("applicationMetadataPtr"); + }); + + it("decodes ListSchema", () => { + const data = + "0x0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000d43727970746f5a6f6d6269657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b68747470733a2f2f636f6e74656e742e6f7074696d69736d2e696f2f72706766334170706c69636174696f6e2f76302f6d657461646174612f31302f3078626231303545336336443634303641334630614142393863443844304533613132376244363433362e6a736f6e000000000000000000000000000000000000000000"; + const decodedData = decodeListSchema(data); + + expect(decodedData).toHaveProperty("listName"); + expect(decodedData).toHaveProperty("listMetadataPtrType"); + expect(decodedData).toHaveProperty("listMetadataPtr"); + }); }); diff --git a/packages/backend/src/indexer/contracts/index.ts b/packages/backend/src/indexer/contracts/index.ts index deb8bc229..6cd7b8eeb 100644 --- a/packages/backend/src/indexer/contracts/index.ts +++ b/packages/backend/src/indexer/contracts/index.ts @@ -2,6 +2,7 @@ import { governanceTokenIndexer } from "./GovernanceToken"; import { governorIndexer } from "./OptimismGovernor"; import { IndexerDefinition } from "../process"; import { governorIndexerTest } from "./OptimismGovernorTest"; +import { EASIndexer } from "./EAS"; export const indexers: IndexerDefinition[] = [ // @ts-ignore @@ -10,9 +11,12 @@ export const indexers: IndexerDefinition[] = [ governorIndexer, // @ts-ignore governorIndexerTest, + // @ts-ignore + EASIndexer, ]; export const entityDefinitions = { ...governanceTokenIndexer.entities, ...governorIndexer.entities, + ...EASIndexer.entities, }; diff --git a/packages/backend/src/indexer/dataFetcher/asyncDataFetcher.ts b/packages/backend/src/indexer/dataFetcher/asyncDataFetcher.ts new file mode 100644 index 000000000..114823bf6 --- /dev/null +++ b/packages/backend/src/indexer/dataFetcher/asyncDataFetcher.ts @@ -0,0 +1,75 @@ +import { z } from "zod"; + +export type AsyncDataFetcher = { + getApplicationData: ( + url: string + ) => Promise; + getListData: (url: string) => Promise; +}; + +export function asyncDataFetcher(): AsyncDataFetcher { + return { + getApplicationData: async (url: string) => { + const res = await fetch(url); + const data = await res.json(); + + return applicationData.parse(data); + }, + getListData: async (url: string) => { + const res = await fetch(url); + const data = await res.json(); + + return listData.parse(data); + }, + }; +} + +const applicationData = z.object({ + applicantType: z.string(), + websiteUrl: z.string(), + bio: z.string(), + contributionDescription: z.string(), + contributionLinks: z.array( + z.object({ + type: z.string(), + url: z.string(), + description: z.string(), + }) + ), + impactCategory: z.array(z.string()), + impactDescription: z.string(), + impactMetrics: z.array( + z.object({ + description: z.string(), + number: z.number(), + url: z.string(), + }) + ), + fundingSources: z.array( + z.object({ + type: z.string(), + currency: z.string(), + amount: z.number(), + description: z.string(), + }) + ), + payoutAddress: z.string(), + understoodKYCRequirements: z.boolean(), + understoodFundClaimPeriod: z.boolean(), + certifiedNotDesignatedOrSanctionedOrBlocked: z.boolean(), + certifiedNotSponsoredByPoliticalFigureOrGovernmentEntity: z.boolean(), + certifiedNotBarredFromParticipating: z.boolean(), +}); + +const listData = z.object({ + listName: z.string(), + listDescription: z.string(), + impactEvaluationDescription: z.string(), + impactEvaluationLink: z.string(), + listContent: z.array( + z.object({ + RPGF3_Application_UID: z.string(), + OPAmount: z.number(), + }) + ), +}); diff --git a/packages/backend/src/indexer/dataFetcher/dataFetcher.ts b/packages/backend/src/indexer/dataFetcher/dataFetcher.ts new file mode 100644 index 000000000..a32aa4eba --- /dev/null +++ b/packages/backend/src/indexer/dataFetcher/dataFetcher.ts @@ -0,0 +1,17 @@ +import { ethers } from "ethers"; +import { EASDataFetcher, easDataFetcher } from "./easDataFetcher"; +import { AsyncDataFetcher, asyncDataFetcher } from "./asyncDataFetcher"; + +export type DataFetcher = { + asyncDataFetcher: AsyncDataFetcher; + easDataFetcher: EASDataFetcher; +}; + +export function makeDataFetcher( + provider: ethers.providers.BaseProvider +): DataFetcher { + return { + asyncDataFetcher: asyncDataFetcher(), + easDataFetcher: easDataFetcher(provider), + }; +} diff --git a/packages/backend/src/indexer/dataFetcher/easDataFetcher.ts b/packages/backend/src/indexer/dataFetcher/easDataFetcher.ts new file mode 100644 index 000000000..715022867 --- /dev/null +++ b/packages/backend/src/indexer/dataFetcher/easDataFetcher.ts @@ -0,0 +1,21 @@ +import { ethers } from "ethers"; +import { EAS__factory } from "../../contracts/generated"; + +export type EASDataFetcher = { + getEASData: (uid: string) => Promise; +}; + +export function easDataFetcher( + provider: ethers.providers.BaseProvider +): EASDataFetcher { + return { + getEASData: async (uid: string) => { + const easContract = EAS__factory.connect( + "0x4200000000000000000000000000000000000021", + provider + ); + + return (await easContract.getAttestation(uid)).data; + }, + }; +} diff --git a/packages/backend/src/indexer/followChain.test.ts b/packages/backend/src/indexer/followChain.test.ts index e1bc67a80..26d6c3508 100644 --- a/packages/backend/src/indexer/followChain.test.ts +++ b/packages/backend/src/indexer/followChain.test.ts @@ -1,3 +1,4 @@ +import { ethers } from "ethers"; import { followChain, makeInitialStorageArea } from "./followChain"; import { DurableObjectEntityStore } from "./storage/durableObjects/durableObjectEntityStore"; import { MemoryStorage } from "./storage/durableObjects/memoryStorage"; @@ -58,6 +59,8 @@ const testContractIndexer = makeIndexerDefinition(testContractInstance, { describe("followChain", () => { it("handles processing events", async () => { + const provider = new ethers.providers.JsonRpcProvider(); + const initialFinalizedBlock = makeBlockIdentifier(finalizedBlockNumber); const memoryStorage = new MemoryStorage( @@ -78,6 +81,7 @@ describe("followChain", () => { entityDefinitions, blockProvider, logProvider, + provider, storageArea, "dev" ); diff --git a/packages/backend/src/indexer/followChain.ts b/packages/backend/src/indexer/followChain.ts index c9c733fe5..782eae3c2 100644 --- a/packages/backend/src/indexer/followChain.ts +++ b/packages/backend/src/indexer/followChain.ts @@ -26,6 +26,7 @@ import { import { ethers } from "ethers"; import { StructuredError } from "./utils/errorUtils"; import { Toucan } from "toucan-js"; +import { makeDataFetcher } from "./dataFetcher/dataFetcher"; export function followChain( store: EntityStore, @@ -33,6 +34,7 @@ export function followChain( entityDefinitions: EntitiesType, blockProvider: BlockProvider, logProvider: LogProvider, + ethProvider: ethers.providers.BaseProvider, storageArea: StorageArea, env: String, sentry?: Toucan @@ -50,6 +52,7 @@ export function followChain( block: BlockProviderBlock, logsCache?: Map ) { + const dataFetcher = makeDataFetcher(ethProvider); const [storageHandle, loadedEntities] = makeStorageHandleForStorageArea( storageArea, block, @@ -73,7 +76,12 @@ export function followChain( )!; try { - await eventHandler.handle(storageHandle, event as any, log); + await eventHandler.handle( + storageHandle, + event as any, + log, + dataFetcher + ); } catch (e) { throw new StructuredError( { diff --git a/packages/backend/src/indexer/logProvider/fakeLogProvider.ts b/packages/backend/src/indexer/logProvider/fakeLogProvider.ts index de46857ce..cafa64560 100644 --- a/packages/backend/src/indexer/logProvider/fakeLogProvider.ts +++ b/packages/backend/src/indexer/logProvider/fakeLogProvider.ts @@ -31,11 +31,11 @@ export function normalizeTopics(filter: TopicFilter) { * @param log */ export function logMatchesTopics( - normalizedTopics: string[][], + normalizedTopics: (null | string)[][], log: ethers.providers.Log ): boolean { for (const [idx, segment] of normalizedTopics.entries()) { - if (!segment.includes(log.topics[idx])) { + if (!segment || !segment.includes(log.topics[idx])) { return false; } } diff --git a/packages/backend/src/indexer/logProvider/logProvider.ts b/packages/backend/src/indexer/logProvider/logProvider.ts index 0ea8d3a10..65fc03cbf 100644 --- a/packages/backend/src/indexer/logProvider/logProvider.ts +++ b/packages/backend/src/indexer/logProvider/logProvider.ts @@ -9,7 +9,7 @@ export type LogFilter = BlockSpec & TopicFilter; export type TopicFilter = { address?: string[]; - topics?: Array | Array>; + topics?: Array | Array | null>; }; export type BlockSpec = diff --git a/packages/backend/src/indexer/process.ts b/packages/backend/src/indexer/process.ts index 6eebdbb70..1755cfafd 100644 --- a/packages/backend/src/indexer/process.ts +++ b/packages/backend/src/indexer/process.ts @@ -2,6 +2,7 @@ import { ContractInstance, TypedInterface } from "../contracts"; import { ethers } from "ethers"; import { StorageHandle } from "./storageHandle"; import { RuntimeType, SerDe } from "./serde"; +import { DataFetcher } from "./dataFetcher/dataFetcher"; // The latest block is at depth zero with the block depth of each block below // latest defined as LATEST.blockNumber - OTHER.blockNumber. @@ -35,6 +36,7 @@ type IndexerDefinitionArgs< name: string; entities: Entities; eventHandlers: EventHandler[]; + topicsFilter?: (null | string[])[]; }; export type StorageHandleEntities = { @@ -69,7 +71,8 @@ type EventHandler< K, EventFragmentArg >, - log: ethers.providers.Log + log: ethers.providers.Log, + dataFetcher: DataFetcher ) => Promise | void; }; }[keyof InterfaceType["events"] & string]; diff --git a/packages/backend/src/indexer/serde/index.ts b/packages/backend/src/indexer/serde/index.ts index 58045a17d..f726e8916 100644 --- a/packages/backend/src/indexer/serde/index.ts +++ b/packages/backend/src/indexer/serde/index.ts @@ -6,3 +6,4 @@ export * from "./primitives"; export * from "./constant"; export * from "./bigNumber"; export * from "./discriminatedUnion"; +export * from "./nullable"; diff --git a/packages/backend/src/indexer/serde/nullable.ts b/packages/backend/src/indexer/serde/nullable.ts new file mode 100644 index 000000000..96881b5e1 --- /dev/null +++ b/packages/backend/src/indexer/serde/nullable.ts @@ -0,0 +1,14 @@ +import { SerDe } from "./types"; + +export function nullable( + serde: SerDe +): SerDe { + return { + serialize(item: T | null): TSerialized | null { + return item === null ? null : serde.serialize(item); + }, + deserialize(serialized: TSerialized | null): T | null { + return serialized === null ? null : serde.deserialize(serialized); + }, + }; +} diff --git a/packages/backend/src/indexer/serde/primitives.ts b/packages/backend/src/indexer/serde/primitives.ts index 804bd64c1..7f8f072fd 100644 --- a/packages/backend/src/indexer/serde/primitives.ts +++ b/packages/backend/src/indexer/serde/primitives.ts @@ -4,6 +4,8 @@ export const number = passthrough(); export const string = passthrough(); +export const boolean = passthrough(); + export function passthrough(): SerDe { return { serialize(item) { diff --git a/packages/backend/src/indexer/utils/generatorUtils.ts b/packages/backend/src/indexer/utils/generatorUtils.ts index 39461d511..999949dca 100644 --- a/packages/backend/src/indexer/utils/generatorUtils.ts +++ b/packages/backend/src/indexer/utils/generatorUtils.ts @@ -178,9 +178,9 @@ export async function takeLast(gen: AsyncIterable): Promise { export async function* mapGenerator( generator: AsyncGenerator, - mapper: (item: T) => U + mapper: (item: T) => Promise | U ): AsyncGenerator { for await (const item of generator) { - yield mapper(item); + yield await mapper(item); } } diff --git a/packages/backend/src/schema/index.ts b/packages/backend/src/schema/index.ts index 005d810dc..e6f6c3747 100644 --- a/packages/backend/src/schema/index.ts +++ b/packages/backend/src/schema/index.ts @@ -4,6 +4,7 @@ import * as scalars from "./resolvers/scalars"; import * as commonResolvers from "./resolvers/common"; import * as governanceResolvers from "./resolvers/governance"; import * as delegateStatement from "./resolvers/delegateStatement"; +import * as retroPGFResolvers from "./resolvers/retroPGF"; import { attachTracingContextInjection } from "./transformers/tracingContext"; import { applyIdPrefix } from "./transformers/applyIdPrefix"; import { makeExecutableSchema } from "@graphql-tools/schema"; @@ -18,6 +19,7 @@ export const resolvers: Resolvers = { ...scalars, ...commonResolvers, ...delegateStatement, + ...retroPGFResolvers, }; export function makeGatewaySchema() { diff --git a/packages/backend/src/schema/resolvers/governance.ts b/packages/backend/src/schema/resolvers/governance.ts index edb5e7ceb..3b01772cb 100644 --- a/packages/backend/src/schema/resolvers/governance.ts +++ b/packages/backend/src/schema/resolvers/governance.ts @@ -233,6 +233,10 @@ export const Query: QueryResolvers = { after ?? null ); }, + + retroPGF() { + return {}; + }, }; export type DelegateModel = RuntimeType< diff --git a/packages/backend/src/schema/resolvers/retroPGF.ts b/packages/backend/src/schema/resolvers/retroPGF.ts new file mode 100644 index 000000000..666f16126 --- /dev/null +++ b/packages/backend/src/schema/resolvers/retroPGF.ts @@ -0,0 +1,70 @@ +import { ethers, BigNumber } from "ethers"; +import { entityDefinitions } from "../../indexer/contracts"; +import { RuntimeType } from "../../indexer/serde"; +import { + collectGenerator, + mapGenerator, +} from "../../indexer/utils/generatorUtils"; +import { ApplicationResolvers, RetroPgfResolvers } from "./generated/types"; + +export type RetroPGFModel = {}; + +export const RetroPGF: RetroPgfResolvers = { + async badgeholders(_parent, _args, { reader }) { + const delegates = ( + await collectGenerator( + reader.getEntitiesByIndex("Badgeholder", "byRecipient", {}) + ) + ).map(async (it) => { + const delegate = await reader.getEntity("Address", it.value.recipient); + if (!delegate) { + return { + address: it.value.recipient, + tokensOwned: BigNumber.from(0), + tokensRepresented: BigNumber.from(0), + delegatingTo: ethers.constants.AddressZero, + accountsRepresentedCount: BigNumber.from(0), + }; + } + return delegate; + }); + + return delegates; + }, + + applications(_parent, _args, { reader }) { + return collectGenerator( + mapGenerator( + reader.getEntitiesByIndex("Application", "byDisplayName", {}), + (it) => it.value + ) + ); + }, + + lists(_parent, _args, { reader }) { + return collectGenerator( + mapGenerator( + reader.getEntitiesByIndex("List", "byDisplayName", {}), + (it) => it.value + ) + ); + }, +}; + +export type ApplicationModel = RuntimeType< + typeof entityDefinitions["Application"]["serde"] +>; + +export const Application: ApplicationResolvers = { + id({ uid }) { + return uid; + }, +}; + +export type ListModel = RuntimeType; + +export const List: ApplicationResolvers = { + id({ uid }) { + return uid; + }, +}; diff --git a/packages/backend/src/schema/schema.graphql b/packages/backend/src/schema/schema.graphql index 3a8d2701c..bcfce13aa 100644 --- a/packages/backend/src/schema/schema.graphql +++ b/packages/backend/src/schema/schema.graphql @@ -18,6 +18,8 @@ type Query { proposals: [Proposal!]! metrics: Metrics! + + retroPGF: RetroPGF! } type Delegate { @@ -314,6 +316,65 @@ type ApprovalVotingProposalOption { budgetTokensSpent: TokenAmount! } +type RetroPGF { + badgeholders: [Delegate!]! + applications: [Application!]! + lists: [List!]! +} + +type Application { + id: ID! + displayName: String! + applicantType: String! + websiteUrl: String! + bio: String! + contributionDescription: String! + contributionLinks: [ContributionLink!]! + impactCategory: [String!]! + impactDescription: String! + impactMetrics: [ImpactMetric!]! + fundingSources: [FundingSource!] + payoutAddress: String! + understoodKYCRequirements: Boolean! + understoodFundClaimPeriod: Boolean! + certifiedNotDesignatedOrSanctionedOrBlocked: Boolean! + certifiedNotSponsoredByPoliticalFigureOrGovernmentEntity: Boolean! + certifiedNotBarredFromParticipating: Boolean! +} + +type ContributionLink { + type: String! + url: String! + description: String! +} + +type ImpactMetric { + description: String! + number: Int! + url: String! +} + +type FundingSource { + type: String! + currency: String! + amount: Int! + description: String! +} + +type List { + id: ID! + listName: String! + listDescription: String! + impactEvaluationDescription: String! + impactEvaluationLink: String! + listContent: [ListConent!]! +} + +type ListConent { + RPGF3_Application_UID: String! + OPAmount: Int! +} + type Mutation { createNewDelegateStatement(data: CreateNewDelegateStatementData!): Delegate! } diff --git a/packages/backend/src/services/auth.ts b/packages/backend/src/services/auth.ts new file mode 100644 index 000000000..fd617ab79 --- /dev/null +++ b/packages/backend/src/services/auth.ts @@ -0,0 +1,52 @@ +import { SiweMessage, generateNonce } from "siwe"; +import { SignJWT, jwtVerify } from "jose"; + +// ---------------- +// Nonce +// ---------------- + +export function makeSIWENonce() { + return generateNonce(); +} + +// ---------------- +// Verify +// ---------------- + +export async function verifySIWEMessage( + reqMessage: string, + signature: string, + nonce: string, + jwtSecret: string +) { + let SIWEObject = new SiweMessage(reqMessage); + const { data: message, success } = await SIWEObject.verify({ + signature, + nonce, + }); + + if (success) { + const expiryTimespan = message.expirationTime + ? new Date(message.expirationTime).getTime() - Date.now() + : 3600 * 1000 * 2; // defaults to 2 hours + const expiry = Math.floor(Date.now() / 1000) + expiryTimespan; + const secret = new TextEncoder().encode(jwtSecret); + const jwtToken = await new SignJWT({ address: message.address }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime(expiry) + .sign(secret); + + return { success, jwt: jwtToken, expiryTimespan }; + } else { + return { success: false }; + } +} + +// ---------------- +// Session +// ---------------- + +export async function verifySIWESession(token: string, jwtSecret: string) { + const secret = new TextEncoder().encode(jwtSecret); + return await jwtVerify(token, secret); +} diff --git a/packages/backend/src/services/ballot.ts b/packages/backend/src/services/ballot.ts new file mode 100644 index 000000000..795b085a5 --- /dev/null +++ b/packages/backend/src/services/ballot.ts @@ -0,0 +1,183 @@ +import { ethers } from "ethers"; +import { z } from "zod"; +import { DefaultArgs } from "@envelop/core"; +import { Prisma } from "@prisma/client"; +import PrismaSingleton from "../store/prisma/client"; +import { validateSigned } from "../utils/signing"; + +type Ballot = { + address: string; + createdAt: Date; + updatedAt: Date | null; + publishedAt: Date | null; + votes: Vote[]; +}; + +type Vote = { + projectId: string; + amount: string; +}; + +type Submission = { + success: boolean; + address: string; + timestamp: Date; + error?: { + code: number; + message: string; + }; +}; + +export type BallotsService = { + getBallot(address: string): Promise; + saveBallot(address: string, votes: Vote[]): Promise; + submitBallot( + address: string, + votes: Vote[], + signature: string, + provider: ethers.providers.BaseProvider + ): Promise; +}; + +export function makeBallotService(): BallotsService { + return { + async getBallot(address: string): Promise { + const ballot = await PrismaSingleton.instance.ballot.findUnique({ + where: { address }, + }); + + if (ballot) { + return toBallotType(ballot); + } else { + return toBallotType( + await PrismaSingleton.instance.ballot.create({ + data: { address }, + }) + ); + } + }, + + async saveBallot(address: string, votes: Vote[]): Promise { + return toBallotType( + await PrismaSingleton.instance.ballot.upsert({ + where: { address }, + update: { + votes: votes, + updatedAt: new Date(), + }, + create: { + address, + votes, + }, + }) + ); + }, + + async submitBallot( + address: string, + votes: Vote[], + signature: string, + provider: ethers.providers.BaseProvider + ): Promise { + // Verify signature + try { + const code = await provider.getCode(address); + + await validateSigned(provider, { + signerAddress: address, + value: JSON.stringify(votes, undefined, "\t"), + signature, + signatureType: code === "0x" ? "EOA" : ("CONTRACT" as any), + }); + } catch (error) { + return { + success: false, + address, + timestamp: new Date(), + error: { + code: 401, + message: "Invalid signature", + }, + }; + } + + const existingBallot = await PrismaSingleton.instance.ballot.findFirst({ + where: { + address, + }, + }); + + if (!existingBallot || !existingBallot.signature) { + const ballot = await PrismaSingleton.instance.ballot.upsert({ + where: { + address, + }, + update: { + signature, + votes, + updatedAt: new Date(), + publishedAt: new Date(), + }, + create: { + address, + votes, + signature, + updatedAt: new Date(), + publishedAt: new Date(), + }, + }); + + if (ballot) { + return { + success: true, + address, + timestamp: new Date(), + }; + } + + return { + success: false, + address, + timestamp: new Date(), + error: { + code: 500, + message: "Internal Server Error", + }, + }; + } else { + return { + success: false, + address, + timestamp: new Date(), + error: { + code: 409, + message: "Already voted", + }, + }; + } + }, + }; +} + +function toBallotType( + prismaBallot: Prisma.BallotGetPayload +): Ballot { + return { + ...prismaBallot, + votes: prismaBallot.votes as Vote[], + }; +} + +export const votesSchema = z.array( + z.object({ + projectId: z.string(), + amount: z.string(), + }) +); + +export const ballotSchema = z + .object({ + address: z.string(), + votes: votesSchema, + }) + .strict(); diff --git a/packages/backend/src/services/likes.ts b/packages/backend/src/services/likes.ts new file mode 100644 index 000000000..4ce1c6ce5 --- /dev/null +++ b/packages/backend/src/services/likes.ts @@ -0,0 +1,67 @@ +import { DefaultArgs } from "@envelop/core"; +import { Prisma } from "@prisma/client/edge"; +import PrismaSingleton from "../store/prisma/client"; + +type Like = { + listId: boolean; +}; + +type Likes = { + [listId: string]: string[]; +}; + +export type LikesService = { + like({ listId, address }: { listId: string; address: string }): Promise; + getLikes(listId: string): Promise; + getAllLikes(): Promise; +}; + +export function makeLikesService(): LikesService { + return { + async like({ listId, address }: { listId: string; address: string }) { + const result = await PrismaSingleton.instance.$queryRaw< + Prisma.LikeGetPayload[] + >( + Prisma.sql` + INSERT INTO likes (id, addresses) + VALUES (${listId}, ARRAY[${address}::TEXT]) + ON CONFLICT (id) + DO UPDATE + SET addresses = CASE + WHEN likes.addresses @> ARRAY[${address}::TEXT] THEN + array_remove(likes.addresses, ${address}::TEXT) + ELSE + array_append(likes.addresses, ${address}::TEXT) + END + RETURNING *; + ` + ); + + const returnedLike = result[0]; + return { listId: returnedLike.addresses.includes(address) }; + }, + + async getLikes(listId: string) { + const like = await PrismaSingleton.instance.like.findUnique({ + where: { id: listId }, + }); + + if (like) { + return { [listId]: like.addresses }; + } else { + return { [listId]: [] }; + } + }, + + async getAllLikes() { + const likesData = await PrismaSingleton.instance.like.findMany(); + + const likes: Likes = {}; + likesData.forEach((like) => { + likes[like.id] = like.addresses; + }); + + return likes; + }, + }; +} diff --git a/packages/backend/src/store/prisma/client.ts b/packages/backend/src/store/prisma/client.ts new file mode 100644 index 000000000..9c3ba306f --- /dev/null +++ b/packages/backend/src/store/prisma/client.ts @@ -0,0 +1,45 @@ +import { PrismaClient } from "@prisma/client/edge"; + +class PrismaSingleton { + private static prismaInstance: any; + private static connectionUrl: string | undefined; + + private constructor() {} + + public static setConnectionUrl(connectionUrl: string): void { + if (!PrismaSingleton.connectionUrl) + PrismaSingleton.connectionUrl = connectionUrl; + } + + public static get instance(): PrismaClient { + if (!PrismaSingleton.prismaInstance) { + if (!PrismaSingleton.connectionUrl) { + throw new Error("DATABASE_URL is not set"); + } + + PrismaSingleton.prismaInstance = new PrismaClient({ + datasources: { + db: { + url: this.connectionUrl, + }, + }, + }); + } + + return PrismaSingleton.prismaInstance; + } + + public static get isConnected(): boolean { + return !!PrismaSingleton.prismaInstance; + } + + public static get getConnectionUrl(): string | undefined { + return PrismaSingleton.connectionUrl; + } + + public static get isInstanceConnected(): boolean { + return !!PrismaSingleton.prismaInstance?.$connect; + } +} + +export default PrismaSingleton; diff --git a/packages/backend/src/utils/signing.ts b/packages/backend/src/utils/signing.ts index b6bb22829..0aee2f647 100644 --- a/packages/backend/src/utils/signing.ts +++ b/packages/backend/src/utils/signing.ts @@ -42,9 +42,9 @@ export async function validateSigned( { signerAddress, value, signature, signatureType }: ValueWithSignature ): Promise { const parsedSignature = ethers.utils.arrayify(signature); - const hashedMessage = hashEnvelopeValue(value); if (signatureType === "CONTRACT") { + const hashedMessage = hashEnvelopeValue(value); const safe = GnosisSafe__factory.connect(signerAddress, provider); const isSigned = await checkSafeSignature(safe, hashedMessage, signature); if (!isSigned) { @@ -58,7 +58,7 @@ export async function validateSigned( signatureType, }; } else { - const address = ethers.utils.verifyMessage(hashedMessage, parsedSignature); + const address = ethers.utils.verifyMessage(value, parsedSignature); if (address.toLowerCase() !== signerAddress.toLowerCase()) { throw new Error("signature address does not match signer address"); } diff --git a/packages/backend/src/worker/durableObject.ts b/packages/backend/src/worker/durableObject.ts index 88f279840..9cf40e85f 100644 --- a/packages/backend/src/worker/durableObject.ts +++ b/packages/backend/src/worker/durableObject.ts @@ -18,6 +18,7 @@ import { } from "../indexer/utils/generatorUtils"; import { EthersBlockProvider } from "../indexer/blockProvider/blockProvider"; import { EthersLogProvider } from "../indexer/logProvider/logProvider"; +import { makeDataFetcher } from "../indexer/dataFetcher/dataFetcher"; export const blockUpdateIntervalSeconds = 4; @@ -224,6 +225,7 @@ export class StorageDurableObjectV1 { entityDefinitions, blockProvider, logProvider, + this.provider, storageArea, this.env.ENVIRONMENT, toucan diff --git a/packages/backend/src/worker/env.ts b/packages/backend/src/worker/env.ts index 64de1d7d8..b0950ab81 100644 --- a/packages/backend/src/worker/env.ts +++ b/packages/backend/src/worker/env.ts @@ -29,6 +29,12 @@ export interface Env { TENDERLY_USER: string; TENDERLY_PROJECT: string; TENDERLY_ACCESS_KEY: string; + + /** + * API + */ + JWT_SECRET: string; + DATABASE_URL: string; } export function safelyLoadBlockStepSize(env: Env): number | undefined { diff --git a/packages/backend/src/worker/fetch.ts b/packages/backend/src/worker/fetch.ts index 6a56044f4..be44a560e 100644 --- a/packages/backend/src/worker/fetch.ts +++ b/packages/backend/src/worker/fetch.ts @@ -1,13 +1,22 @@ -import { Env, shouldUseCache } from "./env"; +import { ethers } from "ethers"; +import { Env, mustGetAlchemyApiKey, shouldUseCache } from "./env"; import { getAssetFromKV } from "@cloudflare/kv-asset-handler"; import { fetchThroughCache } from "./cache"; import { simulateTransaction } from "./tenderly"; import { fetchMessage } from "./genosis"; import manifestJSON from "__STATIC_CONTENT_MANIFEST"; +import { handleAuthRequest } from "./rpgfApi/auth"; +import PrismaSingleton from "../store/prisma/client"; +import { handleBallotsRequest } from "./rpgfApi/ballots"; +import { handleLikesRequest } from "./rpgfApi/likes"; +import { createResponse } from "./rpgfApi/utils"; + const assetManifest = JSON.parse(manifestJSON); export async function fetch(request: Request, env: Env, ctx: ExecutionContext) { + PrismaSingleton.setConnectionUrl(env.DATABASE_URL!); + const url = new URL(request.url); const name = request.headers.get("x-durable-object-instance-name") || @@ -22,11 +31,7 @@ export async function fetch(request: Request, env: Env, ctx: ExecutionContext) { accessKey: env.TENDERLY_ACCESS_KEY, }); - return new Response(JSON.stringify(result), { - headers: { - "content-type": "application/json", - }, - }); + return createResponse(result, 200); } if (url.pathname === "/fetch_signature") { @@ -37,23 +42,35 @@ export async function fetch(request: Request, env: Env, ctx: ExecutionContext) { }); if (result.error) { - return new Response(JSON.stringify({ error: result.error }), { - status: 500, - headers: { "content-type": "application/json" }, - }); + return createResponse(result.error, 500); } - return new Response(JSON.stringify(result.response), { - headers: { - "content-type": "application/json", - }, - }); + return createResponse(result.response as any, 200); } - return new Response(JSON.stringify({ message: "Invalid request" }), { - status: 400, - headers: { "content-type": "application/json" }, - }); + return createResponse("Invalid request", 400); + } + + // ---------------- + // RPGF API + // ---------------- + + if (url.pathname.startsWith("/api/")) { + const path = url.pathname.split("/").slice(1); + + switch (path[1]) { + case "auth": + return await handleAuthRequest(path[2], request, env); + + case "ballot": + return await handleBallotsRequest(request, env); + + case "likes": + return await handleLikesRequest(request, env); + + default: + return createResponse({ error: `Invalid path: ${url.pathname}` }, 400); + } } if ( diff --git a/packages/backend/src/worker/rpgfApi/auth.ts b/packages/backend/src/worker/rpgfApi/auth.ts new file mode 100644 index 000000000..a4b603753 --- /dev/null +++ b/packages/backend/src/worker/rpgfApi/auth.ts @@ -0,0 +1,144 @@ +import { Env } from "../env"; +import { + makeSIWENonce, + verifySIWEMessage, + verifySIWESession, +} from "../../services/auth"; +import { createResponse } from "./utils"; + +export async function handleAuthRequest( + path: string, + request: Request, + env: Env +) { + switch (path) { + case "nonce": + return handleNonceRequest(); + + case "verify": + return handleVerifyRequest(request, env); + + case "session": + return handleSessionRequest(request, env); + + case "signout": + return handleSignOut(); + + default: + return createResponse( + { error: `Invalid path: ${new URL(request.url).pathname}` }, + 400 + ); + } +} + +// ---------------- +// Nonce +// ---------------- + +async function handleNonceRequest() { + try { + const nonce = await makeSIWENonce(); + + return createResponse({ nonce }, 200, { + "Set-Cookie": `nonce=${nonce}; Path=/; HttpOnly; Secure; SameSite=Strict; max-age=300`, + }); + } catch (error) { + return createResponse({ error }, 500); + } +} + +// ---------------- +// Verify +// ---------------- + +async function handleVerifyRequest( + request: Request, + env: Env +): Promise { + try { + const { message, signature } = (await request.json()) as any; + + if (!message || !signature) { + return createResponse({ error: "Invalid request" }, 400); + } + + const cookies = request.headers.get("Cookie"); + if (cookies && cookies.includes("nonce")) { + const nonce = cookies + .split("; ") + .find((row) => row.startsWith("nonce="))! + .split("=")[1]; + + const { success, jwt } = await verifySIWEMessage( + message, + signature, + nonce, + env.JWT_SECRET + ); + if (success) { + return createResponse({ success }, 200, { + "Set-Cookie": `access-token=${jwt}; Path=/; HttpOnly; Secure; SameSite=Strict; max-age=3600`, // TODO: define max-age + }); + } else { + return createResponse({ error: "Invalid nonce or signature" }, 401); + } + } else { + return createResponse({ error: "Missing nonce cookie" }, 401); + } + } catch (error) { + return createResponse({ error }, 500); + } +} + +// ---------------- +// Session +// ---------------- + +async function handleSessionRequest( + request: Request, + env: Env +): Promise { + try { + // Verify JWT & return session + const cookies = request.headers.get("Cookie"); + if (cookies && cookies.includes("access-token")) { + const jwt = cookies + .split("; ") + .find((row) => row.startsWith("access-token="))! + .split("=")[1]; + + try { + const session = await verifySIWESession(jwt, env.JWT_SECRET); + if (session) { + return createResponse({ session: session.payload }); + } else { + return createResponse( + { error: "Invalid or expired access token" }, + 401 + ); + } + } catch (error) { + return createResponse( + { error: "Invalid or expired access token" }, + 401 + ); + } + } else { + return createResponse({ error: "Missing access token" }, 401); + } + } catch (error) { + return createResponse({ error }, 500); + } +} + +// ---------------- +// Signout +// ---------------- + +async function handleSignOut() { + // Remove cookies + return createResponse({ success: true }, 200, { + "Set-Cookie": `access-token=; Path=/; HttpOnly; Secure; SameSite=Strict; max-age=0`, + }); +} diff --git a/packages/backend/src/worker/rpgfApi/ballots.ts b/packages/backend/src/worker/rpgfApi/ballots.ts new file mode 100644 index 000000000..4f51c6475 --- /dev/null +++ b/packages/backend/src/worker/rpgfApi/ballots.ts @@ -0,0 +1,108 @@ +import { ethers } from "ethers"; +import { makeBallotService, votesSchema } from "../../services/ballot"; +import { Env, mustGetAlchemyApiKey } from "../env"; +import { authWrap, createResponse, Handler, handlerWrap } from "./utils"; + +export async function handleBallotsRequest(request: Request, env: Env) { + const path = new URL(request.url).pathname.split("/")[3]; + + switch (path) { + case "save": + return wrappedSaveHandler(request, env); + + case "submit": + return wrappedSubmitHandler(request, env); + + default: + return wrappedGetHandler(request, env); + } +} + +// ---------------- +// Get +// ---------------- + +const handleGetRequest: Handler = async ( + request: Request, + env: Env, + address?: string +) => { + if (!address) { + return createResponse({ error: "Address is missing" }, 400); + } + return createResponse(await makeBallotService().getBallot(address)); +}; + +const wrappedGetHandler = authWrap(handlerWrap(handleGetRequest)); + +// ---------------- +// Save +// ---------------- + +const handleSaveRequest: Handler = async ( + request: Request, + env: Env, + address?: string +): Promise => { + const { votes } = (await request.json()) as any; + + const validationResult = votesSchema.safeParse(votes); + + if (!validationResult.success || !address) { + return createResponse( + { error: "Bad request: incorrect ballot schema" }, + 400 + ); + } + + return createResponse({ + ballot: await makeBallotService().saveBallot(address, votes), + }); +}; + +const wrappedSaveHandler = authWrap(handlerWrap(handleSaveRequest)); + +// ---------------- +// Submit +// ---------------- + +const handleSubmitRequest: Handler = async ( + request: Request, + env: Env +): Promise => { + const provider = new ethers.providers.AlchemyProvider( + "optimism", + mustGetAlchemyApiKey(env) + ); + + const { votes, signature, address } = (await request.json()) as any; + + const validationResult = votesSchema.safeParse(votes); + + if (!validationResult.success || !address || !signature) { + return createResponse( + { error: "Bad request: incorrect ballot schema" }, + 400 + ); + } + try { + const submission = await makeBallotService().submitBallot( + address, + votes, + signature, + provider + ); + + if (submission.error) { + return createResponse({ error: submission.error }, submission.error.code); + } + + return createResponse({ + submission, + }); + } catch (error) { + return createResponse({ error }, 500); + } +}; + +const wrappedSubmitHandler = handlerWrap(handleSubmitRequest); diff --git a/packages/backend/src/worker/rpgfApi/likes.ts b/packages/backend/src/worker/rpgfApi/likes.ts new file mode 100644 index 000000000..88ccf8a87 --- /dev/null +++ b/packages/backend/src/worker/rpgfApi/likes.ts @@ -0,0 +1,59 @@ +import { ethers } from "ethers"; +import { makeLikesService } from "../../services/likes"; +import { Env } from "../env"; +import { authWrap, createResponse, Handler, handlerWrap } from "./utils"; + +export async function handleLikesRequest(request: Request, env: Env) { + const path = new URL(request.url).pathname.split("/")[4]; + switch (path) { + case "like": + return wrappedLikeHandler(request, env); + + default: + return wrappedGetHandler(request, env); + } +} + +// ---------------- +// Get +// ---------------- + +const handleGetRequest: Handler = async (request: Request, env: Env) => { + const listId = new URL(request.url).pathname.split("/")[3]; + if (!listId) { + return createResponse(await makeLikesService().getAllLikes()); + } + + return createResponse(await makeLikesService().getLikes(listId)); +}; + +const wrappedGetHandler = handlerWrap(handleGetRequest); + +// ---------------- +// Like +// ---------------- + +const handleLikeRequest: Handler = async ( + request: Request, + env: Env, + address?: string +): Promise => { + const listId = new URL(request.url).pathname.split("/")[3]; + + if (!listId || !address) { + return createResponse( + { + error: "Bad request: address of listId is missing", + listId, + address, + }, + 400 + ); + } + + // TODO: verify that listId exists + + return createResponse(await makeLikesService().like({ listId, address })); +}; + +const wrappedLikeHandler = authWrap(handlerWrap(handleLikeRequest)); diff --git a/packages/backend/src/worker/rpgfApi/utils.ts b/packages/backend/src/worker/rpgfApi/utils.ts new file mode 100644 index 000000000..eff176dc5 --- /dev/null +++ b/packages/backend/src/worker/rpgfApi/utils.ts @@ -0,0 +1,72 @@ +import { verifySIWESession } from "../../services/auth"; +import { Env } from "../env"; + +export type Handler = ( + request: Request, + env: Env, + address?: string +) => Promise; + +export function createResponse( + body: string | object, + status: number = 200, + headers: Record = {} +) { + return new Response(JSON.stringify(body), { + status, + headers: { + "content-type": "application/json", + ...headers, + }, + }); +} + +export function authWrap(asyncFn: Handler): Handler { + const wrapper = async (request: Request, env: Env) => { + try { + // Verify JWT & forward address to handler + const cookies = request.headers.get("Cookie"); + if (cookies && cookies.includes("access-token")) { + const jwt = cookies + .split("; ") + .find((row) => row.startsWith("access-token="))! + .split("=")[1]; + + try { + const session = await verifySIWESession(jwt, env.JWT_SECRET); + if (session?.payload?.address) { + return asyncFn(request, env, session.payload.address as string); + } else { + return createResponse( + { error: "Invalid or expired access token" }, + 401 + ); + } + } catch (error) { + return createResponse( + { error: "Invalid or expired access token" }, + 401 + ); + } + } else { + return createResponse({ error: "Missing access token" }, 401); + } + } catch (error) { + return createResponse({ error }, 500); + } + }; + + return wrapper; +} + +export function handlerWrap(asyncFunction: Handler): Handler { + const wrapper = async (request: Request, env: Env, address?: string) => { + try { + return await asyncFunction(request, env, address); + } catch (error) { + return createResponse({ error }, 500); + } + }; + + return wrapper; +} diff --git a/packages/backend/wrangler.toml b/packages/backend/wrangler.toml index c2d7cf5f1..3f688f22f 100644 --- a/packages/backend/wrangler.toml +++ b/packages/backend/wrangler.toml @@ -16,7 +16,7 @@ durable_objects.bindings = [ ENVIRONMENT = "dev" DEPLOYMENT = "optimism" ADMIN_API_KEY = '' -PRIMARY_DURABLE_OBJECT_INSTANCE_NAME = 'stable7' +PRIMARY_DURABLE_OBJECT_INSTANCE_NAME = 'stable8' BLOCK_STEP_SIZE = "100" ALLOW_READS_PERCENTAGE = "100" USE_CACHE_PERCENTAGE = "100" diff --git a/packages/frontend/package.json b/packages/frontend/package.json index aa8600bac..113434625 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -24,7 +24,7 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/react-window-infinite-loader": "^1.0.6", - "connectkit": "1.3.0", + "connectkit": "1.5.2", "date-fns": "^2.29.2", "ethers": "^5.7.2", "framer-motion": "^7.2.1", @@ -47,9 +47,10 @@ "recoil": "^0.7.5", "relay-runtime": "^14.1.0", "remark-breaks": "^3.0.2", + "siwe": "^2.1.4", "source-map-explorer": "^2.5.2", "typescript": "^4.4.2", - "wagmi": "^0.12.13", + "wagmi": "^1.3.11", "web-vitals": "^2.1.0" }, "relay": { @@ -88,6 +89,8 @@ "browserslist": { "production": [ ">0.2%", + "not ie <= 99", + "not android <= 4.4.4", "not dead", "not op_mini all" ], diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 0658e435c..88d4f90b8 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -2,8 +2,13 @@ import React, { Suspense } from "react"; import { RelayEnvironmentProvider } from "react-relay/hooks"; import { relayEnvironment } from "./relayEnvironment"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { createClient, WagmiConfig } from "wagmi"; -import { ConnectKitProvider, getDefaultClient } from "connectkit"; +import { createConfig, WagmiConfig } from "wagmi"; +import { + ConnectKitProvider, + SIWEConfig, + SIWEProvider, + getDefaultConfig, +} from "connectkit"; import { PageContainer } from "./components/PageContainer"; import { PageHeader } from "./components/PageHeader"; import { @@ -16,9 +21,10 @@ import { RecoilRoot } from "recoil"; import { DialogProvider } from "./components/DialogProvider/DialogProvider"; import { optimism } from "wagmi/chains"; import { ENSAvatarProvider } from "./components/ENSAvatar"; +import { SiweMessage } from "siwe"; -const wagmiClient = createClient( - getDefaultClient({ +const wagmiClient = createConfig( + getDefaultConfig({ appName: "Agora", walletConnectProjectId: process.env.REACT_APP_WALLETCONNECT_PROJECT_ID || "", @@ -27,6 +33,43 @@ const wagmiClient = createClient( }) ); +const siweConfig: SIWEConfig = { + getNonce: async () => + fetch("api/auth/nonce").then(async (res) => { + const result = await res.json(); + console.log(result); + return result.nonce; + }), + createMessage: ({ nonce, address, chainId }) => { + const message = new SiweMessage({ + version: "1", + domain: window.location.host, + uri: window.location.origin, + address, + chainId, + nonce, + statement: "Sign in to Agora Optimism", + }).prepareMessage(); + + console.log("create message", message); + + return message; + }, + verifyMessage: async ({ message, signature }) => + fetch("api/auth/verify", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ message, signature }), + }).then((res) => { + console.log(res); + return res.json(); + }), + getSession: async () => fetch("api/auth/session").then((res) => res.json()), + signOut: async () => fetch("api/auth/signout").then((res) => res.json()), +}; + function App() { const queryClient = new QueryClient(); @@ -34,24 +77,26 @@ function App() { - + - - - - - - - }> - - - - - - - - - + + + + + + + + }> + + + + + + + + + + diff --git a/packages/frontend/src/components/PageHeader.tsx b/packages/frontend/src/components/PageHeader.tsx index 9417419bc..38231ab76 100644 --- a/packages/frontend/src/components/PageHeader.tsx +++ b/packages/frontend/src/components/PageHeader.tsx @@ -2,17 +2,19 @@ import { css } from "@emotion/css"; import * as theme from "../theme"; import logo from "../logo.svg"; import graphql from "babel-plugin-relay/macro"; -import { ConnectKitButton } from "connectkit"; +import { ConnectKitButton, SIWESession, useModal, useSIWE } from "connectkit"; import { useAccount } from "wagmi"; import { useLazyLoadQuery } from "react-relay/hooks"; -import { PageHeaderQuery } from "./__generated__/PageHeaderQuery.graphql"; +import { + PageHeaderQuery, + PageHeaderQuery$data, +} from "./__generated__/PageHeaderQuery.graphql"; import { HStack } from "./VStack"; import { Link } from "./HammockRouter/Link"; import { useLocation } from "./HammockRouter/HammockRouter"; import { icons } from "../icons/icons"; import { ProfileDropDownButton } from "./ProfileDropDownButton"; import { MobileProfileDropDownButton } from "./MobileProfileDropDownButton"; -import React from "react"; export const orgName = "Optimism"; @@ -178,43 +180,44 @@ export const DesktopButton = () => { ); return ( - - {({ isConnected, isConnecting, show, hide, address, ensName }) => ( -
- {!accountAddress && ( -
- Connect Wallet -
- )} - {accountAddress && delegate && ( - - )} -
- )} -
+ + // + // {({ isConnected, isConnecting, show, hide, address, ensName }) => ( + //
+ // {!accountAddress && ( + //
+ // Connect Wallet + //
+ // )} + // {accountAddress && delegate && ( + // + // )} + //
+ // )} + //
); }; @@ -273,3 +276,113 @@ export const MobileButton = () => { ); }; + +const CustomSIWEButton = ({ + delegate, +}: { + delegate: PageHeaderQuery$data["delegate"]; +}) => { + const { setOpen } = useModal(); + const { isConnected } = useAccount(); + + const { + data, + // isReady, + isRejected, + isLoading, + isSignedIn, + signOut, + signIn, + status, + error, + } = useSIWE({ + onSignIn: (session?: SIWESession) => { + // Do something with the data + console.log(session); + console.log("need to sign?"); + }, + onSignOut: () => { + // Do something when signed out + }, + }); + + const handleSignIn = async () => { + await signIn()?.then((session?: SIWESession) => { + // Do something when signed in + console.log(session); + console.log("signed in"); + console.log(status); + console.log(error); + }); + }; + + const handleSignOut = async () => { + await signOut()?.then(() => { + // Do something when signed out + }); + }; + + /** Wallet is connected and signed in */ + if (isSignedIn) { + return ( +
+ {/* {!accountAddress && ( +
+ Connect Wallet +
+ )} */} + {delegate && ( + + )} +
+ // <> + //
Address: {data?.address}
+ //
ChainId: {data?.chainId}
+ // + // + ); + } + + /** Wallet is connected, but not signed in */ + if (isConnected) { + return ( + <> + + + ); + } + + /** A wallet needs to be connected first */ + return ( + <> + + + ); +}; diff --git a/packages/frontend/src/components/ProfileDropDownButton.tsx b/packages/frontend/src/components/ProfileDropDownButton.tsx index 6423e29bb..a3a2480ee 100644 --- a/packages/frontend/src/components/ProfileDropDownButton.tsx +++ b/packages/frontend/src/components/ProfileDropDownButton.tsx @@ -21,14 +21,11 @@ import { AnimatePresence, motion } from "framer-motion"; import { ethers } from "ethers"; type Props = { - isConnected: boolean; - isConnecting: boolean; - show?: () => void; - hide?: () => void; address: `0x${string}` | undefined; ensName: string | undefined; fragment: ProfileDropDownButtonFragment$key; hasStatment?: boolean; + handleSignOut?: () => void; }; const ValueWrapper = ({ children }: { children: ReactNode }) => ( @@ -36,14 +33,11 @@ const ValueWrapper = ({ children }: { children: ReactNode }) => ( ); export const ProfileDropDownButton = ({ - isConnected, - isConnecting, - show, - hide, address, ensName, fragment, hasStatment, + handleSignOut, }: Props) => { const { disconnect } = useDisconnect(); @@ -222,7 +216,10 @@ export const ProfileDropDownButton = ({ disconnect()} + onClick={(e) => { + if (handleSignOut) handleSignOut(); + disconnect(); + }} alt="Disconnect Wallet" className={css` width: 32px; diff --git a/packages/frontend/src/hooks/useContractWrite.ts b/packages/frontend/src/hooks/useContractWrite.ts index 161a26ac0..461bfbb0c 100644 --- a/packages/frontend/src/hooks/useContractWrite.ts +++ b/packages/frontend/src/hooks/useContractWrite.ts @@ -62,7 +62,7 @@ export function useContractWrite< onSuccess() { setOnPrepareError(false); }, - overrides, + // overrides, }); const { data, write } = useContractWriteUNSAFE({ @@ -72,10 +72,7 @@ export function useContractWrite< return; } - return { - ...config.request, - gasLimit: config.request.gasLimit.mul(2), - }; + return config.request; })(), onSuccess() { onSuccess(); diff --git a/packages/frontend/src/pages/EditDelegatePage/DelegateStatementForm.tsx b/packages/frontend/src/pages/EditDelegatePage/DelegateStatementForm.tsx index ed310f89e..72eba1e97 100644 --- a/packages/frontend/src/pages/EditDelegatePage/DelegateStatementForm.tsx +++ b/packages/frontend/src/pages/EditDelegatePage/DelegateStatementForm.tsx @@ -13,22 +13,18 @@ import { SelectedProposal } from "./PastProposalsFormSection"; import { OtherInfoFormSection } from "./OtherInfoFormSection"; import { buttonStyles } from "./EditDelegatePage"; import { UseForm, useForm } from "./useForm"; -import { useAccount, useProvider, useSigner } from "wagmi"; +import { useAccount, useWalletClient, useSignMessage } from "wagmi"; +import { recoverMessageAddress } from "viem"; import { useFragment, useMutation, VariablesOf } from "react-relay"; import { useMutation as useReactQueryMutation } from "@tanstack/react-query"; import graphql from "babel-plugin-relay/macro"; -import { - DelegateStatementFormMutation, - ValueWithSignature, -} from "./__generated__/DelegateStatementFormMutation.graphql"; +import { DelegateStatementFormMutation } from "./__generated__/DelegateStatementFormMutation.graphql"; import { HStack, VStack } from "../../components/VStack"; import { DelegateStatementFormFragment$key } from "./__generated__/DelegateStatementFormFragment.graphql"; import { useMemo, useState } from "react"; import { isEqual } from "lodash"; import { useNavigate } from "../../components/HammockRouter/HammockRouter"; -import { ethers, Signer } from "ethers"; import * as Sentry from "@sentry/react"; -import { GnosisSafe, GnosisSafe__factory } from "../../contracts/generated"; type DelegateStatementFormProps = { queryFragment: DelegateStatementFormFragment$key; @@ -67,7 +63,6 @@ export function DelegateStatementForm({ className, }: DelegateStatementFormProps) { const { address } = useAccount(); - const provider = useProvider(); const data = useFragment( graphql` @@ -179,11 +174,13 @@ export function DelegateStatementForm({ const navigate = useNavigate(); const [lastErrorMessage, setLastErrorMessage] = useState(); - const { data: signer } = useSigner(); + const messageSigner = useSignMessage(); + + const walletClient = useWalletClient(); const submitMutation = useReactQueryMutation< unknown, unknown, - { values: FormValues; address?: string } + { values: FormValues; address?: `0x${string}` } >({ mutationKey: ["submit"], onError: (error, variables) => { @@ -200,7 +197,7 @@ export function DelegateStatementForm({ setLastErrorMessage(`An error occurred, id: ${exceptionId}`); }, async mutationFn({ values: formState, address }) { - if (!signer) { + if (!walletClient) { throw new Error("signer not available"); } @@ -227,50 +224,73 @@ export function DelegateStatementForm({ const serializedBody = JSON.stringify(signingBody, undefined, "\t"); - const variables: VariablesOf = { - input: { - statement: await makeSignedValue( - signer, - provider, - address, - serializedBody - ), - email: formState.email - ? await makeSignedValue(signer, provider, address, formState.email) - : null, - }, - }; + const recoveredAddress = await recoverMessageAddress({ + message: serializedBody, + signature: await messageSigner.signMessageAsync({ + message: serializedBody, + }), + }); - await new Promise((resolve, reject) => - createNewDelegateStatement({ - variables, - updater(store) { - store.invalidateStore(); + console.log("recoveredAddress", recoveredAddress); + console.log(messageSigner.data); + + if (messageSigner.data) { + const variables: VariablesOf = { + input: { + statement: { + signature: messageSigner.data, + signatureType: recoveredAddress === address ? "EOA" : "CONTRACT", + signerAddress: recoveredAddress, + value: serializedBody, + }, + email: formState.email + ? { + signature: await messageSigner.signMessageAsync({ + message: formState.email, + }), + signatureType: + recoveredAddress === address ? "EOA" : "CONTRACT", + signerAddress: address, + value: formState.email, + } + : null, }, - onCompleted() { - resolve(); - }, - onError(error) { - reject(error); - }, - }) - ); + }; + + await new Promise((resolve, reject) => + createNewDelegateStatement({ + variables, + updater(store) { + store.invalidateStore(); + }, + onCompleted() { + resolve(); + }, + onError(error) { + reject(error); + }, + }) + ); - if (!data.delegate) { - return; - } + if (!data.delegate) { + return; + } - navigate({ - path: `/delegate/${ - data.delegate.address.resolvedName.name ?? - data.delegate.address.resolvedName.address - }`, - }); + navigate({ + path: `/delegate/${ + data.delegate.address.resolvedName.name ?? + data.delegate.address.resolvedName.address + }`, + }); + } }, }); const canSubmit = - !!signer && !isMutationInFlight && !submitMutation.isLoading && isDirty; + !!walletClient && + !isMutationInFlight && + !submitMutation.isLoading && + isDirty; return ( { - const response = await fetch("/fetch_signature", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - safeMessageHash, - }), - }); - - return (await response.json()) as TransactionServiceSafeMessage | undefined; -} - -async function getSafeSignature(safe: GnosisSafe, value: string) { - const hashed = ethers.utils.hashMessage(value); - const messageHash = await safe.getMessageHash(hashed); - console.log("messageHash", messageHash); - try { - const safeMessage = await tryFetchMessage(messageHash); - console.log("safeMessage", safeMessage); - return safeMessage?.preparedSignature; - } catch (e) { - return false; - } -} - -async function makeSignedValue( - signer: Signer, - provider: ethers.providers.Provider, - signerAddress: string, - value: string -): Promise { - const signaturePayload = hashEnvelopeValue(value); - - const addressCode = await provider.getCode(signerAddress); - if (addressCode === "0x") { - // eoa account - return { - signerAddress, - value, - signature: await signer.signMessage(signaturePayload), - signatureType: "EOA", - }; - } - - // some kind of multi-sig wallet, likely a gnosis safe. - const gnosisSafe = GnosisSafe__factory.connect(signerAddress, provider); - const safeSignature = await getSafeSignature(gnosisSafe, signaturePayload); - console.log("safeSignature", safeSignature); - if (safeSignature) { - // already signed, post to backend - return { - signerAddress, - value, - signature: safeSignature, - signatureType: "CONTRACT", - }; - } - - await signer.signMessage(signaturePayload); - throw new UserVisibleError( - "click submit again once all signatures have been provided, leaving this page will cause form values to be lost" - ); -} - class UserVisibleError extends Error { public readonly message: string; constructor(message: string) { diff --git a/patches/@typechain+ethers-v5+10.1.0.patch b/patches/@typechain+ethers-v5+10.1.0.patch index 12931c6a5..076eeaf62 100644 --- a/patches/@typechain+ethers-v5+10.1.0.patch +++ b/patches/@typechain+ethers-v5+10.1.0.patch @@ -2,7 +2,7 @@ diff --git a/node_modules/@typechain/ethers-v5/dist/codegen/events.js b/node_mod index ef89623..e30afef 100644 --- a/node_modules/@typechain/ethers-v5/dist/codegen/events.js +++ b/node_modules/@typechain/ethers-v5/dist/codegen/events.js -@@ -44,12 +44,51 @@ function generateEventTypeExport(event, includeArgTypes) { +@@ -44,12 +44,53 @@ function generateEventTypeExport(event, includeArgTypes) { `; } exports.generateEventTypeExport = generateEventTypeExport; @@ -35,9 +35,11 @@ index ef89623..e30afef 100644 + case 'uinteger': + return `uint${evmType.bits}`; + -+ case 'bytes': + case 'dynamic-bytes': -+ return 'bytes'; ++ return `bytes`; ++ ++ case 'bytes': ++ return `bytes${evmType.size}`; + + case 'array': + return `${generateEventType(evmType.itemType)}[]`; diff --git a/yarn.lock b/yarn.lock index 518983eda..4679166f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.0.1.tgz#b38b444ad3aa5fedbb15f2f746dcd934226a12dd" integrity sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g== +"@adraffy/ens-normalize@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.0.tgz#223572538f6bea336750039bb43a4016dcc8182d" + integrity sha512-iowxq3U30sghZotgl4s/oJRci6WPBfNO5YYgk2cIOMCHr3LeGPcsZjCEr+33Q4N+oV3OABDAtA+pyvWjbvBifQ== + "@ampproject/remapping@^2.1.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -4666,35 +4671,10 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@json-rpc-tools/provider@^1.5.5": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@json-rpc-tools/provider/-/provider-1.7.6.tgz#8a17c34c493fa892632e278fd9331104e8491ec6" - integrity sha512-z7D3xvJ33UfCGv77n40lbzOYjZKVM3k2+5cV7xS8G6SCvKTzMkhkUYuD/qzQUNT4cG/lv0e9mRToweEEVLVVmA== - dependencies: - "@json-rpc-tools/utils" "^1.7.6" - axios "^0.21.0" - safe-json-utils "^1.1.1" - ws "^7.4.0" - -"@json-rpc-tools/types@^1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@json-rpc-tools/types/-/types-1.7.6.tgz#5abd5fde01364a130c46093b501715bcce5bdc0e" - integrity sha512-nDSqmyRNEqEK9TZHtM15uNnDljczhCUdBmRhpNZ95bIPKEDQ+nTDmGMFd2lLin3upc5h2VVVd9tkTDdbXUhDIQ== - dependencies: - keyvaluestorage-interface "^1.0.0" - -"@json-rpc-tools/utils@^1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@json-rpc-tools/utils/-/utils-1.7.6.tgz#67f04987dbaa2e7adb6adff1575367b75a9a9ba1" - integrity sha512-HjA8x/U/Q78HRRe19yh8HVKoZ+Iaoo3YZjakJYxR+rw52NHo6jM+VE9b8+7ygkCFXl/EHID5wh/MkXaE/jGyYw== - dependencies: - "@json-rpc-tools/types" "^1.7.6" - "@pedrouid/environment" "^1.0.1" - -"@ledgerhq/connect-kit-loader@^1.0.1": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@ledgerhq/connect-kit-loader/-/connect-kit-loader-1.0.2.tgz#8554e16943f86cc2a5f6348a14dfe6e5bd0c572a" - integrity sha512-TQ21IjcZOw/scqypaVFY3jHVqI7X7Hta3qN/us6FvTol3AY06UmrhhXGww0E9xHmAbdX241ddwXEiMBSQZFr9g== +"@ledgerhq/connect-kit-loader@^1.1.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ledgerhq/connect-kit-loader/-/connect-kit-loader-1.1.2.tgz#d550e3c1f046e4c796f32a75324b03606b7e226a" + integrity sha512-mscwGroSJQrCTjtNGBu+18FQbZYA4+q6Tyx6K7CXHl6AwgZKbWfZYdgP2F+fyZcRUdGRsMX8QtvU61VcGGtO1A== "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" @@ -5262,16 +5242,45 @@ resolved "https://registry.yarnpkg.com/@n1ru4l/graphql-live-query/-/graphql-live-query-0.10.0.tgz#2306151630b0e74b017cc1067ebbea351acf56f8" integrity sha512-qZ7OHH/NB0NcG/Xa7irzgjE63UH0CkofZT0Bw4Ko6iRFagPRHBM8RgFXwTt/6JbFGIEUS4STRtaFoc/Eq/ZtzQ== +"@noble/curves@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" + +"@noble/curves@~1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932" + integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw== + dependencies: + "@noble/hashes" "1.3.0" + "@noble/ed25519@^1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.1.tgz#6899660f6fbb97798a6fbd227227c4589a454724" integrity sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw== +"@noble/hashes@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" + integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== + +"@noble/hashes@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + "@noble/hashes@^1.1.2": version "1.1.5" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.5.tgz#1a0377f3b9020efe2fae03290bd2a12140c95c11" integrity sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ== +"@noble/hashes@~1.3.0": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + "@noble/secp256k1@^1.6.3": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -5348,11 +5357,6 @@ tslib "^2.4.0" webcrypto-core "^1.7.4" -"@pedrouid/environment@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@pedrouid/environment/-/environment-1.0.1.tgz#858f0f8a057340e0b250398b75ead77d6f4342ec" - integrity sha512-HaW78NszGzRZd9SeoI3JD11JqY+lubnaOx7Pewj5pfjqWXOEATpeKIFb9Z4t2WBUK2iryiXX3lzWwmYWgUL0Ug== - "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.7" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz#58f8217ba70069cc6a73f5d7e05e85b458c150e2" @@ -5373,6 +5377,23 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== +"@prisma/client@^5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.3.1.tgz#fc7fc2d91e814cc4fe18a4bc5e78bf851c26985e" + integrity sha512-ArOKjHwdFZIe1cGU56oIfy7wRuTn0FfZjGuU/AjgEBOQh+4rDkB6nF+AGHP8KaVpkBIiHGPQh3IpwQ3xDMdO0Q== + dependencies: + "@prisma/engines-version" "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59" + +"@prisma/engines-version@5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59": + version "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz#7eb6f5c6b7628b8b39df55c903f411528a6f761c" + integrity sha512-y5qbUi3ql2Xg7XraqcXEdMHh0MocBfnBzDn5GbV1xk23S3Mq8MGs+VjacTNiBh3dtEdUERCrUUG7Z3QaJ+h79w== + +"@prisma/engines@5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.3.1.tgz#53cc72a5ed176dc27d22305fe5569c64cc78b381" + integrity sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -5502,29 +5523,29 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz#0c8b74c50f29ee44f423f7416829c0bf8bb5eb27" integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA== -"@safe-global/safe-apps-provider@^0.15.2": - version "0.15.2" - resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-provider/-/safe-apps-provider-0.15.2.tgz#fa5c30140134e72bb969da76b80a16c545323e3a" - integrity sha512-BaoGAuY7h6jLBL7P+M6b7hd+1QfTv8uMyNF3udhiNUwA0XwfzH2ePQB13IEV3Mn7wdcIMEEUDS5kHbtAsj60qQ== +"@safe-global/safe-apps-provider@^0.17.1": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-provider/-/safe-apps-provider-0.17.1.tgz#72df2a66be5343940ed505efe594ed3b0f2f7015" + integrity sha512-lYfRqrbbK1aKU1/UGkYWc/X7PgySYcumXKc5FB2uuwAs2Ghj8uETuW5BrwPqyjBknRxutFbTv+gth/JzjxAhdQ== dependencies: - "@safe-global/safe-apps-sdk" "7.9.0" + "@safe-global/safe-apps-sdk" "8.0.0" events "^3.3.0" -"@safe-global/safe-apps-sdk@7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-7.9.0.tgz#0c79a7760470bfdaf4cce9aa5bceef56898c7037" - integrity sha512-S2EI+JL8ocSgE3uGNaDZCzKmwfhtxXZFDUP76vN0FeaY35itFMyi8F0Vhxu0XnZm3yLzJE3tp5px6GhuQFLU6w== +"@safe-global/safe-apps-sdk@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-8.0.0.tgz#9bdfe0e0d85e1b2d279bb840f40c4b930aaf8bc1" + integrity sha512-gYw0ki/EAuV1oSyMxpqandHjnthZjYYy+YWpTAzf8BqfXM3ItcZLpjxfg+3+mXW8HIO+3jw6T9iiqEXsqHaMMw== dependencies: "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" - ethers "^5.7.2" + viem "^1.0.0" -"@safe-global/safe-apps-sdk@^7.9.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-7.11.0.tgz#2cbc164fb70141cdf4d3331ff222cd98a2529316" - integrity sha512-RDamzPM1Lhhiiz0O+Dn6FkFqIh47jmZX+HCV/BBnBBOSKfBJE//IGD3+02zMgojXHTikQAburdPes9qmH1SA1A== +"@safe-global/safe-apps-sdk@^8.0.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-8.1.0.tgz#d1d0c69cd2bf4eef8a79c5d677d16971926aa64a" + integrity sha512-XJbEPuaVc7b9n23MqlF6c+ToYIS3f7P2Sel8f3cSBQ9WORE4xrSuvhMpK9fDSFqJ7by/brc+rmJR/5HViRr0/w== dependencies: "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" - ethers "^5.7.2" + viem "^1.0.0" "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.7.3" @@ -5533,6 +5554,28 @@ dependencies: cross-fetch "^3.1.5" +"@scure/base@~1.1.0": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f" + integrity sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q== + +"@scure/bip32@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.0.tgz#6c8d980ef3f290987736acd0ee2e0f0d50068d87" + integrity sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q== + dependencies: + "@noble/curves" "~1.0.0" + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + +"@scure/bip39@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.0.tgz#a207e2ef96de354de7d0002292ba1503538fc77b" + integrity sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@sentry/browser@7.36.0": version "7.36.0" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.36.0.tgz#097f00e1e48b5fb2705e94f32d915aec44104b57" @@ -5688,6 +5731,16 @@ rpc-websockets "^7.5.0" superstruct "^0.14.2" +"@spruceid/siwe-parser@*": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@spruceid/siwe-parser/-/siwe-parser-2.0.2.tgz#964dbe9e5611fe95d39e21aa96e67407f610374f" + integrity sha512-9WuA0ios2537cWYu39MMeH0O2KdrMKgKlOBUTWRTXQjCYu5B+mHCA0JkCbFaJ/0EjxoVIcYCXIW/DoPEpw+PqA== + dependencies: + "@noble/hashes" "^1.1.2" + apg-js "^4.1.1" + uri-js "^4.4.1" + valid-url "^1.0.9" + "@stablelib/aead@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/aead/-/aead-1.0.1.tgz#c4b1106df9c23d1b867eb9b276d8f42d5fc4c0c3" @@ -6781,55 +6834,56 @@ "@typescript-eslint/types" "5.35.1" eslint-visitor-keys "^3.3.0" -"@wagmi/chains@0.2.22": - version "0.2.22" - resolved "https://registry.yarnpkg.com/@wagmi/chains/-/chains-0.2.22.tgz#25e511e134a00742e4fbf5108613dadf876c5bd9" - integrity sha512-TdiOzJT6TO1JrztRNjTA5Quz+UmQlbvWFG8N41u9tta0boHA1JCAzGGvU6KuIcOmJfRJkKOUIt67wlbopCpVHg== +"@wagmi/chains@1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@wagmi/chains/-/chains-1.8.0.tgz#70e5fd0d50c8f9b8e63585eaf8544481e71707d3" + integrity sha512-UXo0GF0Cl0+neKC2KAmVAahv8L/5rACbFRRqkDvHMefzY6Fh7yzJd8F4GaGNNG3w4hj8eUB/E3+dEpaTYDN62w== -"@wagmi/connectors@0.3.21": - version "0.3.21" - resolved "https://registry.yarnpkg.com/@wagmi/connectors/-/connectors-0.3.21.tgz#0bec726c14217ad391f6e49af1203ccf0249786e" - integrity sha512-yXtczgBQzVhUeo6D2L9yu8HmWQv08v6Ji5Cb4ZNL1mM2VVnvXxv7l40fSschcTw6H5jBZytgeGgL/aTYhn3HYQ== +"@wagmi/connectors@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@wagmi/connectors/-/connectors-3.0.0.tgz#9d596341e9dee6200d27617813ec801e04451e6f" + integrity sha512-phHrd4dKctynlh7eRefsaWuh2NF9T24tkLmUHctJeZg/tPzvw+sRFy0XlP7kLw/kwKArn2oSvck7iFemvMeLoQ== dependencies: "@coinbase/wallet-sdk" "^3.6.6" - "@ledgerhq/connect-kit-loader" "^1.0.1" - "@safe-global/safe-apps-provider" "^0.15.2" - "@safe-global/safe-apps-sdk" "^7.9.0" - "@walletconnect/ethereum-provider" "2.8.1" + "@ledgerhq/connect-kit-loader" "^1.1.0" + "@safe-global/safe-apps-provider" "^0.17.1" + "@safe-global/safe-apps-sdk" "^8.0.0" + "@walletconnect/ethereum-provider" "2.10.0" "@walletconnect/legacy-provider" "^2.0.0" - "@walletconnect/modal" "^2.4.6" - abitype "^0.3.0" + "@walletconnect/modal" "2.6.1" + "@walletconnect/utils" "2.10.0" + abitype "0.8.7" eventemitter3 "^4.0.7" -"@wagmi/core@0.10.14": - version "0.10.14" - resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-0.10.14.tgz#5b07df06c5d5a29bcee76b1ad71695e88559955b" - integrity sha512-+iQj5YNdVQ/kLVpVMLmF71Y2vnW3ox4b4Na4S9fvQazGudhqfqfpQ+YPYmH6SIIuZ1VhRnBmjJIY88IZt/OkwA== +"@wagmi/core@1.3.10": + version "1.3.10" + resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-1.3.10.tgz#9273d47f1c4911008416568c80ee3852e44734d1" + integrity sha512-sJ+nTUppsEkJqZ6gwM4PK6yNlD/CsT4ejRXbad2RoM9fuNDHzZIUA9g+7b0BpgTpyXDYbjk0vBUXHUFMSlmyIA== dependencies: - "@wagmi/chains" "0.2.22" - "@wagmi/connectors" "0.3.21" - abitype "^0.3.0" + "@wagmi/chains" "1.8.0" + "@wagmi/connectors" "3.0.0" + abitype "0.8.7" eventemitter3 "^4.0.7" zustand "^4.3.1" -"@walletconnect/core@2.8.1": - version "2.8.1" - resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.8.1.tgz#f74404af372a11e05c214cbc14b5af0e9c0cf916" - integrity sha512-mN9Zkdl/NeThntK8cydDoQOW6jUEpOeFgYR1RCKPLH51VQwlbdSgvvQIeanSQXEY4U7AM3x8cs1sxqMomIfRQg== +"@walletconnect/core@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.10.0.tgz#b659de4dfb374becd938964abd4f2150d410e617" + integrity sha512-Z8pdorfIMueuiBXLdnf7yloiO9JIiobuxN3j0OTal+MYc4q5/2O7d+jdD1DAXbLi1taJx3x60UXT/FPVkjIqIQ== dependencies: "@walletconnect/heartbeat" "1.2.1" "@walletconnect/jsonrpc-provider" "1.0.13" "@walletconnect/jsonrpc-types" "1.0.3" "@walletconnect/jsonrpc-utils" "1.0.8" - "@walletconnect/jsonrpc-ws-connection" "^1.0.11" + "@walletconnect/jsonrpc-ws-connection" "1.0.13" "@walletconnect/keyvaluestorage" "^1.0.2" "@walletconnect/logger" "^2.0.1" "@walletconnect/relay-api" "^1.0.9" "@walletconnect/relay-auth" "^1.0.4" "@walletconnect/safe-json" "^1.0.2" "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.8.1" - "@walletconnect/utils" "2.8.1" + "@walletconnect/types" "2.10.0" + "@walletconnect/utils" "2.10.0" events "^3.3.0" lodash.isequal "4.5.0" uint8arrays "^3.1.0" @@ -6862,19 +6916,19 @@ dependencies: tslib "1.14.1" -"@walletconnect/ethereum-provider@2.8.1": - version "2.8.1" - resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.8.1.tgz#1743072f42b5c940648b0303a382e8907a362a00" - integrity sha512-YlF8CCiFTSEZRyANIBsop/U+t+d1Z1/UXXoE9+iwjSGKJsaym6PgBLPb2d8XdmS/qR6Tcx7lVodTp4cVtezKnA== +"@walletconnect/ethereum-provider@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.10.0.tgz#eebde38674222a48be35bb4aa3f6a74247ba059b" + integrity sha512-NyTm7RcrtAiSaYQPh6G4sOtr1kg/pL5Z3EDE6rBTV3Se5pMsYvtuwMiSol7MidsQpf4ux9HFhthTO3imcoWImw== dependencies: "@walletconnect/jsonrpc-http-connection" "^1.0.7" "@walletconnect/jsonrpc-provider" "^1.0.13" "@walletconnect/jsonrpc-types" "^1.0.3" "@walletconnect/jsonrpc-utils" "^1.0.8" - "@walletconnect/sign-client" "2.8.1" - "@walletconnect/types" "2.8.1" - "@walletconnect/universal-provider" "2.8.1" - "@walletconnect/utils" "2.8.1" + "@walletconnect/sign-client" "2.10.0" + "@walletconnect/types" "2.10.0" + "@walletconnect/universal-provider" "2.10.0" + "@walletconnect/utils" "2.10.0" events "^3.3.0" "@walletconnect/events@^1.0.1": @@ -6966,10 +7020,10 @@ "@walletconnect/jsonrpc-types" "^1.0.2" tslib "1.14.1" -"@walletconnect/jsonrpc-ws-connection@^1.0.11": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.11.tgz#1ce59d86f273d576ca73385961303ebd44dd923f" - integrity sha512-TiFJ6saasKXD+PwGkm5ZGSw0837nc6EeFmurSPgIT/NofnOV4Tv7CVJqGQN0rQYoJUSYu21cwHNYaFkzNpUN+w== +"@walletconnect/jsonrpc-ws-connection@1.0.13": + version "1.0.13" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.13.tgz#23b0cdd899801bfbb44a6556936ec2b93ef2adf4" + integrity sha512-mfOM7uFH4lGtQxG+XklYuFBj6dwVvseTt5/ahOkkmpcAEgz2umuzu7fTR+h5EmjQBdrmYyEBOWADbeaFNxdySg== dependencies: "@walletconnect/jsonrpc-utils" "^1.0.6" "@walletconnect/safe-json" "^1.0.2" @@ -7053,13 +7107,30 @@ pino "7.11.0" tslib "1.14.1" -"@walletconnect/modal@^2.4.6": - version "2.4.7" - resolved "https://registry.yarnpkg.com/@walletconnect/modal/-/modal-2.4.7.tgz#fd84d6f1ac767865d63153e32150f790739a189a" - integrity sha512-kFpvDTT44CgNGcwQVC0jHrYed4xorghKX1DOGo8ZfBSJ5TJx3p6d6SzLxkH1cZupWbljWkYS6SqvZcUBs8vWpg== +"@walletconnect/modal-core@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@walletconnect/modal-core/-/modal-core-2.6.1.tgz#bc76055d0b644a2d4b98024324825c108a700905" + integrity sha512-f2hYlJ5pwzGvjyaZ6BoGR5uiMgXzWXt6w6ktt1N8lmY6PiYp8whZgqx2hTxVWwVlsGnaIfh6UHp1hGnANx0eTQ== + dependencies: + valtio "1.11.0" + +"@walletconnect/modal-ui@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@walletconnect/modal-ui/-/modal-ui-2.6.1.tgz#200c54c8dfe3c71321abb2724e18bb357dfd6371" + integrity sha512-RFUOwDAMijSK8B7W3+KoLKaa1l+KEUG0LCrtHqaB0H0cLnhEGdLR+kdTdygw+W8+yYZbkM5tXBm7MlFbcuyitA== + dependencies: + "@walletconnect/modal-core" "2.6.1" + lit "2.7.6" + motion "10.16.2" + qrcode "1.5.3" + +"@walletconnect/modal@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@walletconnect/modal/-/modal-2.6.1.tgz#066fdbfcff83b58c8a9da66ab4af0eb93e3626de" + integrity sha512-G84tSzdPKAFk1zimgV7JzIUFT5olZUVtI3GcOk77OeLYjlMfnDT23RVRHm5EyCrjkptnvpD0wQScXePOFd2Xcw== dependencies: - "@web3modal/core" "2.4.7" - "@web3modal/ui" "2.4.7" + "@walletconnect/modal-core" "2.6.1" + "@walletconnect/modal-ui" "2.6.1" "@walletconnect/randombytes@^1.0.3": version "1.0.3" @@ -7105,19 +7176,19 @@ dependencies: tslib "1.14.1" -"@walletconnect/sign-client@2.8.1": - version "2.8.1" - resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.8.1.tgz#8c6de724eff6a306c692dd66e66944089be5e30a" - integrity sha512-6DbpjP9BED2YZOZdpVgYo0HwPBV7k99imnsdMFrTn16EFAxhuYP0/qPwum9d072oNMGWJSA6d4rzc8FHNtHsCA== +"@walletconnect/sign-client@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.10.0.tgz#0fee8f12821e37783099f0c7bd64e6efdfbd9d86" + integrity sha512-hbDljDS53kR/It3oXD91UkcOsT6diNnW5+Zzksm0YEfwww5dop/YfNlcdnc8+jKUhWOL/YDPNQCjzsCSNlVzbw== dependencies: - "@walletconnect/core" "2.8.1" + "@walletconnect/core" "2.10.0" "@walletconnect/events" "^1.0.1" "@walletconnect/heartbeat" "1.2.1" "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/logger" "^2.0.1" "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.8.1" - "@walletconnect/utils" "2.8.1" + "@walletconnect/types" "2.10.0" + "@walletconnect/utils" "2.10.0" events "^3.3.0" "@walletconnect/time@^1.0.2": @@ -7127,10 +7198,10 @@ dependencies: tslib "1.14.1" -"@walletconnect/types@2.8.1": - version "2.8.1" - resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.8.1.tgz#640eb6ad23866886fbe09a9b29832bf3f8647a09" - integrity sha512-MLISp85b+27vVkm3Wkud+eYCwySXCdOrmn0yQCSN6DnRrrunrD05ksz4CXGP7h2oXUvvXPDt/6lXBf1B4AfqrA== +"@walletconnect/types@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.10.0.tgz#5d63235b49e03d609521402a4b49627dbc4ed514" + integrity sha512-kSTA/WZnbKdEbvbXSW16Ty6dOSzOZCHnGg6JH7q1MuraalD2HuNg00lVVu7QAZ/Rj1Gn9DAkrgP5Wd5a8Xq//Q== dependencies: "@walletconnect/events" "^1.0.1" "@walletconnect/heartbeat" "1.2.1" @@ -7139,26 +7210,25 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" -"@walletconnect/universal-provider@2.8.1": - version "2.8.1" - resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.8.1.tgz#3fc51c56d1c94a02eb952f9bf948293cc7aace7e" - integrity sha512-6shgE4PM/S+GEh9oTWMloHZlt2BLsCitRn9tBh2Vf+jZiGlug3WNm+tBc/Fo6ILyHuzeYPbkzCM67AxcutOHGQ== +"@walletconnect/universal-provider@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.10.0.tgz#565d6478dcb5cc66955e5f03d6a00f51c9bcac14" + integrity sha512-jtVWf+AeTCqBcB3lCmWkv3bvSmdRCkQdo67GNoT5y6/pvVHMxfjgrJNBOUsWQMxpREpWDpZ993X0JRjsYVsMcA== dependencies: "@walletconnect/jsonrpc-http-connection" "^1.0.7" "@walletconnect/jsonrpc-provider" "1.0.13" "@walletconnect/jsonrpc-types" "^1.0.2" "@walletconnect/jsonrpc-utils" "^1.0.7" "@walletconnect/logger" "^2.0.1" - "@walletconnect/sign-client" "2.8.1" - "@walletconnect/types" "2.8.1" - "@walletconnect/utils" "2.8.1" - eip1193-provider "1.0.1" + "@walletconnect/sign-client" "2.10.0" + "@walletconnect/types" "2.10.0" + "@walletconnect/utils" "2.10.0" events "^3.3.0" -"@walletconnect/utils@2.8.1": - version "2.8.1" - resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.8.1.tgz#1356f4bba7f8b6664fc5b61ce3497596c8d9d603" - integrity sha512-d6p9OX3v70m6ijp+j4qvqiQZQU1vbEHN48G8HqXasyro3Z+N8vtcB5/gV4pTYsbWgLSDtPHj49mzbWQ0LdIdTw== +"@walletconnect/utils@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.10.0.tgz#6918d12180d797b8bd4a19fb2ff128e394e181d6" + integrity sha512-9GRyEz/7CJW+G04RvrjPET5k7hOEsB9b3fF9cWDk/iDCxSWpbkU/hv/urRB36C+gvQMAZgIZYX3dHfzJWkY/2g== dependencies: "@stablelib/chacha20poly1305" "1.0.1" "@stablelib/hkdf" "1.0.1" @@ -7168,7 +7238,7 @@ "@walletconnect/relay-api" "^1.0.9" "@walletconnect/safe-json" "^1.0.2" "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.8.1" + "@walletconnect/types" "2.10.0" "@walletconnect/window-getters" "^1.0.1" "@walletconnect/window-metadata" "^1.0.1" detect-browser "5.3.0" @@ -7190,24 +7260,6 @@ "@walletconnect/window-getters" "^1.0.1" tslib "1.14.1" -"@web3modal/core@2.4.7": - version "2.4.7" - resolved "https://registry.yarnpkg.com/@web3modal/core/-/core-2.4.7.tgz#e128be449bc5f6f23f6fb32f12021c096b5e7a07" - integrity sha512-FZMmI4JnEZovRDdN+PZBMe2ot8ly+UftVkZ6lmtfgiRZ2Gy3k/4IYE8/KwOSmN63Lf2Oj2077buLR17i0xoKZA== - dependencies: - buffer "6.0.3" - valtio "1.10.5" - -"@web3modal/ui@2.4.7": - version "2.4.7" - resolved "https://registry.yarnpkg.com/@web3modal/ui/-/ui-2.4.7.tgz#94d70e60386eb6fae422c56386019e761f80a50a" - integrity sha512-5tU9u5CVYueZ9y+1x1A1Q0bFUfk3gOIKy3MT6Vx+aI0RDxVu7OYQDw6wbNPlgz/wd9JPYXG6uSv8WTBpdyit8Q== - dependencies: - "@web3modal/core" "2.4.7" - lit "2.7.5" - motion "10.16.2" - qrcode "1.5.3" - "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -7372,10 +7424,15 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== -abitype@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.3.0.tgz#75150e337d88cc0b2423ed0d3fc36935f139d04c" - integrity sha512-0YokyAV4hKMcy97Pl+6QgZBlBdZJN2llslOs7kiFY+cu7kMlVXDBpxMExfv0krzBCQt2t7hNovpQ3y/zvEm18A== +abitype@0.8.7: + version "0.8.7" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.8.7.tgz#e4b3f051febd08111f486c0cc6a98fa72d033622" + integrity sha512-wQ7hV8Yg/yKmGyFpqrNZufCxbszDe5es4AZGYPBitocfSqXtjrTG9JMWFcc4N30ukl2ve48aBTwt7NJxVQdU3w== + +abitype@0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.3.tgz#294d25288ee683d72baf4e1fed757034e3c8c277" + integrity sha512-dz4qCQLurx97FQhnb/EIYTk/ldQ+oafEDUqC0VVIeQS1Q48/YWt/9YNfMmp9SLFqN41ktxny3c8aYxHjmFIB/w== abort-controller@^3.0.0: version "3.0.0" @@ -7612,6 +7669,11 @@ anymatch@^3.0.3, anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +apg-js@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/apg-js/-/apg-js-4.2.0.tgz#c26e7271640e67d3d4738b93bae8ee53773b0635" + integrity sha512-4WI3AYN9DmJWK3+3r/DtVMI+RO45R0u/b7tJWb5EM2c8nIzojx8Oq5LpMalou3sQnmS9qzw7cKmHBrAjdlseWw== + apollo-reporting-protobuf@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.8.0.tgz#ae9d967934d3d8ed816fc85a0d8068ef45c371b9" @@ -7910,13 +7972,6 @@ axios-cache-interceptor@^0.9.3: fast-defer "^1.1.5" object-code "^1.2.0" -axios@^0.21.0: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== - dependencies: - follow-redirects "^1.14.0" - axios@^0.25.0: version "0.25.0" resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" @@ -8554,14 +8609,6 @@ buffer@6.0.1: base64-js "^1.3.1" ieee754 "^1.2.1" -buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - buffer@^5.0.5, buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -8570,6 +8617,14 @@ buffer@^5.0.5, buffer@^5.5.0, buffer@^5.6.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.3, buffer@~6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + bufferutil@^4.0.1: version "4.0.6" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.6.tgz#ebd6c67c7922a0e902f053e5d8be5ec850e48433" @@ -9124,10 +9179,10 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== -connectkit@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/connectkit/-/connectkit-1.3.0.tgz#feed43b43ebb0cbef1e83cd3130578ed6a15b977" - integrity sha512-6EIETiGJjRWEwdRGUDjy+SWOqiCuRStKkib2xCQ8o2sOhKqSlDFC6W4B4L91J9EvrxoulRdlD80yPTYexbkUyA== +connectkit@1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/connectkit/-/connectkit-1.5.2.tgz#fb7193d05f9917778c6631b93740618117d2d0ed" + integrity sha512-H3sRZjd8u2DG68+eQ7OUcTCA8fgTiZx7N6s06seZcC+PV47luevljzTX8nkkdLAcU+zhUJknL2gmko340+f8RA== dependencies: buffer "^6.0.3" detect-browser "^5.3.0" @@ -10077,13 +10132,6 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -eip1193-provider@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/eip1193-provider/-/eip1193-provider-1.0.1.tgz#420d29cf4f6c443e3f32e718fb16fafb250637c3" - integrity sha512-kSuqwQ26d7CzuS/t3yRXo2Su2cVH0QfvyKbr2H7Be7O5YDyIq4hQGCNTo5wRdP07bt+E2R/8nPCzey4ojBHf7g== - dependencies: - "@json-rpc-tools/provider" "^1.5.5" - ejs@^3.1.5, ejs@^3.1.6: version "3.1.8" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" @@ -11602,7 +11650,7 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.0.0, follow-redirects@^1.14.0: +follow-redirects@^1.0.0: version "1.15.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== @@ -12975,16 +13023,16 @@ isomorphic-fetch@^3.0.0: node-fetch "^2.6.1" whatwg-fetch "^3.4.1" +isomorphic-ws@5.0.0, isomorphic-ws@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== + isomorphic-ws@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== -isomorphic-ws@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" - integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== - isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -13942,6 +13990,11 @@ jmespath@0.16.0: resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== +jose@^4.14.6: + version "4.14.6" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.6.tgz#94dca1d04a0ad8c6bff0998cdb51220d473cc3af" + integrity sha512-EqJPEUlZD0/CSUMubKtMaYUOtWe91tZXTWMJZoKSbLk+KtdhNdcvppH8lA9XwVu2V4Ailvsj0GBZJ2ZwDjfesQ== + js-sdsl@^4.1.4: version "4.3.0" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" @@ -14348,10 +14401,10 @@ lit-html@^2.7.0: dependencies: "@types/trusted-types" "^2.0.2" -lit@2.7.5: - version "2.7.5" - resolved "https://registry.yarnpkg.com/lit/-/lit-2.7.5.tgz#60bc82990cfad169d42cd786999356dcf79b035f" - integrity sha512-i/cH7Ye6nBDUASMnfwcictBnsTN91+aBjXoTHF2xARghXScKxpD4F4WYI+VLXg9lqbMinDfvoI7VnZXjyHgdfQ== +lit@2.7.6: + version "2.7.6" + resolved "https://registry.yarnpkg.com/lit/-/lit-2.7.6.tgz#810007b876ed43e0c70124de91831921598b1665" + integrity sha512-1amFHA7t4VaaDe+vdQejSVBklwtH9svGoG6/dZi9JhxtJBBlqY5D1RV7iLUYY0trCqQc4NfhYYZilZiVHt7Hxg== dependencies: "@lit/reactive-element" "^1.6.0" lit-element "^3.3.0" @@ -16657,6 +16710,13 @@ pretty-format@^29.0.0, pretty-format@^29.0.3: ansi-styles "^5.0.0" react-is "^18.0.0" +prisma@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.3.1.tgz#a0932c1c1a5ed4ff449d064b193d9c7e94e8bf77" + integrity sha512-Wp2msQIlMPHe+5k5Od6xnsI/WNG7UJGgFUJgqv/ygc7kOECZapcSz/iU4NIEzISs3H1W9sFLjAPbg/gOqqtB7A== + dependencies: + "@prisma/engines" "5.3.1" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -18053,6 +18113,16 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +siwe@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/siwe/-/siwe-2.1.4.tgz#005a8be3e61224a86bd3457f60fdaab626f2d1d4" + integrity sha512-Dke1Qqa3mgiLm3vjqw/+SQ7dl8WV/Pfk3AlQBF94cBFydTYhztngqYrikzE3X5UTsJ6565dfVbQptszsuYZNYg== + dependencies: + "@spruceid/siwe-parser" "*" + "@stablelib/random" "^1.0.1" + uri-js "^4.4.1" + valid-url "^1.0.9" + slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" @@ -19305,7 +19375,7 @@ upper-case@^2.0.2: dependencies: tslib "^2.0.3" -uri-js@^4.2.2: +uri-js@^4.2.2, uri-js@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== @@ -19480,6 +19550,11 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" +valid-url@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" + integrity sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA== + validate-npm-package-name@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-4.0.0.tgz#fe8f1c50ac20afdb86f177da85b3600f0ac0d747" @@ -19487,10 +19562,10 @@ validate-npm-package-name@^4.0.0: dependencies: builtins "^5.0.0" -valtio@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/valtio/-/valtio-1.10.5.tgz#7852125e3b774b522827d96bd9c76d285c518678" - integrity sha512-jTp0k63VXf4r5hPoaC6a6LCG4POkVSh629WLi1+d5PlajLsbynTMd7qAgEiOSPxzoX5iNvbN7iZ/k/g29wrNiQ== +valtio@1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/valtio/-/valtio-1.11.0.tgz#c029dcd17a0f99d2fbec933721fe64cfd32a31ed" + integrity sha512-65Yd0yU5qs86b5lN1eu/nzcTgQ9/6YnD6iO+DDaDbQLn1Zv2w12Gwk43WkPlUBxk5wL/6cD5YMFf7kj6HZ1Kpg== dependencies: proxy-compare "2.5.1" use-sync-external-store "1.2.0" @@ -19537,6 +19612,21 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" +viem@^1.0.0: + version "1.9.5" + resolved "https://registry.yarnpkg.com/viem/-/viem-1.9.5.tgz#1bc2582f3f11f678fd96f9b89b8effc826361721" + integrity sha512-o6gxd4hOMof4n1Lm4j58Y2VbAa95O6KrQU4tqvkQjMd0MH/s9u0UBh7pv45gFSr53+NW8036G4hYcEV35nry8g== + dependencies: + "@adraffy/ens-normalize" "1.9.0" + "@noble/curves" "1.1.0" + "@noble/hashes" "1.3.0" + "@scure/bip32" "1.3.0" + "@scure/bip39" "1.2.0" + "@types/ws" "^8.5.4" + abitype "0.9.3" + isomorphic-ws "5.0.0" + ws "8.12.0" + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" @@ -19551,16 +19641,16 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" -wagmi@^0.12.13: - version "0.12.16" - resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-0.12.16.tgz#4e5e2a29fd9d646b5a06e0ff8838be4d3459ed03" - integrity sha512-ZnQYC7wkcxNrfIZ+8LIYIXaEmnih8n4aUBZ5J+YI6QwnAWfo80jI79Z5F0BBML6wG7PYP2iIzMbIlnDIfx67uQ== +wagmi@^1.3.11: + version "1.3.11" + resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-1.3.11.tgz#a7fd5508fd324ca5dbaa0b43d2a0234526f4a73d" + integrity sha512-op6iKSt5RFpjNLb99rYDePGVedg3OiNO6reWReQgFynQcva8PLsrfZe7XwyTaluj79GOfnVYnwhIDZvzRjvjRA== dependencies: "@tanstack/query-sync-storage-persister" "^4.27.1" "@tanstack/react-query" "^4.28.0" "@tanstack/react-query-persist-client" "^4.28.0" - "@wagmi/core" "0.10.14" - abitype "^0.3.0" + "@wagmi/core" "1.3.10" + abitype "0.8.7" use-sync-external-store "^1.2.0" walker@^1.0.7, walker@^1.0.8: @@ -20370,6 +20460,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.12.0, ws@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" + integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig== + ws@^3.0.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" @@ -20379,16 +20474,11 @@ ws@^3.0.0: safe-buffer "~5.1.0" ultron "~1.1.0" -ws@^7.4.0, ws@^7.4.5, ws@^7.4.6, ws@^7.5.1: +ws@^7.4.5, ws@^7.4.6, ws@^7.5.1: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== -ws@^8.12.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" - integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig== - ws@^8.2.2, ws@^8.3.0, ws@^8.4.2, ws@^8.5.0: version "8.8.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0"