diff --git a/apps/dapp/package.json b/apps/dapp/package.json index 7167869..ccd9c39 100644 --- a/apps/dapp/package.json +++ b/apps/dapp/package.json @@ -26,10 +26,12 @@ "permissionless": "^0.1.10", "react": "^18.2.0", "react-dom": "^18.2.0", + "spc-lib": "workspace:*", "tailwind-merge": "^2.2.1", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", "typescript": "^5.4.2", + "usehooks-ts": "^3.0.1", "viem": "^2.8.10" } } \ No newline at end of file diff --git a/apps/dapp/src/components/claim-coupon-button.tsx b/apps/dapp/src/components/claim-coupon-button.tsx new file mode 100644 index 0000000..42bc6cf --- /dev/null +++ b/apps/dapp/src/components/claim-coupon-button.tsx @@ -0,0 +1,124 @@ +import { Button } from "@/components/ui/button"; +import { formatMintCouponToModuleExecutionCalldata } from "@/lib/example-nft"; +import { bundlerClient, publicClient } from "@/lib/permissionless"; +import { getPayeeOrigin, getPaymentOrigin } from "@/lib/utils"; +import { + ENTRYPOINT_ADDRESS_V07, + getAccountNonce, + getUserOperationHash, + type UserOperation, +} from "permissionless"; +import { + fallbackToIframeCredentialCreation, + getAvailableCredentials, + getPaymentDetails, + payWithSPC, +} from "spc-lib"; +import type { Address } from "viem"; +import { baseSepolia } from "viem/chains"; + +export function ClaimCouponButton() { + return ( + + ); +} + +const getMintNftUserOperation = async (address: Address) => { + const nonce = await getAccountNonce(publicClient, { + sender: address, + entryPoint: ENTRYPOINT_ADDRESS_V07, + }); + + console.log("nonce ", nonce); + + let userOperation: UserOperation<"v0.7"> = { + sender: address, + nonce, + callData: formatMintCouponToModuleExecutionCalldata(), + // accountGasLimits: 8000000n, + callGasLimit: 900080n, + verificationGasLimit: 8005650n, + preVerificationGas: 801330n, + + maxFeePerGas: 113000000n, + maxPriorityFeePerGas: 113000100n, + signature: "0x", + }; + + // const gasEstimate = await bundlerClient.estimateUserOperationGas({ + // userOperation, + // }); + + // console.log("gasEstimate ", gasEstimate); + + // const paymasterData = await paymasterClient.sponsorUserOperation({ + // userOperation, + // }); + + // console.log("paymasterData", paymasterData); + + // userOperation = { ...userOperation, ...paymasterData }; + + // const userOperation = { + // callData: + // "0x7bb374280000000000000000000000004a56fd1d63d99978fdb3ac5c152ea122514b6792000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000443efc46400000000000000000000000000000000000000000000000000000000", + // initCode: "0x", + // callGasLimit: "117688", + // verificationGasLimit: "61421", + // preVerificationGas: "285339", + // maxFeePerGas: "75550508", + // maxPriorityFeePerGas: "110000", + // nonce: "0", + // paymasterAndData: "0x", + // sender: "0x2fe7892721d8279cec0f8687c4b2e922ca7b76b0", + // signature: "0x", + // }; + + const userOperationHash = getUserOperationHash({ + userOperation, + chainId: baseSepolia.id, + entryPoint: ENTRYPOINT_ADDRESS_V07, + }); + + return { userOperation, userOperationHash }; +}; diff --git a/apps/dapp/src/components/create-payment-button.tsx b/apps/dapp/src/components/create-payment-button.tsx deleted file mode 100644 index d03520d..0000000 --- a/apps/dapp/src/components/create-payment-button.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants"; -import { getPaymentDetails, getPaymentOrigin, payWithSPC } from "@/lib/utils"; -import { getAllowedCredentialsSchema } from "helpers"; - -const getIframe = () => { - const iframeDialog = document.getElementById( - WALLET_IFRAME_DIALOG_ID, - ) as HTMLIFrameElement | null; - - const iframe = iframeDialog?.querySelector("iframe"); - - return iframe; -}; - -const fallbackToIframeCredentialCreation = async () => { - // - trigger the iframe container to open so the user can create a credential - const iframeDialog = document.getElementById( - WALLET_IFRAME_DIALOG_ID, - ) as HTMLDialogElement | null; - - iframeDialog?.classList.remove("hidden"); - iframeDialog?.classList.add("flex"); - - iframeDialog?.showModal(); -}; - -/** - * A utility function to wait for a message from the wallet iframe - */ -const createMessagePromise = (eventType: string): Promise => { - const iframe = getIframe(); - - if (!iframe) throw new Error("No iframe found"); - - const iframeOrigin = iframe?.src ? new URL(iframe.src).origin : undefined; - - let timeout: NodeJS.Timeout | undefined; - const credentialsPromise: Promise = new Promise( - (resolve, reject) => { - // biome-ignore lint/suspicious/noExplicitAny: - function handleMessage(event: any) { - const eventOrigin = new URL(event.origin).origin; - if (eventOrigin !== iframeOrigin) return; - - console.log("received message", event); - - if (event.data.type === eventType) { - resolve(event.data); - } - // Remove the event listener to clean up - window.removeEventListener("message", handleMessage); - } - - // Add an event listener to listen for messages from the iframe - window.addEventListener("message", handleMessage, false); - - // Set a timeout to reject the promise if no response is received within a specific timeframe - timeout = setTimeout(() => { - window.removeEventListener("message", handleMessage); - reject(new Error("Timeout waiting for credentials response")); - }, 10000); // 10 seconds timeout - }, - ); - - // ! Ensure to clean up on promise resolution or rejection - credentialsPromise.finally(() => clearTimeout(timeout)); - - return credentialsPromise; -}; - -/** - * Get the available credentials from the wallet iframe - */ -const getAvailableCredentials = async () => { - const iframe = getIframe(); - - if (!iframe) throw new Error("No iframe found"); - - // - Create a Promise that resolves when the expected message is received from the wallet iframe - const waitForCredentials = async () => { - const eventData = await createMessagePromise("credentials.get"); - const parsedMessage = getAllowedCredentialsSchema.parse(eventData); - - return parsedMessage.credentials; - }; - - // - Ask the iframe for the available credentials - iframe.contentWindow?.postMessage( - { type: "credentials.get" }, - getPaymentOrigin(), - ); - - return await waitForCredentials(); -}; - -export function CreatePaymentButton() { - return ( - - ); -} diff --git a/apps/dapp/src/components/nft-image-card.tsx b/apps/dapp/src/components/nft-image-card.tsx index 9e20007..700395a 100644 --- a/apps/dapp/src/components/nft-image-card.tsx +++ b/apps/dapp/src/components/nft-image-card.tsx @@ -1,34 +1,48 @@ -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { getNftUri, getBalance } from "@/lib/example-nft"; import React from "react"; +import { ClaimCouponButton } from "./claim-coupon-button"; +import { useLocalStorage } from "usehooks-ts"; +import type { Address } from "viem"; export function NftImageCard() { - const tokenId = 1n; const [uri, setUri] = React.useState(null); const [hasClaimed, setHasClaimed] = React.useState(false); + const [address] = useLocalStorage
("address", undefined); React.useEffect(() => { - getNftUri(0).then((r) => setUri(r)); + getNftUri(0n).then((r) => setUri(r)); }, []); React.useEffect(() => { - getBalance('0x3896a2938d345d3A351cE152AF1b8Cb17bb006be', 0n).then((r) => { - console.log({ r }); - setHasClaimed(r > 0) - }); - }, []); + getBalance(address, 0n).then((r) => setHasClaimed(r > 0)); + }, [address]); return ( 10 Eth Coupon - Claim your free 10 eth coupon which can + + Claim your free 10 eth coupon which can + - - NFT + + {uri && NFT} - -

You have {hasClaimed ? '' : 'not'} claimed

+ + {hasClaimed ? ( +

You have claimed

+ // TODO: add the txHash of the claim + ) : ( + + )}
); diff --git a/apps/dapp/src/components/wallet-iframe-dialog.astro b/apps/dapp/src/components/wallet-iframe-dialog.astro index e4d5b91..c02703c 100644 --- a/apps/dapp/src/components/wallet-iframe-dialog.astro +++ b/apps/dapp/src/components/wallet-iframe-dialog.astro @@ -1,6 +1,6 @@ --- import { cn } from "@/lib/utils"; -import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants"; +import { WALLET_IFRAME_DIALOG_ID } from "spc-lib"; import { getPaymentOrigin } from "@/lib/utils"; const paymentOrigin = getPaymentOrigin(); @@ -21,7 +21,7 @@ const paymentOrigin = getPaymentOrigin(); allow={`payment ${paymentOrigin}`}> diff --git a/apps/wallet/src/pages/index.astro b/apps/wallet/src/pages/index.astro index 4f56edd..d27a68c 100644 --- a/apps/wallet/src/pages/index.astro +++ b/apps/wallet/src/pages/index.astro @@ -27,19 +27,25 @@ import { UsernameForm } from "../components/forms/username"; diff --git a/apps/wallet/src/pages/relay/deploy.ts b/apps/wallet/src/pages/relay/deploy.ts index 786ba72..721b573 100644 --- a/apps/wallet/src/pages/relay/deploy.ts +++ b/apps/wallet/src/pages/relay/deploy.ts @@ -76,9 +76,25 @@ const relaySafeDeploy = async ({ account: privateKeyToAccount(import.meta.env.PRIVATE_KEY), }); + // const { request, result } = await publicClient.simulateContract({ + // account: walletClient.account, + // address: V6_BUNDLER_ONIT_FACTORY_ADDRESS, + // abi: createSafe4337Abi, + // functionName: "createSafe4337", + // args: [publicKey, 0n], + // }); + + // const { request, result } = await publicClient.simulateContract({ + // account: walletClient.account, + // address: V6_BUNDLER_ONIT_FACTORY_ADDRESS, + // abi: createBasic4337Abi, + // functionName: "createBasic4337", + // args: [publicKey, 0n], + // }); + const { request, result } = await publicClient.simulateContract({ account: walletClient.account, - address: ONIT_FACTORY_ADDRESS, + address: V7_BUNDLER_SAFE_FACTORY_ADDRESS, abi: createSafe4337Abi, functionName: "createSafe4337", args: [publicKey, 0n], @@ -91,9 +107,18 @@ const relaySafeDeploy = async ({ // abis -------------------------------------------- -const ONIT_FACTORY_ADDRESS = +const V7_BUNDLER_SAFE_ONIT_FACTORY_ADDRESS = "0x42ab880ea77fc7a09eb6ba0fe82fbc9901c114b6" as const; +const V6_BUNDLER_SAFE_ONIT_FACTORY_ADDRESS = + "0xa4025cc96a042a4522F9115478D4d527F954a40E" as const; + +const V6_BUNDLER_ONIT_FACTORY_ADDRESS = + "0x70b3b72f7737c017888d64e60c5bfee6ca226b85" as const; + +const V7_BUNDLER_SAFE_FACTORY_ADDRESS = + "0x758f1ce181e74b4eb3d38441a0b2b117991c5cc8" as const; + const createSafe4337Abi = [ { type: "function", @@ -112,3 +137,22 @@ const createSafe4337Abi = [ stateMutability: "nonpayable", }, ] as const; + +const createBasic4337Abi = [ + { + type: "function", + name: "createBasic4337", + inputs: [ + { + name: "passkeyPublicKey", + type: "uint256[2]", + internalType: "uint256[2]", + }, + { name: "nonce", type: "uint256", internalType: "uint256" }, + ], + outputs: [ + { name: "onitAccountAddress", type: "address", internalType: "address" }, + ], + stateMutability: "nonpayable", + }, +] as const; diff --git a/packages/helpers/index.ts b/packages/helpers/index.ts index dadc488..437e282 100644 --- a/packages/helpers/index.ts +++ b/packages/helpers/index.ts @@ -1,5 +1,4 @@ export * from "./base64url"; export * from "./domains"; export * from "./is-spc-available"; -export * from "./validators"; export * from "./webauthn"; diff --git a/packages/helpers/tsconfig.json b/packages/helpers/tsconfig.json new file mode 100644 index 0000000..4f86b7e --- /dev/null +++ b/packages/helpers/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "declaration": true, + "composite": true, + "sourceMap": true, + "declarationMap": true, + "moduleResolution": "NodeNext", + "module": "NodeNext", + "outDir": "dist", + "strict": true, + "noUncheckedIndexedAccess": true, + "lib": [ + "es2022" + ] + } +} \ No newline at end of file diff --git a/packages/spc-lib/package.json b/packages/spc-lib/package.json new file mode 100644 index 0000000..89c2bc9 --- /dev/null +++ b/packages/spc-lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "spc-lib", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "dependencies": { + "helpers": "workspace:*", + "zod": "^3.22.4" + } +} \ No newline at end of file diff --git a/apps/dapp/src/lib/constants.ts b/packages/spc-lib/src/constants.ts similarity index 100% rename from apps/dapp/src/lib/constants.ts rename to packages/spc-lib/src/constants.ts diff --git a/packages/spc-lib/src/index.ts b/packages/spc-lib/src/index.ts new file mode 100644 index 0000000..3c9d681 --- /dev/null +++ b/packages/spc-lib/src/index.ts @@ -0,0 +1,4 @@ +export * from "./constants"; +export * from "./utils"; +export * from "./validators"; +export * from "./pay-with-spc"; diff --git a/packages/spc-lib/src/pay-with-spc.ts b/packages/spc-lib/src/pay-with-spc.ts new file mode 100644 index 0000000..2b6f3e5 --- /dev/null +++ b/packages/spc-lib/src/pay-with-spc.ts @@ -0,0 +1,104 @@ +import { defaultInstrument, fromBuffer, toBuffer } from "helpers"; + +export type Address = `0x${string}`; +export type SerialisableCredentialRequestOptions = Omit< + Partial, + "allowedCredentials" | "challenge" +> & { + challenge: string; + /** + * The allowed credential ids that are allowed to be used for the payment. Simplified to be only the base64url id + */ + allowedCredentials: string[]; +}; + +export const payWithSPC = async ( + requestOptions: SerialisableCredentialRequestOptions, + payeeOrigin: string, + paymentDetails: PaymentDetailsInit, + address: Address, +) => { + const { challenge, timeout } = requestOptions; + + // - convert the allowed credential ids to a buffer + const credentialIds = requestOptions.allowedCredentials + .map((credentialId) => { + const credential = toBuffer(credentialId); + + // - verify credentialId is reasonable + if (credential.byteLength > 32) return undefined; + + return credential; + }) + .filter(Boolean); + + const paymentMethodData = [ + { + // Specify `secure-payment-confirmation` as payment method. + supportedMethods: "secure-payment-confirmation", + data: { + // The RP ID + rpId: requestOptions.rpId, + + // List of credential IDs obtained from the RP. + credentialIds, + + // The challenge is also obtained from the RP. + challenge: toBuffer(challenge), + + // A display name and an icon that represent the payment instrument. + instrument: { + ...defaultInstrument, + displayName: `Sent From Wallet - ${address}`, + }, + + // The origin of the payee + payeeOrigin, + + // The number of milliseconds to timeout. + timeout, + }, + }, + ]; + + console.log("spc paymentMethodData", paymentMethodData, paymentDetails); + + const request = new PaymentRequest(paymentMethodData, paymentDetails); + + console.log("spc request", JSON.stringify(request)); + + // biome-ignore lint/suspicious/noExplicitAny: + let response: any; + try { + response = await request.show(); + + console.log("spc response", response, response.details); + + // response.details is a PublicKeyCredential, with a clientDataJSON that + // contains the transaction data for verification by the issuing bank. + const cred = response.details; + + // TODO: send the wallet the response for verification before executing the payment + const serialisableCredential = { + id: cred.id, + type: cred.type, + // credential.rawId = base64url.encode(cred.rawId); + response: { + clientDataJSON: fromBuffer(cred.response.clientDataJSON), + authenticatorData: fromBuffer(cred.response.authenticatorData), + signature: fromBuffer(cred.response.signature, "hex"), + userHandle: fromBuffer(cred.response.userHandle), + }, + }; + + await response.complete("success"); + + /* send response.details to the issuing bank for verification */ + return serialisableCredential; + } catch (err) { + await response.complete("fail"); + /* SPC cannot be used; merchant should fallback to traditional flows */ + console.error((err as Error).message); + throw err; + } +}; diff --git a/packages/spc-lib/src/utils/create-message-promise.ts b/packages/spc-lib/src/utils/create-message-promise.ts new file mode 100644 index 0000000..4ece06c --- /dev/null +++ b/packages/spc-lib/src/utils/create-message-promise.ts @@ -0,0 +1,46 @@ +import { getIframe } from "./get-Iframe"; + +/** + * A utility function to wait for a message from the wallet iframe + */ + +export const createMessagePromise = (eventType: string): Promise => { + const iframe = getIframe(); + + if (!iframe) throw new Error("No iframe found"); + + const iframeOrigin = iframe?.src ? new URL(iframe.src).origin : undefined; + + let timeout: number | undefined; + const credentialsPromise: Promise = new Promise( + (resolve, reject) => { + // biome-ignore lint/suspicious/noExplicitAny: + function handleMessage(event: any) { + const eventOrigin = new URL(event.origin).origin; + if (eventOrigin !== iframeOrigin) return; + + console.log("received message", event); + + if (event.data.type === eventType) { + resolve(event.data); + } + // Remove the event listener to clean up + window.removeEventListener("message", handleMessage); + } + + // Add an event listener to listen for messages from the iframe + window.addEventListener("message", handleMessage, false); + + // Set a timeout to reject the promise if no response is received within a specific timeframe + timeout = setTimeout(() => { + window.removeEventListener("message", handleMessage); + reject(new Error("Timeout waiting for credentials response")); + }, 10000); // 10 seconds timeout + }, + ); + + // ! Ensure to clean up on promise resolution or rejection + credentialsPromise.finally(() => clearTimeout(timeout)); + + return credentialsPromise; +}; diff --git a/packages/spc-lib/src/utils/fallback-to-iframe-credential-creation.ts b/packages/spc-lib/src/utils/fallback-to-iframe-credential-creation.ts new file mode 100644 index 0000000..f014c67 --- /dev/null +++ b/packages/spc-lib/src/utils/fallback-to-iframe-credential-creation.ts @@ -0,0 +1,17 @@ +import { WALLET_IFRAME_DIALOG_ID } from "../constants"; + +/** + * A utility function to trigger the wallet iframe to open for credential creation + */ + +export const fallbackToIframeCredentialCreation = async () => { + // - trigger the iframe container to open so the user can create a credential + const iframeDialog = document.getElementById( + WALLET_IFRAME_DIALOG_ID, + ) as HTMLDialogElement | null; + + iframeDialog?.classList.remove("hidden"); + iframeDialog?.classList.add("flex"); + + iframeDialog?.showModal(); +}; diff --git a/packages/spc-lib/src/utils/get-Iframe.ts b/packages/spc-lib/src/utils/get-Iframe.ts new file mode 100644 index 0000000..b89ef9f --- /dev/null +++ b/packages/spc-lib/src/utils/get-Iframe.ts @@ -0,0 +1,17 @@ +import { WALLET_IFRAME_DIALOG_ID } from "../constants"; + +/** + * A utility function to get the iframe element + * + * @returns The wallet iframe + */ + +export const getIframe = () => { + const iframeDialog = document.getElementById( + WALLET_IFRAME_DIALOG_ID, + ) as HTMLIFrameElement | null; + + const iframe = iframeDialog?.querySelector("iframe"); + + return iframe; +}; diff --git a/packages/spc-lib/src/utils/get-available-credentials.ts b/packages/spc-lib/src/utils/get-available-credentials.ts new file mode 100644 index 0000000..c52887e --- /dev/null +++ b/packages/spc-lib/src/utils/get-available-credentials.ts @@ -0,0 +1,29 @@ +import { getAllowedCredentialsSchema } from "../validators"; +import { getIframe } from "./get-Iframe"; +import { createMessagePromise } from "./create-message-promise"; + +/** + * Get the available credentials from the wallet iframe + * + * @param origin The origin of the wallet iframe + * @returns The available credentials + */ + +export const getAvailableCredentials = async (origin: string) => { + const iframe = getIframe(); + + if (!iframe) throw new Error("No iframe found"); + + // - Create a Promise that resolves when the expected message is received from the wallet iframe + const waitForCredentials = async () => { + const eventData = await createMessagePromise("credentials.get"); + const parsedMessage = getAllowedCredentialsSchema.parse(eventData); + + return parsedMessage.credentials; + }; + + // - Ask the iframe for the available credentials + iframe.contentWindow?.postMessage({ type: "credentials.get" }, origin); + + return await waitForCredentials(); +}; diff --git a/packages/spc-lib/src/utils/get-payment-details.ts b/packages/spc-lib/src/utils/get-payment-details.ts new file mode 100644 index 0000000..7a57776 --- /dev/null +++ b/packages/spc-lib/src/utils/get-payment-details.ts @@ -0,0 +1,21 @@ +export const getPaymentDetails = (amount: string) => ({ + // - the SPC spec allows for multiple items to be displayed + // - but currently the Chrome implementation does NOT display ANY items + displayItems: [ + { + label: "NFT", + amount: { currency: "USD", value: "0.0000001" }, + pending: true, + }, + { + label: "Gas Fee", + amount: { currency: "USD", value: "0.0000001" }, + pending: true, + }, + ], + total: { + label: "Total", + // - currency must be a ISO 4217 currency code + amount: { currency: "USD", value: amount }, + }, +}); diff --git a/packages/spc-lib/src/utils/index.ts b/packages/spc-lib/src/utils/index.ts new file mode 100644 index 0000000..5eab28c --- /dev/null +++ b/packages/spc-lib/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from "./create-message-promise"; +export * from "./fallback-to-iframe-credential-creation"; +export * from "./get-available-credentials"; +export * from "./get-Iframe"; +export * from "./get-payment-details"; diff --git a/packages/helpers/validators.ts b/packages/spc-lib/src/validators.ts similarity index 68% rename from packages/helpers/validators.ts rename to packages/spc-lib/src/validators.ts index 605182f..0f9907d 100644 --- a/packages/helpers/validators.ts +++ b/packages/spc-lib/src/validators.ts @@ -6,5 +6,7 @@ export const getAllowedCredentialsMessageTypeSchema = z.enum([ export const getAllowedCredentialsSchema = z.object({ type: getAllowedCredentialsMessageTypeSchema, - credentials: z.array(z.string()), + credentials: z.array( + z.object({ credentialId: z.string(), address: z.string().optional() }), + ), }); diff --git a/packages/spc-lib/tsconfig.json b/packages/spc-lib/tsconfig.json new file mode 100644 index 0000000..1645344 --- /dev/null +++ b/packages/spc-lib/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "declaration": true, + "composite": true, + "sourceMap": true, + "declarationMap": true, + "moduleResolution": "NodeNext", + "module": "NodeNext", + "outDir": "dist", + "strict": true, + "noUncheckedIndexedAccess": true, + "lib": [ + "es2022", + "dom", + "dom.iterable" + ] + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ca19ea..73b62fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + spc-lib: + specifier: workspace:* + version: link:../../packages/spc-lib tailwind-merge: specifier: ^2.2.1 version: 2.2.1 @@ -74,6 +77,9 @@ importers: typescript: specifier: ^5.4.2 version: 5.4.2 + usehooks-ts: + specifier: ^3.0.1 + version: 3.0.1(react@18.2.0) viem: specifier: ^2.8.10 version: 2.8.10(typescript@5.4.2)(zod@3.22.4) @@ -177,6 +183,15 @@ importers: specifier: ^3.22.4 version: 3.22.4 + packages/spc-lib: + dependencies: + helpers: + specifier: workspace:* + version: link:../helpers + zod: + specifier: ^3.22.4 + version: 3.22.4 + packages: /@adraffy/ens-normalize@1.10.0: @@ -5295,6 +5310,10 @@ packages: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} dev: false + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: false + /lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} dev: false @@ -8342,6 +8361,16 @@ packages: resolution: {integrity: sha512-H6dnQ/yPAAVzMQRvEvyz01hhfQL5qRWSEt7BX8t9DqnPw9BjMb64fjIRq76Uvf1hkHp+mTZvEVJ5guXOT0Xqaw==} dev: false + /usehooks-ts@3.0.1(react@18.2.0): + resolution: {integrity: sha512-bgJ8S9w/SnQyACd3RvWp3CGncROxEENGqQLCsdaoyTb0zTENIna7MIV3OW6ywCfPaYYD2OPokw7oLPmSLLWP4w==} + engines: {node: '>=16.15.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + lodash.debounce: 4.0.8 + react: 18.2.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false