diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1e04f5b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +node_modules/ +.next/ +out/ +build/ +dist/ +next-env.d.ts + diff --git a/.eslintrc.json b/.eslintrc.json index cb300c2..6ae79cd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,6 +8,7 @@ "plugins": ["react", "prettier", "react-hooks"], "rules": { "react/react-in-jsx-scope": "off", + "react/no-unknown-property": ["error", { "ignore": ["sx"] }], "no-console": "warn" }, "settings": { diff --git a/.github/workflows/build-bundle.yml b/.github/workflows/build-bundle.yml index f3a90c8..59d9aef 100644 --- a/.github/workflows/build-bundle.yml +++ b/.github/workflows/build-bundle.yml @@ -19,7 +19,7 @@ jobs: - name: Build Bundle uses: actions/setup-node@v3 with: - node-version: '16' + node-version: '20' cache: 'npm' - run: npm install diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32187e5..6d21672 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [20.x] steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index f96d6e5..72a79ff 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,10 @@ yarn-error.log* .vercel .idea -/.env \ No newline at end of file +/.env + +# private keys +*.pkey + +# TypeScript incremental build cache +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/README.md b/README.md index 595fa9b..2c724a2 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,15 @@ app to interact with the dev-wallet during development: Navigate to http://localhost:8701/harness +### Fork Mode + +When the dev wallet detects it's connected to a mainnet or testnet access node (instead of the local emulator), it automatically enters "fork mode". In fork mode: + +- Transaction signature validation is assumed to be disabled on the network +- Users can authenticate with any existing account address on the network +- This is useful for testing against forked mainnet/testnet environments where signatures are not validated + +Learn more about [forking mainnet/testnet with the Flow Emulator](https://developers.flow.com/build/tools/emulator). 🚀 ## Contributing diff --git a/cadence/contracts/FCL.cdc b/cadence/contracts/FCL.cdc index a8fe8b2..570e083 100644 --- a/cadence/contracts/FCL.cdc +++ b/cadence/contracts/FCL.cdc @@ -85,6 +85,11 @@ access(all) contract FCL { return acct } + access(all) fun add(address: Address, label: String, scopes: [String]) { + let acct = FCLAccount(address: address, label: label, scopes: scopes) + self.account.storage.borrow<&Root>(from: self.storagePath)!.add(acct) + } + access(all) fun update(address: Address, label: String, scopes: [String]) { self.account.storage.borrow<&Root>(from: self.storagePath)! .update(address: address, label: label, scopes: scopes) diff --git a/cadence/transactions/addAccount.cdc b/cadence/transactions/addAccount.cdc new file mode 100644 index 0000000..b58a63b --- /dev/null +++ b/cadence/transactions/addAccount.cdc @@ -0,0 +1,8 @@ +import "FCL" + +transaction(address: Address, label: String, scopes: [String]) { + prepare(acct: &Account) { + FCL.add(address: address, label: label, scopes: scopes) + } +} + diff --git a/components/AccountBalances.tsx b/components/AccountBalances.tsx index 84378ad..3e7c7f6 100644 --- a/components/AccountBalances.tsx +++ b/components/AccountBalances.tsx @@ -5,7 +5,8 @@ import {fundAccount} from "src/accounts" import {formattedBalance} from "src/balance" import {FLOW_TYPE, TokenTypes} from "src/constants" import useConfig from "hooks/useConfig" -import {Label, Themed} from "theme-ui" +import {Label} from "theme-ui" +import {Themed} from "@theme-ui/mdx" import {SXStyles} from "types" import AccountSectionHeading from "./AccountSectionHeading" import Button from "./Button" diff --git a/components/AccountForm.tsx b/components/AccountForm.tsx index e32dbc5..373b3e4 100644 --- a/components/AccountForm.tsx +++ b/components/AccountForm.tsx @@ -79,7 +79,13 @@ export default function AccountForm({ } }} > - {({values, setFieldValue}) => ( + {({ + values, + setFieldValue, + }: { + values: {label: string | undefined; scopes: Set} + setFieldValue: (field: string, value: any) => void + }) => ( <>
diff --git a/components/AccountListItemScopes.tsx b/components/AccountListItemScopes.tsx index e8f6a6c..8425e07 100644 --- a/components/AccountListItemScopes.tsx +++ b/components/AccountListItemScopes.tsx @@ -1,7 +1,8 @@ /** @jsxImportSource theme-ui */ import Switch from "components/Switch" import useAuthnContext from "hooks/useAuthnContext" -import {Label, Themed} from "theme-ui" +import {Label} from "theme-ui" +import {Themed} from "@theme-ui/mdx" import {SXStyles} from "types" import AccountSectionHeading from "./AccountSectionHeading" diff --git a/components/AccountsList.tsx b/components/AccountsList.tsx index 62d0311..22b414b 100644 --- a/components/AccountsList.tsx +++ b/components/AccountsList.tsx @@ -5,9 +5,11 @@ import PlusButton from "components/PlusButton" import useAuthnContext from "hooks/useAuthnContext" import {Account, NewAccount} from "src/accounts" import accountGenerator from "src/accountGenerator" -import {Box, Themed} from "theme-ui" +import {Box} from "theme-ui" +import {Themed} from "@theme-ui/mdx" import {SXStyles} from "types" import FormErrors from "./FormErrors" +import useConfig from "hooks/useConfig" const styles: SXStyles = { accountCreated: { @@ -19,9 +21,15 @@ const styles: SXStyles = { mb: 3, }, plusButtonContainer: { - height: 90, display: "flex", alignItems: "center", + gap: 3, + }, + plusButtonContainerColumn: { + flexDirection: "column", + alignItems: "stretch", + gap: 2, + mt: 3, }, footer: { lineHeight: 1.7, @@ -33,6 +41,7 @@ const styles: SXStyles = { export default function AccountsList({ accounts, onEditAccount, + onUseAnyAccount, createdAccountAddress, flowAccountAddress, flowAccountPrivateKey, @@ -40,12 +49,14 @@ export default function AccountsList({ }: { accounts: Account[] onEditAccount: (account: Account | NewAccount) => void + onUseAnyAccount: () => void createdAccountAddress: string | null flowAccountAddress: string flowAccountPrivateKey: string avatarUrl: string }) { const {initError} = useAuthnContext() + const {forkMode} = useConfig() return (
@@ -78,7 +89,21 @@ export default function AccountsList({ /> ))} - + + {forkMode && ( + + Use Existing Address + + )} onEditAccount(accountGenerator(accounts.length - 1)) diff --git a/components/AccountsListItem.tsx b/components/AccountsListItem.tsx index 1975693..0c0cbf1 100644 --- a/components/AccountsListItem.tsx +++ b/components/AccountsListItem.tsx @@ -7,7 +7,8 @@ import {Account, NewAccount} from "src/accounts" import {useEffect, useState} from "react" import {chooseAccount} from "src/accountAuth" import {formattedBalance} from "src/balance" -import {Flex, Themed} from "theme-ui" +import {Flex} from "theme-ui" +import {Themed} from "@theme-ui/mdx" import {SXStyles} from "types" import {getBaseUrl} from "src/utils" diff --git a/components/AnyAccountForm.tsx b/components/AnyAccountForm.tsx new file mode 100644 index 0000000..6fdea13 --- /dev/null +++ b/components/AnyAccountForm.tsx @@ -0,0 +1,191 @@ +/** @jsxImportSource theme-ui */ +import ConnectedAppHeader from "components/ConnectedAppHeader" +import {styles as dialogStyles} from "components/Dialog" +import {Field, Form, Formik} from "formik" +import {useState} from "react" +import {Account, addExistingAccount} from "src/accounts" +import {getBaseUrl} from "src/utils" +import {Box} from "theme-ui" +import {SXStyles} from "types" +import useAuthnContext from "hooks/useAuthnContext" +import useConfig from "hooks/useConfig" +import Button from "./Button" +import {CustomInputComponent} from "./Inputs" +import {chooseAccount} from "src/accountAuth" + +const styles: SXStyles = { + form: { + position: "relative", + }, + backButton: { + background: "none", + border: 0, + position: "absolute", + top: 0, + left: 0, + cursor: "pointer", + p: 0, + zIndex: 10, + }, + actionsContainer: { + display: "flex", + alignItems: "center", + width: "100%", + borderTop: "1px solid", + borderColor: "gray.200", + backgroundColor: "white", + borderBottomLeftRadius: 10, + borderBottomRightRadius: 10, + px: [10, 20], + }, + actions: { + display: "flex", + flex: 1, + pt: 20, + pb: 20, + }, +} + +type FormValues = { + address: string + label: string +} + +export default function AnyAccountForm({ + onCancel, + flowAccountAddress, + flowAccountPrivateKey, + avatarUrl, +}: { + onCancel: () => void + flowAccountAddress: string + flowAccountPrivateKey: string + avatarUrl: string +}) { + const baseUrl = getBaseUrl() + const config = useConfig() + const {connectedAppConfig, appScopes} = useAuthnContext() + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) + + return ( + + initialValues={{ + address: "", + label: "", + }} + validate={values => { + const errors: Record = {} + if (!values.address) errors.address = "Address is required" + return errors + }} + onSubmit={async values => { + setError(null) + setSubmitting(true) + try { + // First, add the account to FCL storage + await addExistingAccount( + config, + values.address, + values.label || values.address, + appScopes as [string] + ) + + const account: Account = { + type: "ACCOUNT", + address: values.address, + keyId: 0, + label: values.label || values.address, + scopes: appScopes, + } + + // Then authenticate with it + await chooseAccount( + baseUrl, + flowAccountPrivateKey, + account, + new Set(appScopes), + connectedAppConfig + ) + } catch (e: unknown) { + setError(String(e)) + setSubmitting(false) + } + }} + > + {({submitForm, errors: formErrors}) => ( + <> +
+ + + + + + + + + + + + + + + + {error &&
{error}
} + {Object.values(formErrors).length > 0 && ( +
+ {Object.values(formErrors).join(". ")} +
+ )} + +
+
+
+
+ + +
+
+
+ + )} + + ) +} diff --git a/components/AuthzDetailsTable.tsx b/components/AuthzDetailsTable.tsx index 63b3ebc..3345c01 100644 --- a/components/AuthzDetailsTable.tsx +++ b/components/AuthzDetailsTable.tsx @@ -60,9 +60,10 @@ const styles: SXStyles = { }, } -export function AuthzDetailsAccount({account}: {account: Account}) { +export function AuthzDetailsAccount({account}: {account: Account | null}) { const {currentUser} = useAuthzContext() - const isCurrent = account.address === currentUser.address + if (!account) return null + const isCurrent = currentUser && account.address === currentUser.address return (
- {account.label} + {account.label || account.address}
{account.address}
diff --git a/components/AuthzHeader.tsx b/components/AuthzHeader.tsx index 2a3bbfb..8c6a4c1 100644 --- a/components/AuthzHeader.tsx +++ b/components/AuthzHeader.tsx @@ -60,17 +60,23 @@ function AuthzHeader({
- -