diff --git a/packages/nextjs/app/admin/_components/EditGrantModal.tsx b/packages/nextjs/app/admin/_components/EditGrantModal.tsx index df1d384..3d0addb 100644 --- a/packages/nextjs/app/admin/_components/EditGrantModal.tsx +++ b/packages/nextjs/app/admin/_components/EditGrantModal.tsx @@ -1,9 +1,10 @@ import { ChangeEvent, forwardRef, useState } from "react"; import { useSWRConfig } from "swr"; import useSWRMutation from "swr/mutation"; -import { useAccount, useNetwork, useSignTypedData } from "wagmi"; +import { useAccount, useNetwork, usePublicClient, useSignTypedData } from "wagmi"; import { GrantDataWithPrivateNote } from "~~/services/database/schema"; import { EIP_712_DOMAIN, EIP_712_TYPES__EDIT_GRANT } from "~~/utils/eip712"; +import { isSafeContext } from "~~/utils/safe-signature"; import { getParsedError, notification } from "~~/utils/scaffold-eth"; import { patchMutationFetcher } from "~~/utils/swr"; @@ -19,6 +20,8 @@ type ReqBody = { signature?: `0x${string}`; signer?: string; private_note?: string; + isSafeSignature?: boolean; + chainId?: number; }; export const EditGrantModal = forwardRef(({ grant, closeModal }, ref) => { @@ -31,6 +34,7 @@ export const EditGrantModal = forwardRef const { address } = useAccount(); const { chain: connectedChain } = useNetwork(); + const publiClient = usePublicClient({ chainId: connectedChain?.id }); const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData(); const { trigger: editGrant, isMutating } = useSWRMutation(`/api/grants/${grant.id}`, patchMutationFetcher); @@ -69,11 +73,15 @@ export const EditGrantModal = forwardRef }, }); notificationId = notification.loading("Updating grant"); + + const isSafeSignature = await isSafeContext(publiClient, address); await editGrant({ signer: address, signature, ...formData, askAmount: parseFloat(formData.askAmount), + isSafeSignature, + chainId: connectedChain.id, }); await mutate("/api/grants/review"); notification.remove(notificationId); diff --git a/packages/nextjs/app/admin/hooks/useBatchReviewGrants.ts b/packages/nextjs/app/admin/hooks/useBatchReviewGrants.ts index 7b3b54d..4f2a951 100644 --- a/packages/nextjs/app/admin/hooks/useBatchReviewGrants.ts +++ b/packages/nextjs/app/admin/hooks/useBatchReviewGrants.ts @@ -1,8 +1,9 @@ import { useSWRConfig } from "swr"; import useSWRMutation from "swr/mutation"; -import { useAccount, useNetwork, useSignTypedData } from "wagmi"; +import { useAccount, useNetwork, usePublicClient, useSignTypedData } from "wagmi"; import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT_BATCH } from "~~/utils/eip712"; import { ProposalStatusType } from "~~/utils/grants"; +import { isSafeContext } from "~~/utils/safe-signature"; import { getParsedError, notification } from "~~/utils/scaffold-eth"; import { postMutationFetcher } from "~~/utils/swr"; @@ -15,6 +16,7 @@ type BatchReqBody = { txHash: string; txChainId: string; }[]; + isSafeSignature?: boolean; }; export const useBatchReviewGrants = () => { @@ -22,6 +24,7 @@ export const useBatchReviewGrants = () => { const { mutate } = useSWRConfig(); const { address: connectedAddress } = useAccount(); const { chain: connectedChain } = useNetwork(); + const publicClient = usePublicClient({ chainId: connectedChain?.id }); const { trigger: postBatchReviewGrant, isMutating: isPostingBatchReviewGrant } = useSWRMutation( `/api/grants/review`, postMutationFetcher, @@ -51,10 +54,13 @@ export const useBatchReviewGrants = () => { message: message, }); + const isSafeSignature = await isSafeContext(publicClient, connectedAddress); + await postBatchReviewGrant({ signature: signature, reviews: grantReviews, signer: connectedAddress, + isSafeSignature: isSafeSignature, }); await mutate("/api/grants/review"); notification.success(`Grants reviews successfully submitted!`); diff --git a/packages/nextjs/app/admin/hooks/useReviewGrant.ts b/packages/nextjs/app/admin/hooks/useReviewGrant.ts index 91fdacb..a39f991 100644 --- a/packages/nextjs/app/admin/hooks/useReviewGrant.ts +++ b/packages/nextjs/app/admin/hooks/useReviewGrant.ts @@ -1,9 +1,10 @@ import { useSWRConfig } from "swr"; import useSWRMutation from "swr/mutation"; -import { useAccount, useNetwork, useSignTypedData } from "wagmi"; +import { useAccount, useNetwork, usePublicClient, useSignTypedData } from "wagmi"; import { GrantData } from "~~/services/database/schema"; import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT, EIP_712_TYPES__REVIEW_GRANT_WITH_NOTE } from "~~/utils/eip712"; import { PROPOSAL_STATUS, ProposalStatusType } from "~~/utils/grants"; +import { isSafeContext } from "~~/utils/safe-signature"; import { notification } from "~~/utils/scaffold-eth"; import { postMutationFetcher } from "~~/utils/swr"; @@ -14,11 +15,13 @@ type ReqBody = { txHash: string; txChainId: string; note?: string; + isSafeSignature?: boolean; }; export const useReviewGrant = (grant: GrantData) => { const { address } = useAccount(); const { chain: connectedChain } = useNetwork(); + const publicClient = usePublicClient({ chainId: connectedChain?.id }); const { signTypedDataAsync, isLoading: isSigningMessage } = useSignTypedData(); const { trigger: postReviewGrant, isMutating: isPostingNewGrant } = useSWRMutation( `/api/grants/${grant.id}/review`, @@ -71,6 +74,7 @@ export const useReviewGrant = (grant: GrantData) => { let notificationId; try { notificationId = notification.loading("Submitting review"); + const isSafeSignature = await isSafeContext(publicClient, address); await postReviewGrant({ signer: address, signature, @@ -78,6 +82,7 @@ export const useReviewGrant = (grant: GrantData) => { txHash: txnHash, txChainId: connectedChain.id.toString(), note, + isSafeSignature, }); await mutate("/api/grants/review"); notification.remove(notificationId); diff --git a/packages/nextjs/app/admin/page.tsx b/packages/nextjs/app/admin/page.tsx index 9d79d94..84c34d9 100644 --- a/packages/nextjs/app/admin/page.tsx +++ b/packages/nextjs/app/admin/page.tsx @@ -8,11 +8,12 @@ import useSWR from "swr"; import useSWRMutation from "swr/mutation"; import { useLocalStorage } from "usehooks-ts"; import { parseEther } from "viem"; -import { useAccount, useSignTypedData } from "wagmi"; +import { useAccount, useNetwork, usePublicClient, useSignTypedData } from "wagmi"; import { useScaffoldContractWrite } from "~~/hooks/scaffold-eth"; import { GrantDataWithBuilder } from "~~/services/database/schema"; import { EIP_712_DOMAIN, EIP_712_TYPES__ADMIN_SIGN_IN } from "~~/utils/eip712"; import { PROPOSAL_STATUS } from "~~/utils/grants"; +import { isSafeContext } from "~~/utils/safe-signature"; import { getParsedError, notification } from "~~/utils/scaffold-eth"; import { postMutationFetcher } from "~~/utils/swr"; @@ -38,6 +39,8 @@ const fetcherWithHeader = async (url: string, headers: { address: string; apiKey const AdminPage = () => { const { address } = useAccount(); const [selectedApproveGrants, setSelectedApproveGrants] = useState([]); + const { chain: connectedChain } = useNetwork(); + const publicClient = usePublicClient({ chainId: connectedChain?.id }); const [selectedCompleteGrants, setSelectedCompleteGrants] = useState([]); const [modalBtnLabel, setModalBtnLabel] = useState<"Approve" | "Complete">("Approve"); const modalRef = useRef(null); @@ -48,6 +51,8 @@ const AdminPage = () => { postMutationFetcher<{ signer?: string; signature?: `0x${string}`; + isSafeSignature?: boolean; + chainId?: number; }>, ); @@ -130,7 +135,16 @@ const AdminPage = () => { message: { action: "Sign In", description: "I authorize myself as admin" }, }); - const resData = (await postAdminSignIn({ signer: address, signature })) as { data: { apiKey: string } }; + const isSafeSignature = await isSafeContext(publicClient, address); + + const resData = (await postAdminSignIn({ + signer: address, + signature, + isSafeSignature, + chainId: connectedChain?.id, + })) as { + data: { apiKey: string }; + }; setApiKey(resData.data.apiKey); } catch (error) { console.error("Error signing in", error); diff --git a/packages/nextjs/app/api/admin/signin/route.ts b/packages/nextjs/app/api/admin/signin/route.ts index 08a38b5..7db5151 100644 --- a/packages/nextjs/app/api/admin/signin/route.ts +++ b/packages/nextjs/app/api/admin/signin/route.ts @@ -2,14 +2,17 @@ import { NextResponse } from "next/server"; import { recoverTypedDataAddress } from "viem"; import { findUserByAddress } from "~~/services/database/users"; import { EIP_712_DOMAIN, EIP_712_TYPES__ADMIN_SIGN_IN } from "~~/utils/eip712"; +import { validateSafeSignature } from "~~/utils/safe-signature"; type AdminSignInBody = { signer?: string; signature?: `0x${string}`; + isSafeSignature?: boolean; + chainId?: number; }; export async function POST(req: Request) { try { - const { signer, signature } = (await req.json()) as AdminSignInBody; + const { signer, signature, isSafeSignature, chainId } = (await req.json()) as AdminSignInBody; if (!signer || !signature) { return new Response("Missing signer or signature", { status: 400 }); @@ -21,15 +24,26 @@ export async function POST(req: Request) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const recoveredAddress = await recoverTypedDataAddress({ + let isValidSignature = false; + + const typedData = { domain: EIP_712_DOMAIN, types: EIP_712_TYPES__ADMIN_SIGN_IN, primaryType: "Message", message: { action: "Sign In", description: "I authorize myself as admin" }, signature, - }); - if (recoveredAddress !== signer) { - console.error("Signer and Recovered address does not match", recoveredAddress, signer); + } as const; + + if (isSafeSignature) { + if (!chainId) return new Response("Missing chainId", { status: 400 }); + isValidSignature = await validateSafeSignature({ chainId, typedData, safeAddress: signer, signature }); + } else { + const recoveredAddress = await recoverTypedDataAddress(typedData); + isValidSignature = recoveredAddress === signer; + } + + if (!isValidSignature) { + console.error("Signer and Recovered address does not match"); return NextResponse.json({ error: "Unauthorized in batch" }, { status: 401 }); } diff --git a/packages/nextjs/app/api/grants/[grantId]/review/route.tsx b/packages/nextjs/app/api/grants/[grantId]/review/route.tsx index 173c6ea..52ae0e6 100644 --- a/packages/nextjs/app/api/grants/[grantId]/review/route.tsx +++ b/packages/nextjs/app/api/grants/[grantId]/review/route.tsx @@ -1,9 +1,11 @@ import { NextRequest, NextResponse } from "next/server"; +import { EIP712TypedData } from "@safe-global/safe-core-sdk-types"; import { recoverTypedDataAddress } from "viem"; import { reviewGrant } from "~~/services/database/grants"; import { findUserByAddress } from "~~/services/database/users"; import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT, EIP_712_TYPES__REVIEW_GRANT_WITH_NOTE } from "~~/utils/eip712"; import { PROPOSAL_STATUS, ProposalStatusType } from "~~/utils/grants"; +import { validateSafeSignature } from "~~/utils/safe-signature"; type ReqBody = { signer: string; @@ -12,11 +14,12 @@ type ReqBody = { txHash: string; txChainId: string; note?: string; + isSafeSignature?: boolean; }; export async function POST(req: NextRequest, { params }: { params: { grantId: string } }) { const { grantId } = params; - const { signature, signer, action, txHash, txChainId, note } = (await req.json()) as ReqBody; + const { signature, signer, action, txHash, txChainId, note, isSafeSignature } = (await req.json()) as ReqBody; // Validate action is valid const validActions = Object.values(PROPOSAL_STATUS); @@ -25,11 +28,11 @@ export async function POST(req: NextRequest, { params }: { params: { grantId: st return NextResponse.json({ error: "Invalid action" }, { status: 400 }); } - let recoveredAddress: string; + let isValidSignature: boolean; // If action is approved or rejected, include note in signature if (action === PROPOSAL_STATUS.APPROVED || action === PROPOSAL_STATUS.REJECTED) { - recoveredAddress = await recoverTypedDataAddress({ + const typedData = { domain: { ...EIP_712_DOMAIN, chainId: Number(txChainId) }, types: EIP_712_TYPES__REVIEW_GRANT_WITH_NOTE, primaryType: "Message", @@ -41,10 +44,21 @@ export async function POST(req: NextRequest, { params }: { params: { grantId: st note: note ?? "", }, signature, - }); + } as const; + if (isSafeSignature) { + isValidSignature = await validateSafeSignature({ + chainId: Number(txChainId), + typedData: typedData as unknown as EIP712TypedData, + signature, + safeAddress: signer, + }); + } else { + const recoveredAddress = await recoverTypedDataAddress(typedData); + isValidSignature = recoveredAddress === signer; + } } else { // Validate Signature - recoveredAddress = await recoverTypedDataAddress({ + const typedData = { domain: { ...EIP_712_DOMAIN, chainId: Number(txChainId) }, types: EIP_712_TYPES__REVIEW_GRANT, primaryType: "Message", @@ -55,11 +69,22 @@ export async function POST(req: NextRequest, { params }: { params: { grantId: st txChainId, }, signature, - }); + } as const; + if (isSafeSignature) { + isValidSignature = await validateSafeSignature({ + chainId: Number(txChainId), + typedData: typedData as unknown as EIP712TypedData, + signature, + safeAddress: signer, + }); + } else { + const recoveredAddress = await recoverTypedDataAddress(typedData); + isValidSignature = recoveredAddress === signer; + } } - if (recoveredAddress !== signer) { - console.error("Signature error", recoveredAddress, signer); + if (!isValidSignature) { + console.error("Invalid signature", signer); return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/packages/nextjs/app/api/grants/[grantId]/route.ts b/packages/nextjs/app/api/grants/[grantId]/route.ts index eedc31e..5aed5cb 100644 --- a/packages/nextjs/app/api/grants/[grantId]/route.ts +++ b/packages/nextjs/app/api/grants/[grantId]/route.ts @@ -1,8 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; +import { EIP712TypedData } from "@safe-global/safe-core-sdk-types"; import { recoverTypedDataAddress } from "viem"; import { updateGrant } from "~~/services/database/grants"; import { findUserByAddress } from "~~/services/database/users"; import { EIP_712_DOMAIN, EIP_712_TYPES__EDIT_GRANT } from "~~/utils/eip712"; +import { validateSafeSignature } from "~~/utils/safe-signature"; type ReqBody = { title?: string; @@ -11,25 +13,44 @@ type ReqBody = { signature?: `0x${string}`; signer?: string; private_note?: string; + isSafeSignature?: boolean; + chainId?: number; }; export async function PATCH(req: NextRequest, { params }: { params: { grantId: string } }) { try { const { grantId } = params; - const { title, description, signature, signer, askAmount, private_note } = (await req.json()) as ReqBody; + const { title, description, signature, signer, askAmount, private_note, isSafeSignature, chainId } = + (await req.json()) as ReqBody; if (!title || !description || !askAmount || typeof askAmount !== "number" || !signature || !signer) { return NextResponse.json({ error: "Invalid form details submited" }, { status: 400 }); } - const recoveredAddress = await recoverTypedDataAddress({ + let isValidSignature: boolean; + + const typedData = { domain: EIP_712_DOMAIN, types: EIP_712_TYPES__EDIT_GRANT, primaryType: "Message", message: { title, description, askAmount: askAmount.toString(), grantId, private_note: private_note ?? "" }, signature: signature, - }); - if (recoveredAddress !== signer) { + } as const; + + if (isSafeSignature) { + if (!chainId) return new Response("Missing chainId", { status: 400 }); + isValidSignature = await validateSafeSignature({ + chainId: Number(chainId), + typedData: typedData as unknown as EIP712TypedData, + safeAddress: signer, + signature, + }); + } else { + const recoveredAddress = await recoverTypedDataAddress(typedData); + isValidSignature = recoveredAddress === signer; + } + + if (!isValidSignature) { return NextResponse.json({ error: "Recovered address did not match signer" }, { status: 401 }); } diff --git a/packages/nextjs/app/api/grants/review/route.tsx b/packages/nextjs/app/api/grants/review/route.tsx index 002670c..bb7847f 100644 --- a/packages/nextjs/app/api/grants/review/route.tsx +++ b/packages/nextjs/app/api/grants/review/route.tsx @@ -1,10 +1,12 @@ import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; +import { EIP712TypedData } from "@safe-global/safe-core-sdk-types"; import { recoverTypedDataAddress } from "viem"; import { getAllGrantsForReview, reviewGrant } from "~~/services/database/grants"; import { findUserByAddress } from "~~/services/database/users"; import { EIP_712_DOMAIN, EIP_712_TYPES__REVIEW_GRANT_BATCH } from "~~/utils/eip712"; import { PROPOSAL_STATUS, ProposalStatusType } from "~~/utils/grants"; +import { validateSafeSignature } from "~~/utils/safe-signature"; export const dynamic = "force-dynamic"; @@ -52,10 +54,11 @@ type BatchReqBody = { txHash: string; txChainId: string; }[]; + isSafeSignature?: boolean; }; export async function POST(req: NextRequest) { - const { signer, signature, reviews } = (await req.json()) as BatchReqBody; + const { signer, signature, reviews, isSafeSignature } = (await req.json()) as BatchReqBody; if (!reviews.length) { console.error("No reviews in batch"); @@ -78,17 +81,30 @@ export async function POST(req: NextRequest) { console.error("Unauthorized", signer); return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - - const recoveredAddress = await recoverTypedDataAddress({ + const typedData = { domain: { ...EIP_712_DOMAIN, chainId: Number(txChainId) }, types: EIP_712_TYPES__REVIEW_GRANT_BATCH, primaryType: "Message", message: { reviews }, signature, - }); + } as const; + + let isValidSignature: boolean; + + if (isSafeSignature) { + isValidSignature = await validateSafeSignature({ + chainId: Number(txChainId), + typedData: typedData as unknown as EIP712TypedData, + signature, + safeAddress: signer, + }); + } else { + const recoveredAddress = await recoverTypedDataAddress(typedData); + isValidSignature = recoveredAddress === signer; + } - if (recoveredAddress !== signer) { - console.error("Signature error in batch", recoveredAddress, signer); + if (!isValidSignature) { + console.error("Signature error in batch"); return NextResponse.json({ error: "Unauthorized in batch" }, { status: 401 }); } diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 3cf6b98..141d485 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -17,6 +17,8 @@ "@ethersproject/providers": "^5.7.2", "@heroicons/react": "^2.0.11", "@rainbow-me/rainbowkit": "1.3.5", + "@safe-global/protocol-kit": "^4.0.4", + "@safe-global/safe-core-sdk-types": "^5.0.3", "@uniswap/sdk-core": "^4.0.1", "@uniswap/v2-sdk": "^3.0.1", "blo": "^1.0.1", diff --git a/packages/nextjs/utils/safe-signature.ts b/packages/nextjs/utils/safe-signature.ts new file mode 100644 index 0000000..405ad56 --- /dev/null +++ b/packages/nextjs/utils/safe-signature.ts @@ -0,0 +1,64 @@ +import Safe, { hashSafeMessage } from "@safe-global/protocol-kit"; +import { EIP712TypedData } from "@safe-global/safe-core-sdk-types"; +import * as chains from "viem/chains"; +import { Address, PublicClient } from "wagmi"; +import scaffoldConfig from "~~/scaffold.config"; + +// Mapping of chainId to RPC chain name an format followed by alchemy and infura +export const RPC_CHAIN_NAMES: Record = { + [chains.mainnet.id]: "eth-mainnet", + [chains.goerli.id]: "eth-goerli", + [chains.sepolia.id]: "eth-sepolia", + [chains.optimism.id]: "opt-mainnet", + [chains.optimismGoerli.id]: "opt-goerli", + [chains.optimismSepolia.id]: "opt-sepolia", + [chains.arbitrum.id]: "arb-mainnet", + [chains.arbitrumGoerli.id]: "arb-goerli", + [chains.arbitrumSepolia.id]: "arb-sepolia", + [chains.polygon.id]: "polygon-mainnet", + [chains.polygonMumbai.id]: "polygon-mumbai", + [chains.astar.id]: "astar-mainnet", + [chains.polygonZkEvm.id]: "polygonzkevm-mainnet", + [chains.polygonZkEvmTestnet.id]: "polygonzkevm-testnet", + [chains.base.id]: "base-mainnet", + [chains.baseGoerli.id]: "base-goerli", + [chains.baseSepolia.id]: "base-sepolia", +}; + +export const getAlchemyHttpUrl = (chainId: number) => { + return RPC_CHAIN_NAMES[chainId] + ? `https://${RPC_CHAIN_NAMES[chainId]}.g.alchemy.com/v2/${scaffoldConfig.alchemyApiKey}` + : undefined; +}; + +export const isSafeContext = async (publicClient: PublicClient, address: Address) => { + const code = await publicClient.getBytecode({ + address, + }); + + // If contract code is `0x` => no contract deployed on that address + if (!code || code === "0x") return false; + + return true; +}; + +export const validateSafeSignature = async ({ + chainId, + safeAddress, + typedData, + signature, +}: { + chainId: number; + safeAddress: string; + typedData: EIP712TypedData; + signature: string; +}) => { + const protocolKit = await Safe.init({ + provider: getAlchemyHttpUrl(chainId) as string, + safeAddress: safeAddress, + }); + const safeMessage = hashSafeMessage(typedData); + + const isValidSignature = await protocolKit.isValidSignature(safeMessage, signature); + return isValidSignature; +}; diff --git a/yarn.lock b/yarn.lock index 5c5969f..c3703de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,13 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:1.10.1": + version: 1.10.1 + resolution: "@adraffy/ens-normalize@npm:1.10.1" + checksum: 0836f394ea256972ec19a0b5e78cb7f5bcdfd48d8a32c7478afc94dd53ae44c04d1aa2303d7f3077b4f3ac2323b1f557ab9188e8059978748fdcd83e04a80dcc + languageName: node + linkType: hard + "@adraffy/ens-normalize@npm:1.9.4": version: 1.9.4 resolution: "@adraffy/ens-normalize@npm:1.9.4" @@ -1515,6 +1522,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:^1.3.3": + version: 1.4.0 + resolution: "@noble/hashes@npm:1.4.0" + checksum: 8ba816ae26c90764b8c42493eea383716396096c5f7ba6bea559993194f49d80a73c081f315f4c367e51bd2d5891700bcdfa816b421d24ab45b41cb03e4f3342 + languageName: node + linkType: hard + "@noble/secp256k1@npm:1.7.1, @noble/secp256k1@npm:~1.7.0": version: 1.7.1 resolution: "@noble/secp256k1@npm:1.7.1" @@ -2154,6 +2168,21 @@ __metadata: languageName: node linkType: hard +"@safe-global/protocol-kit@npm:^4.0.4": + version: 4.0.4 + resolution: "@safe-global/protocol-kit@npm:4.0.4" + dependencies: + "@noble/hashes": ^1.3.3 + "@safe-global/safe-core-sdk-types": ^5.0.3 + "@safe-global/safe-deployments": ^1.37.3 + abitype: ^1.0.2 + ethereumjs-util: ^7.1.5 + ethers: ^6.13.1 + semver: ^7.6.2 + checksum: 3fa1e4b1d305ca040851758bf01d4e3f0dceadb2b877e9bf66cb00704af9ff62468e3f762a9e826d66fdc86ef074706d71c88097baf8cb71a1a258b356aaaa21 + languageName: node + linkType: hard + "@safe-global/safe-apps-provider@npm:^0.18.1": version: 0.18.1 resolution: "@safe-global/safe-apps-provider@npm:0.18.1" @@ -2174,6 +2203,24 @@ __metadata: languageName: node linkType: hard +"@safe-global/safe-core-sdk-types@npm:^5.0.3": + version: 5.0.3 + resolution: "@safe-global/safe-core-sdk-types@npm:5.0.3" + dependencies: + abitype: ^1.0.2 + checksum: 49ad154cae576e28c114dc8c246151827c1de0c757eee1c6bceef0a360a5277398207235539b20949ab93dea479362a28d7424afc4c0401b6553b284b1f36844 + languageName: node + linkType: hard + +"@safe-global/safe-deployments@npm:^1.37.3": + version: 1.37.3 + resolution: "@safe-global/safe-deployments@npm:1.37.3" + dependencies: + semver: ^7.6.2 + checksum: 8c83944822aea1468fc51944edb964a050065f0f7404ee138a34d74228d7f5113102ec4e10f1866798969624193a072f6d65f2b6c8b79dc76f03081cefc5f06a + languageName: node + linkType: hard + "@safe-global/safe-gateway-typescript-sdk@npm:^3.5.3": version: 3.12.0 resolution: "@safe-global/safe-gateway-typescript-sdk@npm:3.12.0" @@ -2300,6 +2347,8 @@ __metadata: "@ethersproject/providers": ^5.7.2 "@heroicons/react": ^2.0.11 "@rainbow-me/rainbowkit": 1.3.5 + "@safe-global/protocol-kit": ^4.0.4 + "@safe-global/safe-core-sdk-types": ^5.0.3 "@trivago/prettier-plugin-sort-imports": ^4.1.1 "@types/node": ^17.0.35 "@types/nprogress": ^0 @@ -4393,6 +4442,21 @@ __metadata: languageName: node linkType: hard +"abitype@npm:^1.0.2": + version: 1.0.6 + resolution: "abitype@npm:1.0.6" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + checksum: 0bf6ed5ec785f372746c3ec5d6c87bf4d8cf0b6db30867b8d24e86fbc66d9f6599ae3d463ccd49817e67eedec6deba7cdae317bcf4da85b02bc48009379b9f84 + languageName: node + linkType: hard + "abort-controller@npm:^3.0.0": version: 3.0.0 resolution: "abort-controller@npm:3.0.0" @@ -7677,7 +7741,7 @@ __metadata: languageName: node linkType: hard -"ethereumjs-util@npm:^7.1.4": +"ethereumjs-util@npm:^7.1.4, ethereumjs-util@npm:^7.1.5": version: 7.1.5 resolution: "ethereumjs-util@npm:7.1.5" dependencies: @@ -7760,6 +7824,21 @@ __metadata: languageName: node linkType: hard +"ethers@npm:^6.13.1": + version: 6.13.2 + resolution: "ethers@npm:6.13.2" + dependencies: + "@adraffy/ens-normalize": 1.10.1 + "@noble/curves": 1.2.0 + "@noble/hashes": 1.3.2 + "@types/node": 18.15.13 + aes-js: 4.0.0-beta.5 + tslib: 2.4.0 + ws: 8.17.1 + checksum: 981860c736c7ae121774ad38ea07e3611ce524a77d2fcb77db499b65afe0cbe8f344cd5204d94b68b316349ff770fd2a7d9c8b2039da41c072f98d9864099925 + languageName: node + linkType: hard + "ethjs-unit@npm:0.1.6": version: 0.1.6 resolution: "ethjs-unit@npm:0.1.6" @@ -13710,6 +13789,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.6.2": + version: 7.6.3 + resolution: "semver@npm:7.6.3" + bin: + semver: bin/semver.js + checksum: 4110ec5d015c9438f322257b1c51fe30276e5f766a3f64c09edd1d7ea7118ecbc3f379f3b69032bacf13116dc7abc4ad8ce0d7e2bd642e26b0d271b56b61a7d8 + languageName: node + linkType: hard + "serialize-javascript@npm:6.0.0": version: 6.0.0 resolution: "serialize-javascript@npm:6.0.0" @@ -16016,6 +16104,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 442badcce1f1178ec87a0b5372ae2e9771e07c4929a3180321901f226127f252441e8689d765aa5cfba5f50ac60dd830954afc5aeae81609aefa11d3ddf5cecf + languageName: node + linkType: hard + "ws@npm:8.5.0": version: 8.5.0 resolution: "ws@npm:8.5.0"