diff --git a/.changeset/gentle-beers-jog.md b/.changeset/gentle-beers-jog.md new file mode 100644 index 000000000..6ffda0ec4 --- /dev/null +++ b/.changeset/gentle-beers-jog.md @@ -0,0 +1,5 @@ +--- +"@frames.js/render": patch +--- + +fix: allow onTransaction/onSignature to be called from contexts outside of frame e.g. miniapp diff --git a/.changeset/lazy-deers-laugh.md b/.changeset/lazy-deers-laugh.md new file mode 100644 index 000000000..8d2b57ce4 --- /dev/null +++ b/.changeset/lazy-deers-laugh.md @@ -0,0 +1,6 @@ +--- +"template-next-starter-with-examples": patch +"create-frames": patch +--- + +feat: miniapp transaction example diff --git a/.changeset/violet-elephants-taste.md b/.changeset/violet-elephants-taste.md new file mode 100644 index 000000000..2a274c1fa --- /dev/null +++ b/.changeset/violet-elephants-taste.md @@ -0,0 +1,5 @@ +--- +"@frames.js/debugger": patch +--- + +feat: composer action transaction support diff --git a/packages/debugger/app/components/action-debugger.tsx b/packages/debugger/app/components/action-debugger.tsx index fbfb2d2e3..c6835584b 100644 --- a/packages/debugger/app/components/action-debugger.tsx +++ b/packages/debugger/app/components/action-debugger.tsx @@ -438,6 +438,7 @@ export const ActionDebugger = React.forwardRef< {!!composeFormActionDialogSignal && ( { composeFormActionDialogSignal.resolve(undefined); @@ -447,6 +448,8 @@ export const ActionDebugger = React.forwardRef< composerActionState: composerState, }); }} + onTransaction={farcasterFrameConfig.onTransaction} + onSignature={farcasterFrameConfig.onSignature} /> )} diff --git a/packages/debugger/app/components/composer-form-action-dialog.tsx b/packages/debugger/app/components/composer-form-action-dialog.tsx index e42b4cf0c..bc9692d41 100644 --- a/packages/debugger/app/components/composer-form-action-dialog.tsx +++ b/packages/debugger/app/components/composer-form-action-dialog.tsx @@ -1,15 +1,17 @@ import { Dialog, + DialogContent, + DialogFooter, DialogHeader, DialogTitle, - DialogFooter, - DialogContent, } from "@/components/ui/dialog"; +import { OnSignatureFunc, OnTransactionFunc } from "@frames.js/render"; import type { ComposerActionFormResponse, ComposerActionState, } from "frames.js/types"; -import { useEffect, useRef } from "react"; +import { useCallback, useEffect, useRef } from "react"; +import { Abi, TypedDataDomain } from "viem"; import { z } from "zod"; const composerFormCreateCastMessageSchema = z.object({ @@ -23,38 +25,172 @@ const composerFormCreateCastMessageSchema = z.object({ }), }); +const ethSendTransactionActionSchema = z.object({ + chainId: z.string(), + method: z.literal("eth_sendTransaction"), + attribution: z.boolean().optional(), + params: z.object({ + abi: z.custom(), + to: z.custom<`0x${string}`>( + (val): val is `0x${string}` => + typeof val === "string" && val.startsWith("0x") + ), + value: z.string().optional(), + data: z + .custom<`0x${string}`>( + (val): val is `0x${string}` => + typeof val === "string" && val.startsWith("0x") + ) + .optional(), + }), +}); + +const ethSignTypedDataV4ActionSchema = z.object({ + chainId: z.string(), + method: z.literal("eth_signTypedData_v4"), + params: z.object({ + domain: z.custom(), + types: z.unknown(), + primaryType: z.string(), + message: z.record(z.unknown()), + }), +}); + +const transactionRequestBodySchema = z.object({ + requestId: z.string(), + tx: z.union([ethSendTransactionActionSchema, ethSignTypedDataV4ActionSchema]), +}); + +const composerActionMessageSchema = z.discriminatedUnion("type", [ + composerFormCreateCastMessageSchema, + z.object({ + type: z.literal("requestTransaction"), + data: transactionRequestBodySchema, + }), +]); + type ComposerFormActionDialogProps = { composerActionForm: ComposerActionFormResponse; onClose: () => void; onSave: (arg: { composerState: ComposerActionState }) => void; + onTransaction?: OnTransactionFunc; + onSignature?: OnSignatureFunc; + // TODO: Consider moving this into return value of onTransaction + connectedAddress?: `0x${string}`; }; export function ComposerFormActionDialog({ composerActionForm, onClose, onSave, + onTransaction, + onSignature, + connectedAddress, }: ComposerFormActionDialogProps) { const onSaveRef = useRef(onSave); onSaveRef.current = onSave; + const iframeRef = useRef(null); + + const postMessageToIframe = useCallback( + (message: any) => { + if (iframeRef.current && iframeRef.current.contentWindow) { + iframeRef.current.contentWindow.postMessage( + message, + new URL(composerActionForm.url).origin + ); + } + }, + [composerActionForm.url] + ); + useEffect(() => { const handleMessage = (event: MessageEvent) => { - const result = composerFormCreateCastMessageSchema.safeParse(event.data); + const result = composerActionMessageSchema.safeParse(event.data); // on error is not called here because there can be different messages that don't have anything to do with composer form actions // instead we are just waiting for the correct message if (!result.success) { - console.warn("Invalid message received", event.data); + console.warn("Invalid message received", event.data, result.error); return; } - if (result.data.data.cast.embeds.length > 2) { - console.warn("Only first 2 embeds are shown in the cast"); - } + const message = result.data; - onSaveRef.current({ - composerState: result.data.data.cast, - }); + if (message.type === "requestTransaction") { + if (message.data.tx.method === "eth_sendTransaction") { + onTransaction?.({ + transactionData: message.data.tx, + }).then((txHash) => { + if (txHash) { + postMessageToIframe({ + type: "transactionResponse", + data: { + requestId: message.data.requestId, + success: true, + receipt: { + address: connectedAddress, + transactionId: txHash, + }, + }, + }); + } else { + postMessageToIframe({ + type: "transactionResponse", + data: { + requestId: message.data.requestId, + success: false, + message: "User rejected the request", + }, + }); + } + }); + } else if (message.data.tx.method === "eth_signTypedData_v4") { + onSignature?.({ + signatureData: { + chainId: message.data.tx.chainId, + method: "eth_signTypedData_v4", + params: { + domain: message.data.tx.params.domain, + types: message.data.tx.params.types as any, + primaryType: message.data.tx.params.primaryType, + message: message.data.tx.params.message, + }, + }, + }).then((signature) => { + if (signature) { + postMessageToIframe({ + type: "signatureResponse", + data: { + requestId: message.data.requestId, + success: true, + receipt: { + address: connectedAddress, + transactionId: signature, + }, + }, + }); + } else { + postMessageToIframe({ + type: "signatureResponse", + data: { + requestId: message.data.requestId, + success: false, + message: "User rejected the request", + }, + }); + } + }); + } + } else if (message.type === "createCast") { + if (message.data.cast.embeds.length > 2) { + console.warn("Only first 2 embeds are shown in the cast"); + } + + onSaveRef.current({ + composerState: message.data.cast, + }); + } }; window.addEventListener("message", handleMessage); @@ -80,6 +216,7 @@ export function ComposerFormActionDialog({
diff --git a/packages/render/src/types.ts b/packages/render/src/types.ts index 16b39b520..7ad0310b6 100644 --- a/packages/render/src/types.ts +++ b/packages/render/src/types.ts @@ -22,8 +22,10 @@ import type { FrameStackAPI } from "./use-frame-stack"; export type OnTransactionArgs = { transactionData: TransactionTargetResponseSendTransaction; - frameButton: FrameButton; - frame: Frame; + /** If the transaction was triggered by a frame button, this will be the frame that it was from */ + frame?: Frame; + /** If the transaction was triggered by a frame button, this will be the frame button that triggered it */ + frameButton?: FrameButton; }; export type OnTransactionFunc = ( @@ -32,8 +34,10 @@ export type OnTransactionFunc = ( export type OnSignatureArgs = { signatureData: TransactionTargetResponseSignTypedDataV4; - frameButton: FrameButton; - frame: Frame; + /** If the signature was triggered by a frame button, this will be the frame that it was from */ + frame?: Frame; + /** If the signature was triggered by a frame button, this will be the frame button that triggered it */ + frameButton?: FrameButton; }; export type OnSignatureFunc = ( @@ -74,7 +78,7 @@ export type OnConnectWalletFunc = () => void; export type SignFrameActionFunc< TSignerStorageType = Record, TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = ( actionContext: SignerStateActionContext ) => Promise>; @@ -84,7 +88,7 @@ export type UseFetchFrameSignFrameActionFunction< unknown, Record >, - TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload > = (arg: { actionContext: TSignerStateActionContext; /** @@ -96,7 +100,7 @@ export type UseFetchFrameSignFrameActionFunction< export type UseFetchFrameOptions< TSignerStorageType = Record, TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { stackAPI: FrameStackAPI; stackDispatch: React.Dispatch; @@ -212,7 +216,7 @@ export type UseFetchFrameOptions< export type UseFrameOptions< TSignerStorageType = Record, TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { /** skip frame signing, for frames that don't verify signatures */ dangerousSkipSigning?: boolean; @@ -286,7 +290,7 @@ export type UseFrameOptions< type SignerStateActionSharedContext< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { target?: string; frameButton: FrameButton; @@ -303,14 +307,14 @@ type SignerStateActionSharedContext< export type SignerStateDefaultActionContext< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { type?: "default"; } & SignerStateActionSharedContext; export type SignerStateTransactionDataActionContext< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { type: "tx-data"; /** Wallet address used to create the transaction, available only for "tx" button actions */ @@ -319,7 +323,7 @@ export type SignerStateTransactionDataActionContext< export type SignerStateTransactionPostActionContext< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { type: "tx-post"; /** Wallet address used to create the transaction, available only for "tx" button actions */ @@ -329,7 +333,7 @@ export type SignerStateTransactionPostActionContext< export type SignerStateActionContext< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = | SignerStateDefaultActionContext | SignerStateTransactionDataActionContext< @@ -342,7 +346,7 @@ export type SignerStateActionContext< >; export type SignedFrameAction< - TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload > = { body: TFrameActionBodyType; searchParams: URLSearchParams; @@ -353,7 +357,7 @@ export type SignFrameActionFunction< unknown, Record > = SignerStateActionContext, - TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload > = ( actionContext: TSignerStateActionContext ) => Promise>; @@ -361,7 +365,7 @@ export type SignFrameActionFunction< export interface SignerStateInstance< TSignerStorageType = Record, TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > { signer: TSignerStorageType | null; /** @@ -388,7 +392,7 @@ export type FramePOSTRequest< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - > = SignerStateActionContext, + > = SignerStateActionContext > = | { method: "POST"; @@ -414,7 +418,7 @@ export type FrameRequest< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - > = SignerStateActionContext, + > = SignerStateActionContext > = FrameGETRequest | FramePOSTRequest; export type FrameStackBase = { @@ -527,7 +531,7 @@ type ButtonPressFunction< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - >, + > > = ( frame: Frame, frameButton: FrameButton, @@ -570,7 +574,7 @@ export type CastActionRequest< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - > = SignerStateActionContext, + > = SignerStateActionContext > = Omit< FramePOSTRequest, "method" | "frameButton" | "sourceFrame" | "signerStateActionContext" @@ -589,7 +593,7 @@ export type ComposerActionRequest< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - > = SignerStateActionContext, + > = SignerStateActionContext > = Omit< FramePOSTRequest, "method" | "frameButton" | "sourceFrame" | "signerStateActionContext" @@ -609,7 +613,7 @@ export type FetchFrameFunction< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - > = SignerStateActionContext, + > = SignerStateActionContext > = ( request: | FrameRequest @@ -625,7 +629,7 @@ export type FetchFrameFunction< export type FrameState< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { fetchFrame: FetchFrameFunction< SignerStateActionContext diff --git a/templates/next-starter-with-examples/app/examples/transaction-miniapp/frames/actions/miniapp/route.tsx b/templates/next-starter-with-examples/app/examples/transaction-miniapp/frames/actions/miniapp/route.tsx new file mode 100644 index 000000000..17b0e12d7 --- /dev/null +++ b/templates/next-starter-with-examples/app/examples/transaction-miniapp/frames/actions/miniapp/route.tsx @@ -0,0 +1,34 @@ +import { NextRequest } from "next/server"; +import { appURL } from "../../../../../utils"; +import { frames } from "../../frames"; +import { composerAction, composerActionForm, error } from "frames.js/core"; + +export const GET = async (req: NextRequest) => { + return composerAction({ + action: { + type: "post", + }, + icon: "credit-card", + name: "Send a tx", + aboutUrl: `${appURL()}/examples/transaction-miniapp`, + description: "Send ETH to address", + imageUrl: "https://framesjs.org/logo.png", + }); +}; + +export const POST = frames(async (ctx) => { + const walletAddress = await ctx.walletAddress(); + + const miniappUrl = new URL("/examples/transaction-miniapp/miniapp", appURL()); + + if (walletAddress) { + miniappUrl.searchParams.set("fromAddress", walletAddress); + } else { + return error("Must be authenticated"); + } + + return composerActionForm({ + title: "Send ETH", + url: miniappUrl.toString(), + }); +}); diff --git a/templates/next-starter-with-examples/app/examples/transaction-miniapp/frames/frames.ts b/templates/next-starter-with-examples/app/examples/transaction-miniapp/frames/frames.ts new file mode 100644 index 000000000..1a92280d3 --- /dev/null +++ b/templates/next-starter-with-examples/app/examples/transaction-miniapp/frames/frames.ts @@ -0,0 +1,18 @@ +import { createFrames } from "frames.js/next"; +import { appURL } from "../../../utils"; +import { + farcasterHubContext, + warpcastComposerActionState, +} from "frames.js/middleware"; +import { DEFAULT_DEBUGGER_HUB_URL } from "../../../debug"; + +export const frames = createFrames({ + baseUrl: `${appURL()}/examples/transaction-miniapp/frames`, + debug: process.env.NODE_ENV === "development", + middleware: [ + farcasterHubContext({ + hubHttpUrl: DEFAULT_DEBUGGER_HUB_URL, + }), + warpcastComposerActionState(), // necessary to detect and parse state necessary for composer actions + ], +}); diff --git a/templates/next-starter-with-examples/app/examples/transaction-miniapp/frames/route.tsx b/templates/next-starter-with-examples/app/examples/transaction-miniapp/frames/route.tsx new file mode 100644 index 000000000..0c8c57c60 --- /dev/null +++ b/templates/next-starter-with-examples/app/examples/transaction-miniapp/frames/route.tsx @@ -0,0 +1,29 @@ +/* eslint-disable react/jsx-key */ +import { Button } from "frames.js/next"; +import { frames } from "./frames"; +import { appURL } from "../../../utils"; + +function constructCastActionUrl(params: { url: string }): string { + // Construct the URL + const baseUrl = "https://warpcast.com/~/composer-action"; + const urlParams = new URLSearchParams({ + url: params.url, + }); + + return `${baseUrl}?${urlParams.toString()}`; +} + +export const GET = frames(async (ctx) => { + const transactionMiniappUrl = constructCastActionUrl({ + url: `${appURL()}/examples/transaction-miniapp/frames/actions/miniapp`, + }); + + return { + image:
Transaction Miniapp Example
, + buttons: [ + , + ], + }; +}); diff --git a/templates/next-starter-with-examples/app/examples/transaction-miniapp/miniapp/page.tsx b/templates/next-starter-with-examples/app/examples/transaction-miniapp/miniapp/page.tsx new file mode 100644 index 000000000..16eec5e72 --- /dev/null +++ b/templates/next-starter-with-examples/app/examples/transaction-miniapp/miniapp/page.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { v4 as uuidv4 } from "uuid"; + +// pass state from frame message +export default function MiniappPage({ + searchParams, +}: { + // provided by URL returned from composer action server + searchParams: { + fromAddress: string; + }; +}) { + const windowObject = typeof window !== "undefined" ? window : null; + + const [message, setMessage] = useState(null); + + const handleMessage = useCallback((m: MessageEvent) => { + console.log("received", m); + + if (m.source === windowObject?.parent) { + setMessage(m.data); + } + }, []); + + useEffect(() => { + windowObject?.addEventListener("message", handleMessage); + + return () => { + windowObject?.removeEventListener("message", handleMessage); + }; + }, []); + + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const ethAmount = formData.get("ethAmount") as string; + const recipientAddress = formData.get( + "recipientAddress" + ) as `0x${string}`; + + // Handle form submission here + windowObject?.parent.postMessage( + { + type: "requestTransaction", + data: { + requestId: uuidv4(), + tx: { + chainId: "eip155:10", + method: "eth_sendTransaction", + params: { + abi: [], + to: recipientAddress, + value: (BigInt(ethAmount) * BigInt(10 ** 18)).toString(), + }, + }, + }, + }, + "*" + ); + }, + [windowObject?.parent] + ); + + const handleRequestSignature = useCallback(() => { + windowObject?.parent.postMessage( + { + type: "requestTransaction", + data: { + requestId: uuidv4(), + tx: { + chainId: "eip155:10", // OP Mainnet 10 + method: "eth_signTypedData_v4", + params: { + domain: { + chainId: 10, + }, + types: { + Person: [ + { name: "name", type: "string" }, + { name: "wallet", type: "address" }, + ], + Mail: [ + { name: "from", type: "Person" }, + { name: "to", type: "Person" }, + { name: "contents", type: "string" }, + ], + }, + primaryType: "Mail", + message: { + from: { + name: "Cow", + wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + }, + to: { + name: "Bob", + wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + }, + contents: "Hello, Bob!", + }, + }, + }, + }, + }, + "*" + ); + }, [windowObject?.parent]); + + return ( +
+
+ + + + + + + +
+ +
+ +
+ {message?.data?.success ? ( +
+ Transaction sent successfully: {message?.data?.receipt?.transactionId}{" "} + sent from {message?.data?.receipt?.address} +
+ ) : ( +
{message?.data?.message}
+ )} +
+ ); +} diff --git a/templates/next-starter-with-examples/app/examples/transaction-miniapp/page.tsx b/templates/next-starter-with-examples/app/examples/transaction-miniapp/page.tsx new file mode 100644 index 000000000..a89eaa38a --- /dev/null +++ b/templates/next-starter-with-examples/app/examples/transaction-miniapp/page.tsx @@ -0,0 +1,26 @@ +import { createExampleURL } from "../../utils"; +import type { Metadata } from "next"; +import { fetchMetadata } from "frames.js/next"; +import { Frame } from "../../components/Frame"; + +export async function generateMetadata(): Promise { + return { + title: "Frames.js Transaction Miniapp", + other: { + ...(await fetchMetadata( + createExampleURL("/examples/transaction-miniapp/frames") + )), + }, + }; +} + +export default async function Home() { + const metadata = await generateMetadata(); + + return ( + + ); +} diff --git a/templates/next-starter-with-examples/package.json b/templates/next-starter-with-examples/package.json index dbb9b9207..13abbfd4c 100644 --- a/templates/next-starter-with-examples/package.json +++ b/templates/next-starter-with-examples/package.json @@ -19,7 +19,8 @@ "next": "^14.1.4", "react": "^18.2.0", "react-dom": "^18.2.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "uuid": "^10.0.0" }, "engines": { "node": ">=18.17.0" @@ -29,12 +30,13 @@ "@types/node": "^18.17.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", + "@types/uuid": "^10.0.0", "autoprefixer": "^10.0.1", "concurrently": "^8.2.2", "dotenv": "^16.4.5", - "is-port-reachable": "^4.0.0", "eslint": "^8.56.0", "eslint-config-next": "^14.1.0", + "is-port-reachable": "^4.0.0", "postcss": "^8", "tailwindcss": "^3.3.0", "typescript": "^5.4.5" diff --git a/yarn.lock b/yarn.lock index 0bb9ae9a5..39c2b84fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5802,6 +5802,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== +"@types/uuid@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" + integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -17996,6 +18001,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"