From 5f3d3828c5bd54cde52d1e225102c2dec620ba07 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 22 Apr 2024 23:04:45 +0100 Subject: [PATCH] chore: comments, clean-up and loose ends --- app/components/claims-portal.tsx | 64 +- app/page.tsx | 22 +- .../rewards/account-abstraction/sodium.ts | 20 +- .../supabase-browser-client.ts | 29 +- .../rewards/account-abstraction/webauthn.ts | 849 ++++++++++-------- .../read-claim-data-from-url.ts | 12 +- .../render-transaction/render-transaction.ts | 36 +- app/scripts/rewards/web3/erc20-permit.ts | 28 +- .../rewards/web3/verify-current-network.ts | 13 +- package.json | 7 +- tsconfig.json | 1 + yarn.lock | 63 +- 12 files changed, 581 insertions(+), 563 deletions(-) diff --git a/app/components/claims-portal.tsx b/app/components/claims-portal.tsx index 11cdd93d..86b5d02f 100644 --- a/app/components/claims-portal.tsx +++ b/app/components/claims-portal.tsx @@ -8,51 +8,73 @@ import { app } from "../scripts/rewards/app-state"; import { readClaimDataFromUrl } from "../scripts/rewards/render-transaction/read-claim-data-from-url"; import { WebAuthnHandler } from "../scripts/rewards/account-abstraction/webauthn"; import { githubLoginHandler } from "../scripts/rewards/account-abstraction/github-login-button"; -import { toaster } from "../scripts/rewards/toaster"; +import { getButtonController, toaster } from "../scripts/rewards/toaster"; import { User } from "@supabase/supabase-js"; -import { GitHubUser } from "../scripts/rewards/account-abstraction/supabase-server-client"; +import { SupabaseBrowserClient } from "../scripts/rewards/account-abstraction/supabase-browser-client"; +import { renderTransaction } from "../scripts/rewards/render-transaction/render-transaction"; async function readClaimData() { await readClaimDataFromUrl(app); } -export default function ClaimsPortal({ permits, githubUser, supabaseUser }: { permits?: string; githubUser?: GitHubUser; supabaseUser?: User }) { +export default function ClaimsPortal({ permits, supabaseUser }: { permits?: string; supabaseUser?: User | null }) { const webAuthnHandler = new WebAuthnHandler(); + const [isMounted, setMounted] = React.useState(false); + const isLoggedIn = React.useMemo(() => !!supabaseUser, [supabaseUser]); useEffect(() => { async function load() { + if (!isLoggedIn && permits) { + await SupabaseBrowserClient.getInstance().loginWithGitHub(permits); + return; + } await readClaimData(); if (app.claims.length === 0 || !permits) { return; } - if (!githubUser || !supabaseUser) { - console.log("No user data found"); - return; - } + if (supabaseUser && !isMounted) { + // use this to create or authenticate with webauthn + const dataForWebAuthnCredential = { + id: new TextEncoder().encode(supabaseUser.email), + name: supabaseUser.user_metadata.preferred_username, + displayName: supabaseUser.user_metadata.preferred_username, + }; - // use this to create or authenticate with webauthn - const dataForWebAuthnCredential = { - id: new TextEncoder().encode(supabaseUser.email), - name: supabaseUser.user_metadata.preferred_username, - displayName: supabaseUser.user_metadata.preferred_username, - }; + // we'll create an EOA for the user and then attach it to the SMA + if (!window.ethereum) { + app.signer = await webAuthnHandler.handleUserAuthentication(supabaseUser, dataForWebAuthnCredential, app); - // we'll create an EOA for the user and then attach it to the SMA - if (!window.ethereum) { - app.signer = await webAuthnHandler.handleUserAuthentication(supabaseUser, githubUser, dataForWebAuthnCredential, app); - } else { - app.signer = await webAuthnHandler.registerEOA(supabaseUser, githubUser, dataForWebAuthnCredential, app); + if (app.signer.account?.address) { + toaster.create("success", `Successfully authenticated with WebAuthn. Welcome back ${supabaseUser.user_metadata.preferred_username}!`); + } else { + toaster.create("warning", "Failed to authenticate with WebAuthn. Please try again."); + } + } else { + // just saves their EOA to supabase for future use + // webauthn doesn't make sense here unless using it to either + // - create them an EOA like above then we can reproduce the private key to sign txs with + // - embed their current EOA private key in the webauthn credential (not recommended) - console.log("app.signer", app.signer); + app.signer = await webAuthnHandler.registerEOA(supabaseUser, dataForWebAuthnCredential, app); + } } - if (app.signer?.account) { - toaster.create("success", `Successfully authenticated with WebAuthn. Welcome back ${supabaseUser.user_metadata.preferred_username}!`); + const toasterEle = document.getElementsByClassName("toast .fa-circle-check success"); + + await app.signer.getPermissions(); + + const [address] = (await app.signer.getAddresses()) || []; + + if (!toasterEle.length && address) { + toaster.create("success", `Connected to ${address}!`); + await renderTransaction(); + getButtonController().showMakeClaim(); } } load().catch(console.error); + setMounted(true); }, []); return ( diff --git a/app/page.tsx b/app/page.tsx index 12678d20..4c3c6ba4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,29 +1,33 @@ import ClaimsPortal from "./components/claims-portal"; import { SupabaseServerClient } from "./scripts/rewards/account-abstraction/supabase-server-client"; -async function loadUserData() { +async function loadUserData(permits: string) { const supabase = SupabaseServerClient.getInstance(); const { data: { user }, } = await supabase.getSupabaseUser(); - const githubUser = await supabase.getGitHubUser(); + if (!user) { + try { + await supabase.loginWithGitHub(permits); + } catch (error) { + console.error("Failed to login", error); + } + } - return { user, githubUser }; + return { user }; } export default async function Page(params: { searchParams: { claim: string } }) { const permitData = params.searchParams.claim; - const { user, githubUser } = await loadUserData(); + const { user } = await loadUserData(permitData); - if (permitData && user && githubUser) { - return ; - } + return ; /** * good idea to have section for account setup, options, etc. here if we don't have a permit - */ - // return ; + * + */ } diff --git a/app/scripts/rewards/account-abstraction/sodium.ts b/app/scripts/rewards/account-abstraction/sodium.ts index c6c062a6..722e2302 100644 --- a/app/scripts/rewards/account-abstraction/sodium.ts +++ b/app/scripts/rewards/account-abstraction/sodium.ts @@ -1,8 +1,9 @@ +import { LocalAccountSigner } from "@alchemy/aa-core"; import { createHash } from "crypto"; import { wordlists } from "ethers"; import { BytesLike, entropyToMnemonic, isValidMnemonic } from "ethers/lib/utils"; import { crypto_generichash, crypto_generichash_BYTES } from "libsodium-wrappers"; -import { Hex } from "viem"; +import { Hex, keccak256 } from "viem"; export async function generateSAPrivateKey( publicKey: ArrayBuffer | { valueOf(): ArrayBuffer | SharedArrayBuffer } | undefined, @@ -13,7 +14,7 @@ export async function generateSAPrivateKey( /** * Public key is the public key of the credential, good source of entropy * - * credentials ID is unique to the credential and authenticator, good source of entropy + * binaryID is unique to the credential and authenticator, good source of entropy * * userHandle is the email of the user, bad source of entropy * @@ -32,19 +33,18 @@ export async function generateSAPrivateKey( if (!publicKey) throw new Error("No public key created for private key generation"); const salt = process.env.SALT || "ubiquity-rewards"; - const concData = Buffer.concat([ - Buffer.from(publicKey), - Buffer.from(binaryID), - Buffer.from(userHandle), - Buffer.from(supabaseAuthID), - Buffer.from(salt, "hex"), - ]); + const concData = keccak256( + Buffer.concat([Buffer.from(publicKey), Buffer.from(binaryID), Buffer.from(userHandle), Buffer.from(supabaseAuthID), Buffer.from(salt, "hex")]) + ); const hash = crypto_generichash(crypto_generichash_BYTES, concData); const privateKey = ("0x" + createHash("sha256").update(hash).digest().toString("hex")) as Hex; const mnemonic = generateMnemonic(privateKey); - const publicKeyHex = Buffer.from(publicKey).toString("hex"); + + const accSigner = LocalAccountSigner.mnemonicToAccountSigner(mnemonic); + + const publicKeyHex = await accSigner.getAddress(); return { mnemonic, publicKey: publicKeyHex, privateKey }; } diff --git a/app/scripts/rewards/account-abstraction/supabase-browser-client.ts b/app/scripts/rewards/account-abstraction/supabase-browser-client.ts index 1bbfb824..981ce281 100644 --- a/app/scripts/rewards/account-abstraction/supabase-browser-client.ts +++ b/app/scripts/rewards/account-abstraction/supabase-browser-client.ts @@ -51,26 +51,6 @@ export class SupabaseBrowserClient { return await this.supabaseBrowserClient().auth.getUser(); } - async getGitHubUser(permits: string): Promise { - const { - data: { user }, - } = await this.getSupabaseUser(); - - if (!user) { - await this.loginWithGitHub(permits); - } - - let activeSessionToken = await this.getSessionToken(); - - if (!activeSessionToken) { - await this.loginWithGitHub(permits); - } - - activeSessionToken = await this.getSessionToken(); - - return await this.getNewGitHubUser(activeSessionToken); - } - async getSessionToken() { /** * Using supabase.auth.getSession() is potentially insecure as it loads data directly from @@ -78,9 +58,12 @@ export class SupabaseBrowserClient { * Prefer using supabase.auth.getUser() instead. * To suppress this warning call supabase.auth.getUser() before you call supabase.auth.getSession() */ - await this.getSupabaseUser(); - const cachedSessionToken = this.supabaseBrowserClient().auth.getSession(); - return (await cachedSessionToken).data.session?.provider_token ?? null; + const user = this.getSupabaseUser(); + if (user) { + return (await this.supabaseBrowserClient().auth.getSession()).data.session?.provider_token; + } + + return null; } async getNewGitHubUser(providerToken: string | null): Promise { diff --git a/app/scripts/rewards/account-abstraction/webauthn.ts b/app/scripts/rewards/account-abstraction/webauthn.ts index f8b27cd1..63c0b2fa 100644 --- a/app/scripts/rewards/account-abstraction/webauthn.ts +++ b/app/scripts/rewards/account-abstraction/webauthn.ts @@ -8,15 +8,10 @@ import { AppState } from "../app-state"; import { WalletClient, createWalletClient, custom, http } from "viem"; import { createLightAccount, lightAccountClientActions } from "@alchemy/aa-accounts"; import { SupabaseBrowserClient } from "./supabase-browser-client"; -import { GitHubUser } from "./supabase-server-client"; const WEBAUTHN_NOT_SUPPORTED = "WebAuthn is not supported in this browser and cannot be used for account creation"; const FAILED_TO_REGENERATE_ACCOUNT = "Failed to regenerate account"; -function fundingURL(addr: string) { - return `http://127.0.0.1:8787/?address=${addr}`; -} - type NewCredential = { response: { getPublicKey(): ArrayBuffer; @@ -29,7 +24,7 @@ interface UserCredentialsMeta extends PublicKeyCredentialUserEntity { displayName: string; } -const chain = chains.gnosis; +const chain = chains.anvil; export class WebAuthnHandler { private _webauthn: Navigator["credentials"] | undefined; @@ -46,47 +41,78 @@ export class WebAuthnHandler { this._webauthn = navigator.credentials; } - this._supabase = new SupabaseBrowserClient().supabaseBrowserClient(); + this._supabase = new SupabaseBrowserClient(); } - getPermitURLString() { - const urlParams = new URLSearchParams(window.location.search); - return urlParams.get("claim"); - } + private async _threeStepAccountCreation(user: User, userMeta: UserCredentialsMeta) { + let credentials: Credential | undefined | null = await this._authenticate(userMeta); + let publicKey: ArrayBuffer | null = null; - isAllowedToCall() { - const isTimeAllowed = Date.now() - this._lastRequestTime > this._timeout; - const isRequestCountAllowed = this._requestCount < this._requestLimit && isTimeAllowed; - const shouldAuth = !this._isAuthed && !this._isAuthenticating; - return shouldAuth && isRequestCountAllowed; - } + if (!credentials) { + publicKey = await this._handleNoCredentials(userMeta, user); + credentials = await this._authenticate(userMeta); + } - public async registerEOA(user: User, githubUser: GitHubUser, userMeta: UserCredentialsMeta, app: AppState) { - if (this._isAuthenticating || this._isAuthed) { - console.log("Already authenticating or authenticated"); + const account = await this._handleAccountGeneration(user, userMeta, credentials, publicKey); + + if (!account) { + toaster.create("error", FAILED_TO_REGENERATE_ACCOUNT); + throw new Error(FAILED_TO_REGENERATE_ACCOUNT); + } + + return account; + } + /** + * Handles webauthn and account creation for non-web3 browsers + * + * @param user - The Supabase user object. + * @param githubUser - The GitHub user object. + * @param userMeta - The user credentials metadata. + */ + public async handleUserAuthentication(user: User, userMeta: UserCredentialsMeta, app: AppState) { + if (!this.supportsAndCanCall()) { return null; } - const { data, error } = await this._supabase.from("users").select("*").eq("user_id", user.id); - let pubKey: ArrayBuffer | null = null; this._isAuthenticating = true; - if (error) { - console.error("Error fetching public key for user"); - } + const account = await this._threeStepAccountCreation(user, userMeta); - if (data && data.length > 0) { - pubKey = data[0].public_key; - } + const transport = http("https://polygon-rpc.com"); //@ TODO: pull from handler + + const lightAccount = createSmartAccountClient({ + chain, + transport, + account: await createLightAccount({ + chain, + // create a new signer from the private key which is now the owner + // of the smart account + signer: LocalAccountSigner.privateKeyToAccountSigner(account.privateKey), + transport, + }), + }).extend(lightAccountClientActions); + + app.signer = lightAccount; - // obtain their credentials, if they decline they'll be prompted to create new ones - let creds: Credential | undefined | null = await this.authenticate(userMeta); + return app.signer; + } - if (!creds) { - pubKey = await this._handleNoCredentials(userMeta); - creds = await this.authenticate(userMeta); + /** + * Registers an EOA (Externally Owned Account) into the db + * for future permit usage. Not used for smart account creation. + * + * @param user - The Supabase user object. + * @param githubUser - The GitHub user object. + * @param userMeta - The user credentials metadata. + */ + public async registerEOA(user: User, userMeta: UserCredentialsMeta, app: AppState) { + if (this._isAuthenticating) { + console.log("Already authenticating"); + return null; } + const { data } = await this._supabase.supabaseBrowserClient().from("users").select("*").eq("user_id", user.id); + const walletClient = createWalletClient({ chain, transport: custom(window.ethereum), @@ -94,65 +120,68 @@ export class WebAuthnHandler { const [account] = await walletClient.getAddresses(); - if (account && pubKey) { - const { error: userInsertError } = await this._supabase.from("users").insert({ - user_id: githubUser.id, + if (account && !data?.length) { + const { error: newInsertError } = await this._supabase.supabaseBrowserClient().from("users").insert({ + user_id: user.user_metadata.provider_id, + public_key: account, username: userMeta.name, - public_key: pubKey, }); - if (userInsertError) { - console.error("Error inserting user into database: ", userInsertError); + if (newInsertError && newInsertError.code !== "23505") { + console.error("Error inserting user into database: ", newInsertError); } - const { data, error } = await this._supabase.from("users").select("*").eq("user_id", githubUser.id); - const rowID = data?.[0].id; + const { data: newInsertData, error: newInsertDataError } = await this._supabase + .supabaseBrowserClient() + .from("users") + .select("*") + .eq("user_id", user.user_metadata.provider_id); - if (!rowID || error) { - console.error("Error fetching user from database: ", error); + if (newInsertDataError && newInsertDataError.code !== "23505") { + console.error("Error fetching user data: ", newInsertDataError); } - const { error: walletError } = await this._supabase.from("wallets").insert({ - user_id: rowID, + const { error: walletError } = await this._supabase.supabaseBrowserClient().from("wallets").insert({ + user_id: newInsertData?.[0].id, address: account, }); - if (walletError) { + if (walletError && walletError.code !== "23505") { console.error("Error inserting wallet into database: ", walletError); } + const hoistedClient = createWalletClient({ + chain, + transport: custom(window.ethereum), + account, + }); - app.signer = walletClient; - return app.signer; + app.signer = hoistedClient; } - - toaster.create("error", "Failed to connect wallet"); - return app.signer; } - public async registerEOAasSMA(user: User, githubUser: GitHubUser, userMeta: UserCredentialsMeta, app: AppState) { - if (this._isAuthenticating || this._isAuthed) return null; - - const walletClient = app.signer as WalletClient; // we know it's a wallet client if we are in this function - - if (!walletClient || !walletClient.account) { - throw new Error("No signer found"); + /** + * Registers an externally owned account (EOA) as an owner + * of a SMA and tries + * + * @param user - The Supabase user object. + * @param githubUser - The GitHub user object. + * @param userMeta - The user credentials metadata. + */ + public async registerEOAasSMA(user: User, userMeta: UserCredentialsMeta, app: AppState, autoDeploy = false) { + if (!this.supportsAndCanCall()) { + return null; } this._isAuthenticating = true; - // obtain their credentials, if they decline they'll be prompted to create new ones - let creds: Credential | undefined | null = await this.authenticate(userMeta); + const account = await this._threeStepAccountCreation(user, userMeta); - if (!this.isAllowedToCall()) return null; + const walletClient = app.signer as WalletClient; // we know it's a wallet client if we are in this function - if (!creds) { - await this._handleNoCredentials(userMeta); - creds = await this.authenticate(userMeta); + if (!walletClient || !walletClient.account) { + throw new Error("No signer found"); } - - const account = await this._handleAccountGeneration(user, userMeta, creds); - if (!account) { toaster.create("error", FAILED_TO_REGENERATE_ACCOUNT); return null; @@ -168,7 +197,7 @@ export class WebAuthnHandler { // could optionally provide a way to recover this toaster.create("info", `${account.mnemonic}`, 60000 * 3); - if (creds) { + if (autoDeploy) { const { isDeployed, smaAddress } = await this._checkIsDeployed(account.privateKey); const { privateKey } = account; @@ -188,6 +217,10 @@ export class WebAuthnHandler { return app.signer; } + ///////////////////////// + // User Funded Account // + ///////////////////////// + private async _handleUserFundedDeployment(walletClient: WalletClient, smaAddress: string, smartAccountClient: SmartAccountClient) { const gwei90 = BigInt(90000000000); @@ -224,68 +257,28 @@ export class WebAuthnHandler { return lightAccount; } - /** - * @notice Three step process to create a new account: - * 1. try to obtain credentials, if they exist, authenticate, done. - * 2. if they don't exist, create new credentials. - * 3. authenticate the new credentials, set up the account, done. - */ - - public async handleUserAuthentication(user: User, githubUser: GitHubUser, userMeta: UserCredentialsMeta, app: AppState) { - if (this._isAuthenticating || this._isAuthed) { - return null; - } - - if (this.isSupported() && this.isAllowedToCall()) { - this._isAuthenticating = true; - - let creds: Credential | undefined | null = await this.authenticate(userMeta); - - if (!creds) { - const account = await this._handleAccountCreation(userMeta, user, creds); - - if (account) { - creds = account.credentials; - } - } else { - const account = await this._handleAccountGeneration(user, userMeta, creds); - - if (!account) { - toaster.create("error", FAILED_TO_REGENERATE_ACCOUNT); - throw new Error(FAILED_TO_REGENERATE_ACCOUNT); - } - - const { privateKey } = account; - - const transport = http("https://polygon-rpc.com"); //@ TODO: pull from handler + ////////////////////// + // Account Creation // + ////////////////////// - const lightAccount = createSmartAccountClient({ - chain, - transport, - account: await createLightAccount({ - chain, - signer: LocalAccountSigner.privateKeyToAccountSigner(privateKey), - transport, - }), - }).extend(lightAccountClientActions); + private async _handleAccountGeneration( + user: User, + userMeta: UserCredentialsMeta, + creds: Credential | undefined | null, + publicKey: ArrayBuffer | null = null + ) { + const supabaseAuthID = new Uint8Array(new TextEncoder().encode(user?.id as string)); // this is the user's supabase auth id + const userHandle = new Uint8Array(userMeta.id); // this is the user's email + const binaryID = (creds as unknown as { rawId: Uint8Array })?.rawId; // this is the id of the credential - app.signer = lightAccount; - } - } else { - toaster.create("error", WEBAUTHN_NOT_SUPPORTED); + if (publicKey) { + return await this._createPK(publicKey, binaryID, userHandle, supabaseAuthID); } - return app.signer; - } - - private async _handleAccountGeneration(user: User, userMeta: UserCredentialsMeta, creds: Credential | undefined | null) { - const supabaseAuthID = new Uint8Array(new TextEncoder().encode(user?.id as string)); // this is the user's supabase auth id - const userHandle = new Uint8Array(userMeta.id); // this is the user's email - const binaryID = (creds as unknown as { rawId: Uint8Array }).rawId; // this is the id of the credential - const { data, error } = await this._supabase.from("users").select("public_key").eq("user_id", user?.id); + const { data, error } = await this._supabase.supabaseBrowserClient().from("users").select("*").eq("user_id", user.user_metadata.provider_id); if (error || !data.length) { - console.error("Error fetching public key for user"); + console.error("Error fetching public key for user", error); return null; } @@ -295,221 +288,177 @@ export class WebAuthnHandler { const pubKey = data[0].public_key; // this is the raw public key generated during credential creation - // create a wallet from the inputs - return await this._createPK(pubKey, binaryID, userHandle, supabaseAuthID); - } - - private async _handleAccountCreation( - userMeta: UserCredentialsMeta, - user: User, - creds: Credential | undefined | null - ): Promise<{ provider: SmartAccountClient; credentials: Credential } | null> { - let credentials = creds; - - if (!credentials) { - await this._handleNoCredentials(userMeta); - } - - const account = await this._handleAccountGeneration(user, userMeta, credentials); - - if (!account) { - toaster.create("error", FAILED_TO_REGENERATE_ACCOUNT); - return null; - } - - credentials = await this.authenticate(userMeta); - - toaster.create( - "success", - "Successfully created account, your mnemonic is below. Please store it in a safe place, you will not be shown this again.", - 60000 - ); - - // show the mnemonic for the user to write down - // could optionally provide a way to recover this - toaster.create("info", `Store me securely =>: ${account.mnemonic}`, 60000 * 3); + const strippedKey = pubKey.replace("0x", ""); + const pubKeyBuffer = Buffer.from(strippedKey, "hex"); - if (credentials) { - const { isDeployed, smaAddress, provider } = await this._checkIsDeployed(account.privateKey); - const outcome = { - provider, - credentials, - }; + const account = await this._createPK(pubKeyBuffer, binaryID, userHandle, supabaseAuthID); - if (!isDeployed) { - const { isFunded, isNowDeployed } = await this._handleFunding(provider, isDeployed, smaAddress); + const { data: walletData, error: walletError } = await this._supabase.supabaseBrowserClient().from("wallets").select("*").eq("user_id", data[0].id); - if (!isFunded) { - // could be that the tx just hasn't been mined yet - // @TODO: add event listener for the funding transaction? - console.log("Smart account not funded yet."); - } + if (!walletData?.length || walletError) { + const { error: newWalletError } = await this._supabase.supabaseBrowserClient().from("wallets").insert({ + user_id: data[0].id, + address: account.publicKey, + }); - if (isNowDeployed) { - toaster.create("success", `Successfully deployed your smart account. Welcome ${user?.user_metadata.preferred_username}!`); - } + if (newWalletError) { + console.error("Error inserting wallet into database: ", newWalletError); } - - return outcome; } - return null; + return account; } - private async _handleNoCredentials(userMeta: UserCredentialsMeta) { + private async _handleNoCredentials(userMeta: UserCredentialsMeta, supabaseUser: User) { const newCredentials = await this._createNew(userMeta); const publicKey = newCredentials?.getPublicKey(); - if (!publicKey) return null; + if (!publicKey) { + console.error("No public key found"); + return null; + } const strungPublicKey = "0x" + Buffer.from(publicKey).toString("hex"); - const { error } = await this._supabase.from("users").insert({ + const { error } = await this._supabase.supabaseBrowserClient().from("users").upsert({ + user_id: supabaseUser.user_metadata.provider_id, public_key: strungPublicKey, username: userMeta.name, }); if (error) { console.error("Error inserting user into database: ", error); - toaster.create("error", "Failed to create new credentials"); } return publicKey; } - private async _handleCreationUserOp(provider: SmartAccountClient, smaAddress: string) { - const { account } = provider; - if (!account) { - console.error("No account found"); - return null; - } - try { - const { hash } = await provider.sendUserOperation({ - account, - uo: { - target: smaAddress as Hex, - - data: "0x", - value: BigInt(0), - }, - }); + private async _checkIsDeployed(privateKey: Hex) { + // create a signer from the private key + const signer = LocalAccountSigner.privateKeyToAccountSigner(privateKey); + /** + * create a provider which houses the smart account and the new signer + * this will not automatically deploy the smart account + * we'll need to check if it's deployed and if not, deploy it. + * + * We could leave it until they actually need to make a claim + */ - const txHash = await provider.waitForUserOperationTransaction({ hash }); + const provider: SmartAccountClient = await createLightAccountAlchemyClient({ + apiKey: process.env.ALCHEMY_API_KEY, + chain, + signer, + }); - if (!txHash) { - console.error("Failed to deploy smart account"); - return null; - } + const isDeployed = await provider.account?.isAccountDeployed(); + const smaAddress = provider.account?.address; - return txHash; - } catch (err) { - console.error("Error deploying smart account: ", err); + if (isDeployed) { + console.log(`Account is deployed at ${smaAddress}`); + } else { + console.log(`Account to be deployed at ${smaAddress}`); } + + return { + isDeployed: !!isDeployed, + smaAddress: smaAddress ?? "", + provider, + }; } - private async _handleCreationFunding(provider: SmartAccountClient, smaAddress: string) { - try { - const req = await fetch(fundingURL(smaAddress), { - method: "POST", - }); + private async _createPK(pubKey: ArrayBuffer, binaryID: Uint8Array, userHandle: Uint8Array, supabaseAuthID: Uint8Array) { + return await generateSAPrivateKey(pubKey, binaryID, userHandle, supabaseAuthID); + } - const body = await req.json(); - const tx = body.txhash; + ///////////////////////////// + // WebAuthn Authentication // + ///////////////////////////// - const receipt = await provider.waitForTransactionReceipt(tx); + private _createDefaultCredentialsTemplate(crossPlatform?: boolean): CredentialCreationOptions { + const challenge = randomBytes(32); - if (receipt) { - return true; - } - } catch (er) { - console.error("FUNDING ERROR: ", er); - } + /** + * cross-platform offers two options: + * - Iphone, iPad, or Android device + * - Security Key (USB, NFC, or BLE) + * + * platform offers: + * - passkey via pin + * + * we'll go with cross-platform mobile for the demo + */ - return false; - } + let RP_ID; + const host = window.location.origin; - private async _tryDeploy(provider: SmartAccountClient, smaAddress: string) { - const bal = await provider.getBalance({ - address: smaAddress as Hex, - }); + const hostname = new URL(host).hostname; + const NODE_ENV = process.env.NODE_ENV; - if (bal > BigInt(0)) { - try { - const res = await this._handleCreationUserOp(provider, smaAddress); + let isCorrectUrl = false; - if (res) { - return { - isFunded: true, - isNowDeployed: true, - }; - } - } catch (err) { - console.error("Error deploy before checks: ", err); - } + if (NODE_ENV === "development") { + isCorrectUrl = hostname === "localhost"; + } else { + isCorrectUrl = hostname === "ubq.pay.fi"; } - return { - isFunded: false, - isNowDeployed: false, - }; - } + if (isCorrectUrl) { + RP_ID = hostname; + } else { + RP_ID = "localhost"; + } - private async _handleFunding(provider: SmartAccountClient, isDeployed: boolean, smaAddress: string) { - const outcome = { - isFunded: false, - isNowDeployed: false, - }; - - if (smaAddress === "") { - console.error("No smart account address found"); - return outcome; - } - - if (!isDeployed) { - const { isFunded, isNowDeployed } = await this._tryDeploy(provider, smaAddress); - - if (!isFunded) { - await this._handleCreationFunding(provider, smaAddress); - } - - // if we are funded, sending any user op will deploy the smart account - if (isFunded && !isNowDeployed) { - const tx = await this._handleCreationUserOp(provider, smaAddress); - - if (!tx) { - console.error("Smart account not deployed yet. Please try again later."); - } - } - - outcome.isFunded = isFunded; - outcome.isNowDeployed = isNowDeployed; - } + return { + publicKey: { + challenge, + user: { + id: new Uint8Array(0), + name: "", + displayName: "", + }, + authenticatorSelection: { + authenticatorAttachment: crossPlatform ? "cross-platform" : "platform", + requireResidentKey: false, + userVerification: "required", + }, + attestation: "indirect", + excludeCredentials: [], + timeout: 60000, + rp: { + name: "Ubiquity Rewards", + id: RP_ID, + }, - return outcome; + pubKeyCredParams: [ + { + type: "public-key", + alg: -257, // RS256 + }, + { + type: "public-key", + alg: -7, // ES256 + }, + ], + }, + }; } - async authenticate(userMeta: UserCredentialsMeta) { + private async _createNew(userMeta: UserCredentialsMeta): Promise { const template = this._createDefaultCredentialsTemplate(true); - if (!template.publicKey) return; // won't ever happen + if (!template.publicKey) return null; // won't ever happen template.publicKey.user = userMeta; - if (this._requestCount > this._requestLimit) { - toaster.create("error", "Too MFA requests in a short period of time. Please try again in a few minutes."); - return; - } - - let auth: Credential | null | undefined = null; - try { - auth = await this._authenticateCredential({ - mediation: "optional", + const creds = (await this._registerCredential({ publicKey: template.publicKey, signal: new AbortController().signal, - }); + })) as unknown as NewCredential; - if (auth) { + console.log("Creds: ", creds); + if (creds) { this._isAuthed = true; + return creds.response; } } catch (err) { if (typeof err === "object" && err !== null && "name" in err) { @@ -530,30 +479,44 @@ export class WebAuthnHandler { console.error("Unknown error", err); } } + + console.log("Error creating new credentials: ", err); } - this._isAuthenticating = false; - return auth; + return null; } - private _createPK(pubKey: ArrayBuffer, binaryID: Uint8Array, userHandle: Uint8Array, supabaseAuthID: Uint8Array) { - return generateSAPrivateKey(pubKey, binaryID, userHandle, supabaseAuthID); + private async _registerCredential(args: CredentialCreationOptions): Promise { + if (!this._webauthn) { + console.error(WEBAUTHN_NOT_SUPPORTED); + return null; + } + this._requestCount++; + + return await this._webauthn.create(args); } - private async _createNew(userMeta: UserCredentialsMeta): Promise { + private async _authenticate(userMeta: UserCredentialsMeta) { const template = this._createDefaultCredentialsTemplate(true); - if (!template.publicKey) return null; // won't ever happen + if (!template.publicKey) return; // won't ever happen template.publicKey.user = userMeta; + if (this._requestCount > this._requestLimit) { + toaster.create("error", "Too MFA requests in a short period of time. Please try again in a few minutes."); + return; + } + + let auth: Credential | null | undefined = null; + try { - const creds = (await this._registerCredential({ + auth = await this._authenticateCredential({ + mediation: "optional", publicKey: template.publicKey, signal: new AbortController().signal, - })) as unknown as NewCredential; + }); - if (creds) { + if (auth) { this._isAuthed = true; - return creds.response; } } catch (err) { if (typeof err === "object" && err !== null && "name" in err) { @@ -576,133 +539,241 @@ export class WebAuthnHandler { } } - console.error("Failed to create new credentials"); - return null; + this._isAuthenticating = false; + return auth; } - private _createDefaultCredentialsTemplate(crossPlatform?: boolean): CredentialCreationOptions { - const challenge = randomBytes(32); + private async _authenticateCredential(args: CredentialRequestOptions): Promise { + if (!this._webauthn) { + console.error(WEBAUTHN_NOT_SUPPORTED); + return null; + } + this._requestCount++; - /** - * cross-platform offers two options: - * - Iphone, iPad, or Android device - * - Security Key (USB, NFC, or BLE) - * - * platform offers: - * - passkey via pin - * - * we'll go with cross-platform mobile for the demo - */ + return await this._webauthn.get(args); + } - let RP_ID; - const host = window.location.origin; + ///////////// + // Helpers // + ///////////// - const hostname = new URL(host).hostname; - const NODE_ENV = process.env.NODE_ENV; + getPermitURLString() { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get("claim"); + } - let isCorrectUrl = false; + isAllowedToCall() { + const isTimeAllowed = Date.now() - this._lastRequestTime > this._timeout; + const isRequestCountAllowed = this._requestCount < this._requestLimit && isTimeAllowed; + const shouldAuth = !this._isAuthed && !this._isAuthenticating; + return shouldAuth && isRequestCountAllowed; + } - if (NODE_ENV === "development") { - isCorrectUrl = hostname === "localhost"; - } else { - isCorrectUrl = hostname === "ubq.pay.fi"; + isSupported(): boolean { + return !!this._webauthn; + } + + supportsAndCanCall() { + if (this._isAuthenticating || this._isAuthed) { + return false; } - if (isCorrectUrl) { - RP_ID = hostname; - } else { - RP_ID = "localhost"; + if (!this.isSupported()) { + toaster.create("error", WEBAUTHN_NOT_SUPPORTED); + return false; } - return { - publicKey: { - challenge, - user: { - id: new Uint8Array(0), - name: "", - displayName: "", - }, - authenticatorSelection: { - authenticatorAttachment: crossPlatform ? "cross-platform" : "platform", - requireResidentKey: false, - userVerification: "required", - }, - attestation: "indirect", - excludeCredentials: [], - timeout: 60000, - rp: { - name: "Ubiquity Rewards", - id: RP_ID, - }, + if (!this.isAllowedToCall()) { + toaster.create("error", "Too many requests in a short period of time. Please try again in a few minutes."); + return false; + } - pubKeyCredParams: [ - { - type: "public-key", - alg: -257, // RS256 - }, - { - type: "public-key", - alg: -7, // ES256 - }, - ], - }, - }; + return true; } - private async _registerCredential(args: CredentialCreationOptions): Promise { - if (!this._webauthn) { - console.error(WEBAUTHN_NOT_SUPPORTED); + private _fundingURL(addr: string) { + return `http://127.0.0.1:8787/?address=${addr}`; + } + + /** + * //////////////////////////// + * // Funding and Deployment // + * //////////////////////////// + + * Not needed for now and also requires updates to the faucet. + * spec said to introduce a deduction mechanism which we could do + * with SMAs or by bundling the permit into a signed tx that + * ends with the funds in the user's wallet, paying gas, + * which should be possible I'm sure. + + private async _handleAccountCreation( + userMeta: UserCredentialsMeta, + user: User, + creds: Credential | undefined | null, + githubUser: GitHubUser + ): Promise<{ provider: SmartAccountClient; credentials: Credential } | null> { + let credentials = creds; + let publicKey: ArrayBuffer | null = null; + + if (!credentials) { + publicKey = await this._handleNoCredentials(userMeta, user); + } + + credentials = await this._authenticate(userMeta); + + const account = await this._handleAccountGeneration(user, userMeta, credentials, publicKey, githubUser); + + if (!account) { + toaster.create("error", FAILED_TO_REGENERATE_ACCOUNT); return null; } - this._requestCount++; - return await this._webauthn.create(args); + toaster.create( + "success", + "Successfully created account, your mnemonic is below. Please store it in a safe place, you will not be shown this again.", + 60000 + ); + + // show the mnemonic for the user to write down + // could optionally provide a way to recover this + toaster.create("info", `Store me securely =>: ${account.mnemonic}`, 60000 * 3); + + if (credentials) { + const { isDeployed, smaAddress, provider } = await this._checkIsDeployed(account.privateKey); + const outcome = { + provider, + credentials, + }; + + if (!isDeployed) { + const { isFunded, isNowDeployed } = await this._handleFunding(provider, isDeployed, smaAddress); + + if (!isFunded) { + // could be that the tx just hasn't been mined yet + // @TODO: add event listener for the funding transaction? + console.log("Smart account not funded yet."); + } + + if (isNowDeployed) { + toaster.create("success", `Successfully deployed your smart account. Welcome ${user?.user_metadata.preferred_username}!`); + } + } + + return outcome; + } + + return null; } - private async _authenticateCredential(args: CredentialRequestOptions): Promise { - if (!this._webauthn) { - console.error(WEBAUTHN_NOT_SUPPORTED); + private async _handleCreationUserOp(provider: SmartAccountClient, smaAddress: string) { + const { account } = provider; + if (!account) { + console.error("No account found"); return null; } - this._requestCount++; + try { + const { hash } = await provider.sendUserOperation({ + account, + uo: { + target: smaAddress as Hex, - return await this._webauthn.get(args); + data: "0x", + value: BigInt(0), + }, + }); + + const txHash = await provider.waitForUserOperationTransaction({ hash }); + + if (!txHash) { + console.error("Failed to deploy smart account"); + return null; + } + + return txHash; + } catch (err) { + console.error("Error deploying smart account: ", err); + } } - private async _checkIsDeployed(privateKey: Hex) { - // create a signer from the private key - const signer = LocalAccountSigner.privateKeyToAccountSigner(privateKey); - /** - * create a provider which houses the smart account and the new signer - * this will not automatically deploy the smart account - * we'll need to check if it's deployed and if not, deploy it. - * - * We could leave it until they actually need to make a claim - */ + private async _handleCreationFunding(provider: SmartAccountClient, smaAddress: string) { + try { + const req = await fetch(fundingURL(smaAddress), { + method: "POST", + }); - const provider: SmartAccountClient = await createLightAccountAlchemyClient({ - apiKey: process.env.ALCHEMY_API_KEY, - chain, - signer, + const body = await req.json(); + const tx = body.txhash; + + const receipt = await provider.waitForTransactionReceipt(tx); + + if (receipt) { + return true; + } + } catch (er) { + console.error("FUNDING ERROR: ", er); + } + + return false; + } + + private async _tryDeploy(provider: SmartAccountClient, smaAddress: string) { + const bal = await provider.getBalance({ + address: smaAddress as Hex, }); - const isDeployed = await provider.account?.isAccountDeployed(); - const smaAddress = provider.account?.address; + if (bal > BigInt(0)) { + try { + const res = await this._handleCreationUserOp(provider, smaAddress); - if (isDeployed) { - console.log(`Account is deployed at ${smaAddress}`); - } else { - console.log(`Account to be deployed at ${smaAddress}`); + if (res) { + return { + isFunded: true, + isNowDeployed: true, + }; + } + } catch (err) { + console.error("Error deploy before checks: ", err); + } } return { - isDeployed: !!isDeployed, - smaAddress: smaAddress ?? "", - provider, + isFunded: false, + isNowDeployed: false, }; } - isSupported(): boolean { - return !!this._webauthn; + private async _handleFunding(provider: SmartAccountClient, isDeployed: boolean, smaAddress: string) { + const outcome = { + isFunded: false, + isNowDeployed: false, + }; + + if (smaAddress === "") { + console.error("No smart account address found"); + return outcome; + } + + if (!isDeployed) { + const { isFunded, isNowDeployed } = await this._tryDeploy(provider, smaAddress); + + if (!isFunded) { + await this._handleCreationFunding(provider, smaAddress); + } + + // if we are funded, sending any user op will deploy the smart account + if (isFunded && !isNowDeployed) { + const tx = await this._handleCreationUserOp(provider, smaAddress); + + if (!tx) { + console.error("Smart account not deployed yet. Please try again later."); + } + } + + outcome.isFunded = isFunded; + outcome.isNowDeployed = isNowDeployed; + } + + return outcome; } + */ } diff --git a/app/scripts/rewards/render-transaction/read-claim-data-from-url.ts b/app/scripts/rewards/render-transaction/read-claim-data-from-url.ts index d7f33e33..1c4d1b67 100644 --- a/app/scripts/rewards/render-transaction/read-claim-data-from-url.ts +++ b/app/scripts/rewards/render-transaction/read-claim-data-from-url.ts @@ -3,10 +3,9 @@ import { Value } from "@sinclair/typebox/value"; import { AppState, app } from "../app-state"; import { useFastestRpc } from "../rpc-optimization/get-optimal-provider"; import { toaster } from "../toaster"; -import { connectWallet } from "../web3/connect-wallet"; import { checkRenderInvalidatePermitAdminControl, checkRenderMakeClaimControl } from "../web3/erc20-permit"; import { claimRewardsPagination } from "./claim-rewards-pagination"; -import { renderTransaction } from "./render-transaction"; +import { renderTxDetails } from "./render-transaction"; import { setClaimMessage } from "./set-claim-message"; import { RewardPermit, claimTxT } from "./tx-type"; import { createClient } from "../supabase/client"; @@ -34,12 +33,6 @@ export async function readClaimDataFromUrl(app: AppState) { toaster.create("error", `${e}`); } - try { - app.signer = await connectWallet(app); - } catch (error) { - /* empty */ - } - try { // this would throw on mobile browsers & non-web3 browsers window?.ethereum.on("accountsChanged", () => { @@ -55,8 +48,7 @@ export async function readClaimDataFromUrl(app: AppState) { displayRewardDetails(); displayRewardPagination(); - - await renderTransaction(); + await renderTxDetails(app, table); } async function getClaimedTxs(app: AppState): Promise> { diff --git a/app/scripts/rewards/render-transaction/render-transaction.ts b/app/scripts/rewards/render-transaction/render-transaction.ts index 5e8755c3..937429df 100644 --- a/app/scripts/rewards/render-transaction/render-transaction.ts +++ b/app/scripts/rewards/render-transaction/render-transaction.ts @@ -1,6 +1,6 @@ "use client"; -import { app } from "../app-state"; +import { AppState, app } from "../app-state"; import { networkExplorers } from "../constants"; import { getButtonController, getMakeClaimButton } from "../toaster"; import { checkRenderInvalidatePermitAdminControl, fetchTreasury } from "../web3/erc20-permit"; @@ -13,6 +13,24 @@ import { Erc20Permit, RewardPermit } from "./tx-type"; type Success = boolean; +export async function renderTxDetails(app: AppState, table: HTMLTableElement): Promise { + const treasury = await fetchTreasury(app.reward); + // insert tx data into table + const requestedAmountElement = insertErc20PermitTableData(app, table, treasury); + + renderTokenSymbol({ + tokenAddress: app.reward.permit.permitted.token, + ownerAddress: app.reward.owner, + amount: app.reward.transferDetails.requestedAmount, + explorerUrl: networkExplorers[app.reward.networkId], + table, + requestedAmountElement, + }).catch(console.error); + + const toElement = document.getElementById(`rewardRecipient`) as Element; + renderEnsName({ element: toElement, address: app.reward.transferDetails.to }).catch(console.error); +} + export async function renderTransaction(): Promise { const carousel = document.getElementById("carousel") as Element; const table = document.querySelector(`table`) as HTMLTableElement; @@ -32,22 +50,6 @@ export async function renderTransaction(): Promise { verifyCurrentNetwork(app).catch(console.error); if (permitCheck(app.reward)) { - const treasury = await fetchTreasury(app.reward); - // insert tx data into table - const requestedAmountElement = insertErc20PermitTableData(app, table, treasury); - - renderTokenSymbol({ - tokenAddress: app.reward.permit.permitted.token, - ownerAddress: app.reward.owner, - amount: app.reward.transferDetails.requestedAmount, - explorerUrl: networkExplorers[app.reward.networkId], - table, - requestedAmountElement, - }).catch(console.error); - - const toElement = document.getElementById(`rewardRecipient`) as Element; - renderEnsName({ element: toElement, address: app.reward.transferDetails.to }).catch(console.error); - if (app.provider) { checkRenderInvalidatePermitAdminControl(app).catch(console.error); } diff --git a/app/scripts/rewards/web3/erc20-permit.ts b/app/scripts/rewards/web3/erc20-permit.ts index 520f52e0..80672426 100644 --- a/app/scripts/rewards/web3/erc20-permit.ts +++ b/app/scripts/rewards/web3/erc20-permit.ts @@ -74,10 +74,13 @@ async function transferFromPermit(app: AppState) { const permit2Contract = getContract({ abi: permit2Abi, address: permit2Address, - client: { wallet: signer }, + client: signer, }); - if (!permit2Contract) return; + if (!permit2Contract) { + toaster.create("error", `Contract not found`); + return null; + } try { const tx = await permit2Contract.write.permitTransferFrom([reward.permit, reward.transferDetails, reward.owner, reward.signature]); @@ -190,9 +193,8 @@ async function checkPermitClaimable(app: AppState): Promise { let user: string | undefined; try { - const address = app.signer?.account?.address; - console.log("addresses: ", address); - user = address?.[0].toLowerCase(); + const [address] = await app.signer.getAddresses(); + user = address.toLowerCase(); } catch (error: unknown) { console.error("Error in signer.getAddress: ", error); return false; @@ -200,14 +202,18 @@ async function checkPermitClaimable(app: AppState): Promise { const beneficiary = reward.transferDetails.to.toLowerCase(); - console.log("Beneficiary: ", beneficiary); - console.log("User: ", user); + if (beneficiary !== user) { + toaster.create("error", `This reward is not meant for you.`); + getButtonController().hideMakeClaim(); + return false; + } + return true; } export async function checkRenderMakeClaimControl(app: AppState) { try { - const address = app.signer?.account?.address; + const [address] = await app.signer.getAddresses(); const user = address?.toLowerCase(); if (app.reward) { @@ -226,7 +232,7 @@ export async function checkRenderMakeClaimControl(app: AppState) { export async function checkRenderInvalidatePermitAdminControl(app: AppState) { try { - const address = app.signer?.account?.address; + const [address] = await app.signer.getAddresses(); const user = address?.toLowerCase(); if (app.reward) { @@ -297,10 +303,12 @@ async function invalidateNonce(signer: WalletClient | SmartAccountClient | null, client: signer, }); + const [address] = await signer.getAddresses(); + const { wordPos, bitPos } = nonceBitmap(nonce); // mimics https://github.com/ubiquity/pay.ubq.fi/blob/c9e7ed90718fe977fd9f348db27adf31d91d07fb/scripts/solidity/test/Permit2.t.sol#L428 const bit = BigNumber.from(1).shl(bitPos); - const sourceBitmap = (await permit2Contract.read.nonceBitmap([signer?.account?.address, wordPos.toString()])) as BigNumber; + const sourceBitmap = (await permit2Contract.read.nonceBitmap([address, wordPos.toString()])) as BigNumber; const mask = sourceBitmap.xor(bit); await permit2Contract.write.invalidateUnorderedNonces([wordPos, mask]); } diff --git a/app/scripts/rewards/web3/verify-current-network.ts b/app/scripts/rewards/web3/verify-current-network.ts index 4c2aab6a..2e7c009b 100644 --- a/app/scripts/rewards/web3/verify-current-network.ts +++ b/app/scripts/rewards/web3/verify-current-network.ts @@ -2,24 +2,15 @@ import { chains } from "@alchemy/aa-core"; import { AppState } from "../app-state"; -import { getButtonController } from "../toaster"; import { handleIfOnCorrectNetwork } from "./handle-if-on-correct-network"; import { notOnCorrectNetwork } from "./not-on-correct-network"; import { CustomTransport, createWalletClient, custom } from "viem"; // verifyCurrentNetwork checks if the user is on the correct network and displays an error if not export async function verifyCurrentNetwork(app: AppState) { - if (!window.ethereum) { - getButtonController().hideAll(); - return; - } + const [account] = await app.signer.getAddresses(); - const [account] = await window.ethereum.request({ method: "eth_requestAccounts" }); - - const transport: CustomTransport = custom({ - ...window.ethereum, - request: window.ethereum.request.bind(window.ethereum), - }); + const transport: CustomTransport = custom(window.ethereum); const web3provider = createWalletClient({ chain: app.networkId === 31337 ? chains.anvil : app.networkId === 1 ? chains.mainnet : chains.gnosis, diff --git a/package.json b/package.json index 85529815..931e28da 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "build": " next build", "start": "next start", "lint": "next lint", - "anvil": "npx tsx scripts/typescript/anvil.ts" + "anvil": "npx tsx scripts/typescript/anvil.ts", + "test:open": "cypress open", + "test:run": "cypress run" }, "keywords": [ "typescript", @@ -83,7 +85,6 @@ "husky": "^9.0.11", "knip": "^5.0.1", "lint-staged": "^15.2.2", - "nodemon": "^3.0.3", "npm-run-all": "^4.1.5", "postcss": "^8.4.38", "prettier": "^3.2.5", @@ -106,4 +107,4 @@ "@commitlint/config-conventional" ] } -} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 9f663ddc..d8d4c435 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "isolatedModules": true, "jsx": "preserve", "incremental": true, + "types": ["cypress"], "downlevelIteration": true, "plugins": [ { diff --git a/yarn.lock b/yarn.lock index 82730e35..6322f9af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5706,11 +5706,6 @@ JSONStream@^1.3.5: jsonparse "^1.2.0" through ">=2.2.7 <3" -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - abitype@0.8.7: version "0.8.7" resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.8.7.tgz#e4b3f051febd08111f486c0cc6a98fa72d033622" @@ -6596,7 +6591,7 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@^3.5.2, chokidar@^3.5.3, chokidar@^3.6.0: +chokidar@^3.5.3, chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -7251,7 +7246,7 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" -debug@4, debug@4.3.4, debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@4.3.4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -9103,11 +9098,6 @@ ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" - integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== - ignore@^5.1.8, ignore@^5.2.0, ignore@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" @@ -10900,29 +10890,6 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== -nodemon@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.0.tgz#ff7394f2450eb6a5e96fe4180acd5176b29799c9" - integrity sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA== - dependencies: - chokidar "^3.5.2" - debug "^4" - ignore-by-default "^1.0.1" - minimatch "^3.1.2" - pstree.remy "^1.1.8" - semver "^7.5.3" - simple-update-notifier "^2.0.0" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.5" - -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== - dependencies: - abbrev "1" - normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -11699,11 +11666,6 @@ proxy-from-env@^1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -pstree.remy@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - public-encrypt@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" @@ -12492,13 +12454,6 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -simple-update-notifier@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" - integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== - dependencies: - semver "^7.5.3" - siwe-recap@0.0.2-alpha.0: version "0.0.2-alpha.0" resolved "https://registry.yarnpkg.com/siwe-recap/-/siwe-recap-0.0.2-alpha.0.tgz#75a0902c10a8ba5b4471f40e4eafb0afb2f8db59" @@ -12928,7 +12883,7 @@ superstruct@^1.0.3: resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-1.0.4.tgz#0adb99a7578bd2f1c526220da6571b2d485d91ca" integrity sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ== -supports-color@^5.3.0, supports-color@^5.5.0: +supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -13096,13 +13051,6 @@ toggle-selection@^1.0.6: resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== -touch@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" - integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== - dependencies: - nopt "~1.0.10" - tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -13324,11 +13272,6 @@ uncrypto@^0.1.3: resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== -undefsafe@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" - integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== - underscore@1.12.1: version "1.12.1" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e"