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"