diff --git a/package.json b/package.json
index 70bd1da3..b2be9d15 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
"@stacks/network": "2.0.0-beta.0",
"@stacks/rpc-client": "1.0.3",
"@stacks/transactions": "2.0.0-beta.1",
- "@stacks/ui": "7.8.0",
+ "@stacks/ui": "file:.yalc/@stacks/ui",
"@stacks/ui-core": "7.3.0",
"@stacks/ui-theme": "7.5.0",
"@stacks/ui-utils": "7.5.0",
@@ -64,6 +64,7 @@
"bignumber.js": "9.0.1",
"bn.js": "5.2.0",
"body-parser": "1.19.0",
+ "boring-avatars": "^1.5.8",
"buffer": "6.0.3",
"capsize": "2.0.0",
"chroma-js": "2.1.2",
@@ -72,17 +73,23 @@
"dayjs": "1.10.5",
"dompurify": "2.2.9",
"downshift": "6.1.3",
+ "fast-deep-equal": "^3.1.3",
"formik": "2.2.9",
"history": "5.0.0",
"http-server": "0.12.3",
+ "jotai": "^1.0.0",
"jsontokens": "3.0.0",
"mdi-react": "7.5.0",
+ "micro-memoize": "^4.0.9",
+ "object-hash": "^2.2.0",
+ "optics-ts": "^2.1.0",
"preact": "10.5.13",
"prismjs": "1.23.0",
- "react": "17.0.2",
- "react-dom": "17.0.2",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2",
"react-hot-toast": "2.0.0",
"react-icons": "4.2.0",
+ "react-query": "^3.17.0",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"recoil": "0.3.1",
@@ -99,8 +106,6 @@
},
"devDependencies": {
"@actions/core": "1.3.0",
- "@changesets/changelog-github": "0.4.0",
- "@changesets/cli": "2.16.0",
"@babel/core": "7.14.3",
"@babel/plugin-proposal-class-properties": "7.13.0",
"@babel/plugin-transform-regenerator": "7.13.15",
@@ -111,6 +116,9 @@
"@babel/runtime": "7.14.0",
"@blockstack/stacks-blockchain-api-types": "0.55.3",
"@blockstack/stacks-blockchain-sidecar-types": "0.0.22",
+ "@changesets/changelog-github": "0.4.0",
+ "@changesets/cli": "2.16.0",
+ "@emotion/babel-plugin": "^11.3.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.0-beta.8",
"@schemastore/web-manifest": "0.0.5",
"@stacks/prettier-config": "0.0.8",
@@ -125,6 +133,7 @@
"@types/jest": "26.0.23",
"@types/just-debounce-it": "1.5.0",
"@types/node": "15.12.1",
+ "@types/object-hash": "^2.1.0",
"@types/prismjs": "1.16.5",
"@types/qrcode.react": "1.0.1",
"@types/react": "17.0.9",
@@ -148,8 +157,8 @@
"copy-webpack-plugin": "8.1.1",
"cross-env": "7.0.3",
"crypto-browserify": "3.12.0",
- "download": "8.0.0",
"deepmerge": "4.2.2",
+ "download": "8.0.0",
"esbuild-loader": "2.13.1",
"eslint": "7.28.0",
"eslint-plugin-jest": "24.3.6",
@@ -190,7 +199,7 @@
"resolutions": {
"**/**/dns-packet": "5.2.2",
"**/**/browserslist": "4.16.5",
- "@stacks/ui": "7.8.0",
+ "@stacks/ui": "file:.yalc/@stacks/ui",
"@stacks/ui-core": "7.3.0",
"@stacks/ui-theme": "7.5.0",
"@stacks/ui-utils": "7.5.0",
diff --git a/public/html/full-page.html b/public/html/full-page.html
index 9c189bf8..6ca28546 100644
--- a/public/html/full-page.html
+++ b/public/html/full-page.html
@@ -1,11 +1,28 @@
-
+
<%= htmlWebpackPlugin.options.title %>
+
diff --git a/src/app.tsx b/src/app.tsx
index 3c064319..72ad1dfb 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,40 +1,38 @@
import React, { useEffect } from 'react';
import { ThemeProvider, ColorModeProvider } from '@stacks/ui';
import { Toaster } from 'react-hot-toast';
-import { RecoilRoot } from 'recoil';
-import packageJson from '../package.json';
import { theme } from '@common/theme';
import { HashRouter as Router } from 'react-router-dom';
import { GlobalStyles } from '@components/global-styles';
import { VaultLoader } from '@components/vault-loader';
-import { AccountsDrawer } from './components/drawer/accounts';
-import { NetworksDrawer } from './components/drawer/networks-drawer';
-import { DebugObserver } from '@components/debug-observer';
+import { AccountsDrawer } from '@components/drawer/accounts';
+import { NetworksDrawer } from '@components/drawer/networks-drawer';
import { Routes } from './routes';
+import { Devtools } from '@components/devtools';
export const App: React.FC = () => {
useEffect(() => {
- (window as any).__APP_VERSION__ = packageJson.version;
+ (window as any).__APP_VERSION__ = VERSION;
}, []);
return (
-
-
-
+ <>
+
+
<>
+
-
>
-
-
+
+ >
);
};
diff --git a/src/common/atom-utils.ts b/src/common/atom-utils.ts
new file mode 100644
index 00000000..beeaf88e
--- /dev/null
+++ b/src/common/atom-utils.ts
@@ -0,0 +1,10 @@
+import { Atom } from 'jotai';
+import { ContractPrincipal } from '@store/assets/types';
+
+export function debugLabelWithContractPrincipal(
+ atom: Atom,
+ key: string,
+ contractPrincipal: ContractPrincipal
+) {
+ atom.debugLabel = `${key}/${contractPrincipal.contractAddress}.${contractPrincipal.contractName}`;
+}
diff --git a/src/common/atom-with-params.ts b/src/common/atom-with-params.ts
new file mode 100644
index 00000000..9bf2ec0b
--- /dev/null
+++ b/src/common/atom-with-params.ts
@@ -0,0 +1,38 @@
+import { atom, PrimitiveAtom } from 'jotai';
+
+const event = 'popstate';
+
+type ParamType = string | null;
+
+export function atomWithParam(key: string, initialValue: ParamType): PrimitiveAtom {
+ const anAtom: PrimitiveAtom = atom(
+ initialValue,
+ (get, set, update: ParamType | ((prev: ParamType) => ParamType)) => {
+ const newValue =
+ typeof update === 'function'
+ ? (update as (prev: ParamType) => ParamType)(get(anAtom))
+ : update;
+ set(anAtom, newValue);
+ const searchParams = new URLSearchParams(location.hash.slice(2));
+ if (newValue) searchParams.set(key, newValue);
+ if (newValue === null) searchParams.delete(key);
+ const newRelativePathQuery = window.location.pathname + '?' + searchParams.toString();
+ history.pushState(null, '', newRelativePathQuery);
+ }
+ );
+ anAtom.onMount = setAtom => {
+ const callback = () => {
+ const searchParams = new URLSearchParams(location.hash.slice(2));
+ const str = searchParams.get(key);
+ if (str !== null) {
+ setAtom(str);
+ }
+ };
+ window.addEventListener(event, callback);
+ callback();
+ return () => {
+ window.removeEventListener(event, callback);
+ };
+ };
+ return anAtom;
+}
diff --git a/src/common/hooks/account/use-account-activity.ts b/src/common/hooks/account/use-account-activity.ts
index dd1d2c6a..e20d42dd 100644
--- a/src/common/hooks/account/use-account-activity.ts
+++ b/src/common/hooks/account/use-account-activity.ts
@@ -1,6 +1,6 @@
-import { useLoadable } from '@common/hooks/use-loadable';
import { accountTransactionsState } from '@store/accounts';
+import { useAtomValue } from 'jotai/utils';
export function useAccountActivity() {
- return useLoadable(accountTransactionsState);
+ return useAtomValue(accountTransactionsState);
}
diff --git a/src/common/hooks/account/use-account-balances.ts b/src/common/hooks/account/use-account-balances.ts
index df344e59..687be9c1 100644
--- a/src/common/hooks/account/use-account-balances.ts
+++ b/src/common/hooks/account/use-account-balances.ts
@@ -1,7 +1,13 @@
import { useFetchAccountData } from '@common/hooks/account/use-account-info';
+import { useAtomValue } from 'jotai/utils';
+import { accountInfoState } from '@store/accounts';
export function useAccountBalances() {
const accountData = useFetchAccountData();
- return accountData?.value?.balances;
+ return accountData?.balances;
+}
+
+export function useAccountInfo() {
+ return useAtomValue(accountInfoState);
}
diff --git a/src/common/hooks/account/use-account-info.ts b/src/common/hooks/account/use-account-info.ts
index 875d241e..db3d2abb 100644
--- a/src/common/hooks/account/use-account-info.ts
+++ b/src/common/hooks/account/use-account-info.ts
@@ -1,10 +1,10 @@
import { accountBalancesState, accountDataState } from '@store/accounts';
-import { useLoadable } from '../use-loadable';
+import { useAtomValue } from 'jotai/utils';
export const useFetchAccountData = () => {
- return useLoadable(accountDataState);
+ return useAtomValue(accountDataState);
};
export const useFetchBalances = () => {
- return useLoadable(accountBalancesState);
+ return useAtomValue(accountBalancesState);
};
diff --git a/src/common/hooks/account/use-account-names.ts b/src/common/hooks/account/use-account-names.ts
index 92e90f9c..febcb116 100644
--- a/src/common/hooks/account/use-account-names.ts
+++ b/src/common/hooks/account/use-account-names.ts
@@ -1,11 +1,13 @@
import { accountNameState } from '@store/accounts/names';
-import { useLoadable } from '@common/hooks/use-loadable';
import { Account } from '@stacks/wallet-sdk';
import { useCurrentAccount } from '@common/hooks/account/use-current-account';
import { cleanUsername } from '@common/utils';
+import { useAtomValue } from 'jotai/utils';
+import { useMemo } from 'react';
export function useAccountNames() {
- return useLoadable(accountNameState);
+ const atom = useMemo(() => accountNameState, []);
+ return useAtomValue(atom);
}
export function useAccountDisplayName(__account?: Account) {
@@ -14,7 +16,7 @@ export function useAccountDisplayName(__account?: Account) {
const account = __account || _account;
if (!account || typeof account?.index !== 'number') return 'Account';
return (
- names.value?.[account.index]?.names?.[0] ||
+ names?.[account.index]?.names?.[0] ||
(account?.username && cleanUsername(account.username)) ||
`Account ${account?.index + 1}`
);
diff --git a/src/common/hooks/account/use-accounts.ts b/src/common/hooks/account/use-accounts.ts
new file mode 100644
index 00000000..388d79b8
--- /dev/null
+++ b/src/common/hooks/account/use-accounts.ts
@@ -0,0 +1,6 @@
+import { useAtomValue } from 'jotai/utils';
+import { accountsWithAddressState } from '@store/accounts';
+
+export function useAccounts() {
+ return useAtomValue(accountsWithAddressState);
+}
diff --git a/src/common/hooks/account/use-current-account.ts b/src/common/hooks/account/use-current-account.ts
index 5faa4f4b..abd3b155 100644
--- a/src/common/hooks/account/use-current-account.ts
+++ b/src/common/hooks/account/use-current-account.ts
@@ -1,11 +1,6 @@
-import { useRecoilValue } from 'recoil';
-import { currentAccountState, currentAccountStxAddressState } from '@store/accounts';
+import { currentAccountState } from '@store/accounts';
+import { useAtomValue } from 'jotai/utils';
export function useCurrentAccount() {
- const accountInfo = useRecoilValue(currentAccountState);
- const stxAddress = useRecoilValue(currentAccountStxAddressState);
- return {
- ...accountInfo,
- stxAddress,
- };
+ return useAtomValue(currentAccountState);
}
diff --git a/src/common/hooks/account/use-switch-account.ts b/src/common/hooks/account/use-switch-account.ts
index 134601f5..22b06986 100644
--- a/src/common/hooks/account/use-switch-account.ts
+++ b/src/common/hooks/account/use-switch-account.ts
@@ -1,16 +1,19 @@
import { useWallet } from '@common/hooks/use-wallet';
-import { useRecoilState, useRecoilValue } from 'recoil';
import { hasSwitchedAccountsState, transactionAccountIndexState } from '@store/accounts';
import { useCallback } from 'react';
import { transactionNetworkVersionState } from '@store/transactions';
+import { useAtomValue } from 'jotai/utils';
+import { useAtom } from 'jotai';
+import { useCurrentAccount } from '@common/hooks/account/use-current-account';
const TIMEOUT = 350;
export const useSwitchAccount = (callback?: () => void) => {
- const { wallet, currentAccountIndex, doSwitchAccount } = useWallet();
- const txIndex = useRecoilValue(transactionAccountIndexState);
- const transactionVersion = useRecoilValue(transactionNetworkVersionState);
- const [hasSwitched, setHasSwitched] = useRecoilState(hasSwitchedAccountsState);
+ const { doSwitchAccount } = useWallet();
+ const currentAccount = useCurrentAccount();
+ const txIndex = useAtomValue(transactionAccountIndexState);
+ const transactionVersion = useAtomValue(transactionNetworkVersionState);
+ const [hasSwitched, setHasSwitched] = useAtom(hasSwitchedAccountsState);
const handleSwitchAccount = useCallback(
async index => {
@@ -25,9 +28,13 @@ export const useSwitchAccount = (callback?: () => void) => {
[txIndex, setHasSwitched, doSwitchAccount, callback]
);
- const accounts = wallet?.accounts || [];
- const getIsActive = (index: number) =>
- typeof txIndex === 'number' && !hasSwitched ? index === txIndex : index === currentAccountIndex;
+ const getIsActive = useCallback(
+ (index: number) =>
+ typeof txIndex === 'number' && !hasSwitched
+ ? index === txIndex
+ : index === currentAccount?.index,
+ [txIndex, hasSwitched, currentAccount]
+ );
- return { accounts, handleSwitchAccount, getIsActive, transactionVersion };
+ return { handleSwitchAccount, getIsActive, transactionVersion };
};
diff --git a/src/common/hooks/auth/use-auth-request.ts b/src/common/hooks/auth/use-auth-request.ts
index 42045d55..4438bb7d 100644
--- a/src/common/hooks/auth/use-auth-request.ts
+++ b/src/common/hooks/auth/use-auth-request.ts
@@ -1,6 +1,6 @@
-import { useRecoilValue } from 'recoil';
import { authRequestState } from '@store/onboarding';
+import { useAtomValue } from 'jotai/utils';
export function useAuthRequest() {
- return useRecoilValue(authRequestState);
+ return useAtomValue(authRequestState);
}
diff --git a/src/common/hooks/auth/use-magic-recovery-code.ts b/src/common/hooks/auth/use-magic-recovery-code.ts
index 98b6d028..00114da2 100644
--- a/src/common/hooks/auth/use-magic-recovery-code.ts
+++ b/src/common/hooks/auth/use-magic-recovery-code.ts
@@ -1,4 +1,3 @@
-import { useRecoilState } from 'recoil';
import { magicRecoveryCodePasswordState, magicRecoveryCodeState } from '@store/onboarding';
import { useLoading } from '@common/hooks/use-loading';
import { useWallet } from '@common/hooks/use-wallet';
@@ -8,10 +7,11 @@ import { useDoChangeScreen } from '@common/hooks/use-do-change-screen';
import { USERNAMES_ENABLED } from '@common/constants';
import { ScreenPaths } from '@store/common/types';
import { decrypt } from '@stacks/wallet-sdk';
+import { useAtom } from 'jotai';
export function useMagicRecoveryCode() {
- const [magicRecoveryCode, setMagicRecoveryCode] = useRecoilState(magicRecoveryCodeState);
- const [password, setPassword] = useRecoilState(magicRecoveryCodePasswordState);
+ const [magicRecoveryCode, setMagicRecoveryCode] = useAtom(magicRecoveryCodeState);
+ const [password, setPassword] = useAtom(magicRecoveryCodePasswordState);
const { isLoading, setIsLoading, setIsIdle } = useLoading('useMagicRecoveryCode');
const { doStoreSeed, doSetPassword, doFinishSignIn } = useWallet();
const [error, setPasswordError] = useState('');
@@ -37,8 +37,8 @@ export function useMagicRecoveryCode() {
setIsLoading();
try {
const codeBuffer = Buffer.from(magicRecoveryCode, 'base64');
- const seed = await decrypt(codeBuffer, password);
- await doStoreSeed(seed);
+ const secretKey = await decrypt(codeBuffer, password);
+ await doStoreSeed({ secretKey });
await doSetPassword(password);
handleNavigate();
} catch (error) {
diff --git a/src/common/hooks/auth/use-onboarding-state.ts b/src/common/hooks/auth/use-onboarding-state.ts
index 621ba259..95d51f3d 100644
--- a/src/common/hooks/auth/use-onboarding-state.ts
+++ b/src/common/hooks/auth/use-onboarding-state.ts
@@ -1,4 +1,3 @@
-import { useRecoilValue } from 'recoil';
import {
authRequestState,
currentScreenState,
@@ -8,18 +7,19 @@ import {
secretKeyState,
usernameState,
} from '@store/onboarding';
+import { useAtomValue } from 'jotai/utils';
export const useOnboardingState = () => {
- const secretKey = useRecoilValue(secretKeyState);
- const screen = useRecoilValue(currentScreenState);
+ const secretKey = useAtomValue(secretKeyState);
+ const screen = useAtomValue(currentScreenState);
const { authRequest, decodedAuthRequest, appName, appIcon, appURL } =
- useRecoilValue(authRequestState);
+ useAtomValue(authRequestState);
- const magicRecoveryCode = useRecoilValue(magicRecoveryCodeState);
- const isOnboardingInProgress = useRecoilValue(onboardingProgressState);
- const username = useRecoilValue(usernameState);
- const onboardingPath = useRecoilValue(onboardingPathState);
+ const magicRecoveryCode = useAtomValue(magicRecoveryCodeState);
+ const isOnboardingInProgress = useAtomValue(onboardingProgressState);
+ const username = useAtomValue(usernameState);
+ const onboardingPath = useAtomValue(onboardingPathState);
return {
secretKey,
diff --git a/src/common/hooks/auth/use-save-auth-request-callback.ts b/src/common/hooks/auth/use-save-auth-request-callback.ts
index 2be8dd19..1bf64f8a 100644
--- a/src/common/hooks/auth/use-save-auth-request-callback.ts
+++ b/src/common/hooks/auth/use-save-auth-request-callback.ts
@@ -1,6 +1,6 @@
import { useCallback, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
-import { useSetRecoilState } from 'recoil';
+
import { decodeToken } from 'jsontokens';
import { DecodedAuthRequest } from '@common/dev/types';
import { useWallet } from '@common/hooks/use-wallet';
@@ -8,12 +8,13 @@ import { getRequestOrigin, StorageKey } from '@common/storage';
import { ScreenPaths } from '@store/common/types';
import { useOnboardingState } from './use-onboarding-state';
import { authRequestState, currentScreenState } from '@store/onboarding';
+import { useUpdateAtom } from 'jotai/utils';
export function useSaveAuthRequest() {
const { wallet } = useWallet();
const { screen } = useOnboardingState();
- const changeScreen = useSetRecoilState(currentScreenState);
- const saveAuthRequest = useSetRecoilState(authRequestState);
+ const changeScreen = useUpdateAtom(currentScreenState);
+ const saveAuthRequest = useUpdateAtom(authRequestState);
const location = useLocation();
const accounts = wallet?.accounts;
const saveAuthRequestParam = useCallback(
diff --git a/src/common/hooks/auth/use-sign-in.ts b/src/common/hooks/auth/use-sign-in.ts
index 64f3407e..b235124c 100644
--- a/src/common/hooks/auth/use-sign-in.ts
+++ b/src/common/hooks/auth/use-sign-in.ts
@@ -1,4 +1,3 @@
-import { useRecoilState, useSetRecoilState } from 'recoil';
import { magicRecoveryCodeState, seedInputErrorState, seedInputState } from '@store/onboarding';
import React, { useCallback, useEffect, useRef } from 'react';
import { useWallet } from '@common/hooks/use-wallet';
@@ -10,11 +9,13 @@ import {
} from '@common/utils';
import { ScreenPaths } from '@store/common/types';
import { useLoading } from '@common/hooks/use-loading';
+import { useUpdateAtom } from 'jotai/utils';
+import { useAtom } from 'jotai';
export function useSignIn() {
- const setMagicRecoveryCode = useSetRecoilState(magicRecoveryCodeState);
- const [seed, setSeed] = useRecoilState(seedInputState);
- const [error, setError] = useRecoilState(seedInputErrorState);
+ const setMagicRecoveryCode = useUpdateAtom(magicRecoveryCodeState);
+ const [seed, setSeed] = useAtom(seedInputState);
+ const [error, setError] = useAtom(seedInputErrorState);
const { isLoading, setIsLoading, setIsIdle } = useLoading('useSignIn');
const doChangeScreen = useDoChangeScreen();
@@ -57,7 +58,7 @@ export function useSignIn() {
}
try {
- await doStoreSeed(parsedKeyInput);
+ await doStoreSeed({ secretKey: parsedKeyInput });
doChangeScreen(ScreenPaths.SET_PASSWORD);
setIsIdle();
} catch (error) {
diff --git a/src/common/hooks/use-assets.ts b/src/common/hooks/use-assets.ts
index f5a6bad1..108ee50a 100644
--- a/src/common/hooks/use-assets.ts
+++ b/src/common/hooks/use-assets.ts
@@ -1,4 +1,3 @@
-import { useLoadable } from '@common/hooks/use-loadable';
import {
assetsState,
fungibleTokensState,
@@ -6,23 +5,28 @@ import {
stxTokenState,
transferableAssetsState,
} from '@store/assets/tokens';
+import { useAtomValue } from 'jotai/utils';
+import { useMemo } from 'react';
export const useAssets = () => {
- return useLoadable(assetsState);
+ return useAtomValue(assetsState);
};
export const useTransferableAssets = () => {
- return useLoadable(transferableAssetsState);
+ return useAtomValue(transferableAssetsState);
};
export function useFungibleTokenState() {
- return useLoadable(fungibleTokensState);
+ const atom = useMemo(() => fungibleTokensState, []);
+ return useAtomValue(atom);
}
export function useNonFungibleTokenState() {
- return useLoadable(nonFungibleTokensState);
+ const atom = useMemo(() => nonFungibleTokensState, []);
+ return useAtomValue(atom);
}
export function useStxTokenState() {
- return useLoadable(stxTokenState);
+ const atom = useMemo(() => stxTokenState, []);
+ return useAtomValue(atom);
}
diff --git a/src/common/hooks/use-current-network.ts b/src/common/hooks/use-current-network.ts
index 865a0672..3f9e74dc 100644
--- a/src/common/hooks/use-current-network.ts
+++ b/src/common/hooks/use-current-network.ts
@@ -1,12 +1,12 @@
-import { useRecoilValue } from 'recoil';
import { currentNetworkState } from '@store/networks';
import { useMemo } from 'react';
import { ChainID } from '@stacks/transactions';
+import { useAtomValue } from 'jotai/utils';
type Modes = 'testnet' | 'mainnet';
export function useCurrentNetwork() {
- const network = useRecoilValue(currentNetworkState);
+ const network = useAtomValue(currentNetworkState);
const isTestnet = useMemo(() => network.chainId === ChainID.Testnet, [network.chainId]);
const mode = (isTestnet ? 'testnet' : 'mainnet') as Modes;
return {
diff --git a/src/common/hooks/use-do-change-screen.ts b/src/common/hooks/use-do-change-screen.ts
index ad565b7e..c1bb0c12 100644
--- a/src/common/hooks/use-do-change-screen.ts
+++ b/src/common/hooks/use-do-change-screen.ts
@@ -1,14 +1,15 @@
import { ScreenPaths } from '@store/common/types';
import { useNavigate } from 'react-router-dom';
import { useCallback } from 'react';
-import { useSetRecoilState } from 'recoil';
+
import { currentScreenState } from '@store/onboarding';
+import { useUpdateAtom } from 'jotai/utils';
type DoChangeScreen = (path: ScreenPaths, changeRoute?: boolean) => void;
export function useDoChangeScreen(): DoChangeScreen {
const navigate = useNavigate();
- const changeScreen = useSetRecoilState(currentScreenState);
+ const changeScreen = useUpdateAtom(currentScreenState);
const doNavigatePage = useCallback(
(path: ScreenPaths) => {
diff --git a/src/common/hooks/use-drawers.ts b/src/common/hooks/use-drawers.ts
index 35375bcb..d19e38ff 100644
--- a/src/common/hooks/use-drawers.ts
+++ b/src/common/hooks/use-drawers.ts
@@ -1,16 +1,16 @@
+import { useAtom } from 'jotai';
import {
showNetworksStore,
accountDrawerStep,
showAccountsStore,
showSettingsStore,
} from '@store/ui';
-import { useRecoilState } from 'recoil';
export const useDrawers = () => {
- const [accountStep, setAccountStep] = useRecoilState(accountDrawerStep);
- const [showAccounts, setShowAccounts] = useRecoilState(showAccountsStore);
- const [showNetworks, setShowNetworks] = useRecoilState(showNetworksStore);
- const [showSettings, setShowSettings] = useRecoilState(showSettingsStore);
+ const [accountStep, setAccountStep] = useAtom(accountDrawerStep);
+ const [showAccounts, setShowAccounts] = useAtom(showAccountsStore);
+ const [showNetworks, setShowNetworks] = useAtom(showNetworksStore);
+ const [showSettings, setShowSettings] = useAtom(showSettingsStore);
return {
accountStep,
diff --git a/src/common/hooks/use-home-tabs.ts b/src/common/hooks/use-home-tabs.ts
index a937b905..1dea4e1f 100644
--- a/src/common/hooks/use-home-tabs.ts
+++ b/src/common/hooks/use-home-tabs.ts
@@ -1,8 +1,8 @@
-import { useRecoilState } from 'recoil';
import { tabState } from '@store/ui';
+import { useAtom } from 'jotai';
export function useHomeTabs() {
- const [activeTab, setActiveTab] = useRecoilState(tabState('HOME_TABS'));
+ const [activeTab, setActiveTab] = useAtom(tabState('HOME_TABS'));
const setActiveTabBalances = () => setActiveTab(0);
const setActiveTabActivity = () => setActiveTab(1);
diff --git a/src/common/hooks/use-loading.ts b/src/common/hooks/use-loading.ts
index 3502ee13..abc5a91d 100644
--- a/src/common/hooks/use-loading.ts
+++ b/src/common/hooks/use-loading.ts
@@ -1,15 +1,15 @@
-import { useRecoilState } from 'recoil';
import { loadingState } from '@store/ui';
+import { useAtom } from 'jotai';
export enum LOADING_KEYS {
SUBMIT_TRANSACTION = 'loading/SUBMIT_TRANSACTION',
}
export function useLoading(key: string) {
- const [state, setState] = useRecoilState(loadingState(key));
+ const [state, setState] = useAtom(loadingState(key));
- const setIsLoading = () => setState(() => 'loading');
- const setIsIdle = () => setState(() => 'idle');
+ const setIsLoading = () => setState('loading');
+ const setIsIdle = () => setState('idle');
const isLoading = state === 'loading';
return {
diff --git a/src/common/hooks/use-on-cancel.ts b/src/common/hooks/use-on-cancel.ts
index bd00784e..b3328ee6 100644
--- a/src/common/hooks/use-on-cancel.ts
+++ b/src/common/hooks/use-on-cancel.ts
@@ -1,20 +1,23 @@
-import { useRecoilCallback } from 'recoil';
+import { useAtomCallback } from 'jotai/utils';
import { requestTokenState } from '@store/transactions/requests';
import { finalizeTxSignature } from '@common/utils';
import { transactionBroadcastErrorState } from '@store/transactions';
+import { useCallback } from 'react';
export function useOnCancel() {
- return useRecoilCallback(({ snapshot, set }) => async () => {
- const requestToken = await snapshot.getPromise(requestTokenState);
- if (!requestToken) {
- set(transactionBroadcastErrorState, 'No pending transaction found.');
- return;
- }
- try {
- const result = 'cancel';
- finalizeTxSignature(requestToken, result);
- } catch (error) {
- set(transactionBroadcastErrorState, error.message);
- }
- });
+ return useAtomCallback(
+ useCallback(async (get, set) => {
+ const requestToken = get(requestTokenState);
+ if (!requestToken) {
+ set(transactionBroadcastErrorState, 'No pending transaction found.');
+ return;
+ }
+ try {
+ const result = 'cancel';
+ finalizeTxSignature(requestToken, result);
+ } catch (error) {
+ set(transactionBroadcastErrorState, error.message);
+ }
+ }, [])
+ );
}
diff --git a/src/common/hooks/use-origin.ts b/src/common/hooks/use-origin.ts
index 50352720..16914ff4 100644
--- a/src/common/hooks/use-origin.ts
+++ b/src/common/hooks/use-origin.ts
@@ -1,6 +1,6 @@
-import { useRecoilValue } from 'recoil';
+import { useAtomValue } from 'jotai/utils';
import { requestTokenOriginState } from '@store/transactions/requests';
export function useOrigin() {
- return useRecoilValue(requestTokenOriginState);
+ return useAtomValue(requestTokenOriginState);
}
diff --git a/src/common/hooks/use-revalidate-api.ts b/src/common/hooks/use-revalidate-api.ts
deleted file mode 100644
index f3ba6f0f..00000000
--- a/src/common/hooks/use-revalidate-api.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { useRecoilCallback } from 'recoil';
-import { apiRevalidation } from '@store/common/api-helpers';
-
-export function useRevalidateApi() {
- return useRecoilCallback(({ snapshot, set }) => async () => {
- const count = await snapshot.getPromise(apiRevalidation);
- set(apiRevalidation, count + 1);
- });
-}
diff --git a/src/common/hooks/use-selected-asset.ts b/src/common/hooks/use-selected-asset.ts
index d7ecc3ce..54e06dea 100644
--- a/src/common/hooks/use-selected-asset.ts
+++ b/src/common/hooks/use-selected-asset.ts
@@ -1,19 +1,18 @@
-import { useSetRecoilState } from 'recoil';
-import { useLoadable } from '@common/hooks/use-loadable';
import { selectedAssetIdState, selectedAssetStore } from '@store/assets/asset-search';
import { AssetWithMeta } from '@store/assets/types';
import { getTicker } from '@common/utils';
import { useCallback, useMemo } from 'react';
import { ftDecimals, stacksValue } from '@common/stacks-utils';
import BigNumber from 'bignumber.js';
+import { useAtomValue, useUpdateAtom } from 'jotai/utils';
export function getFullyQualifiedAssetName(asset?: AssetWithMeta) {
return asset ? `${asset.contractAddress}.${asset.contractName}::${asset.name}` : undefined;
}
export function useSelectedAsset() {
- const { value: selectedAsset } = useLoadable(selectedAssetStore);
- const setSelectedAsset = useSetRecoilState(selectedAssetIdState);
+ const selectedAsset = useAtomValue(selectedAssetStore);
+ const setSelectedAsset = useUpdateAtom(selectedAssetIdState);
const handleUpdateSelectedAsset = useCallback(
(asset: AssetWithMeta | undefined) => {
setSelectedAsset(getFullyQualifiedAssetName(asset) || undefined);
diff --git a/src/common/hooks/use-send-form-validation.ts b/src/common/hooks/use-send-form-validation.ts
index 87fab265..57386fba 100644
--- a/src/common/hooks/use-send-form-validation.ts
+++ b/src/common/hooks/use-send-form-validation.ts
@@ -30,10 +30,10 @@ export const useSendFormValidation = ({
errors.amount = 'Must be more than zero';
}
if (selectedAsset) {
- if (balances.value) {
+ if (balances) {
const amountBN = new BigNumber(amount);
if (selectedAsset.type === 'stx') {
- const curBalance = microStxToStx(balances.value.stx.balance);
+ const curBalance = microStxToStx(balances.stx.balance);
if (curBalance.lt(amountBN)) {
errors.amount = `You don't have enough tokens, Your balance is ${curBalance.toString()}`;
}
diff --git a/src/common/hooks/use-send-form.ts b/src/common/hooks/use-send-form.ts
index afe4f712..801868c8 100644
--- a/src/common/hooks/use-send-form.ts
+++ b/src/common/hooks/use-send-form.ts
@@ -16,9 +16,9 @@ export function useSendAmountFieldActions({
const isStx = selectedAsset?.type === 'stx';
const handleSetSendMax = useCallback(() => {
- if (!selectedAsset || !balances.value) return;
+ if (!selectedAsset || !balances) return;
if (isStx) {
- const stx = microStxToStx(balances.value.stx.balance);
+ const stx = microStxToStx(balances.stx.balance);
setFieldValue('amount', stx.toNumber());
} else {
if (balance) setFieldValue('amount', removeCommas(balance));
@@ -76,10 +76,10 @@ export function useSendForm({
const assets = useAssets();
useEffect(() => {
- if (assets.value?.length === 1 && amountFieldRef.current) {
+ if (assets?.length === 1 && amountFieldRef.current) {
amountFieldRef?.current?.focus?.();
}
- }, [amountFieldRef, assets.value?.length]);
+ }, [amountFieldRef, assets?.length]);
useEffect(() => {
if (previous !== selectedAsset) {
diff --git a/src/common/hooks/use-transaction-request.ts b/src/common/hooks/use-transaction-request.ts
index f231f13d..37e49e3e 100644
--- a/src/common/hooks/use-transaction-request.ts
+++ b/src/common/hooks/use-transaction-request.ts
@@ -1,7 +1,6 @@
-import { useLoadable } from '@common/hooks/use-loadable';
import { requestTokenPayloadState } from '@store/transactions/requests';
+import { useAtomValue } from 'jotai/utils';
export function useTransactionRequest() {
- const payload = useLoadable(requestTokenPayloadState);
- return payload?.value;
+ return useAtomValue(requestTokenPayloadState);
}
diff --git a/src/common/hooks/use-vault-messenger.ts b/src/common/hooks/use-vault-messenger.ts
index 2c589395..a8e26420 100644
--- a/src/common/hooks/use-vault-messenger.ts
+++ b/src/common/hooks/use-vault-messenger.ts
@@ -5,7 +5,7 @@ import {
UnlockWallet,
SwitchAccount,
} from '@background/vault-types';
-import { RecoilState, useRecoilCallback } from 'recoil';
+
import {
hasSetPasswordState,
walletState,
@@ -15,87 +15,102 @@ import {
} from '@store/wallet';
import { InMemoryVault } from '@background/vault';
import { InternalMethods } from '@content-scripts/message-types';
-import { currentAccountIndexState } from '@store/accounts';
import { textToBytes } from '@store/common/utils';
+import { useAtomCallback } from 'jotai/utils';
+import { useCallback } from 'react';
+import { currentAccountIndexState } from '@store/accounts';
-type Set = (store: RecoilState, value: T) => void;
-
-const innerMessageWrapper = async (message: VaultActions, set: Set) => {
- return new Promise((resolve, reject) => {
- chrome.runtime.sendMessage(message, (vaultOrError: InMemoryVault | Error) => {
- if ('hasSetPassword' in vaultOrError) {
- const vault = vaultOrError;
- set(hasRehydratedVaultStore, true);
- set(hasSetPasswordState, vault.hasSetPassword);
- set(walletState, vault.wallet);
- set(secretKeyState, vault.secretKey ? textToBytes(vault.secretKey) : undefined);
- set(currentAccountIndexState, vault.currentAccountIndex);
- set(encryptedSecretKeyStore, vault.encryptedSecretKey);
- resolve(vault);
- } else {
- reject(vaultOrError);
- }
- });
- });
-};
-
-const messageWrapper = (message: VaultActions) => {
- return useRecoilCallback(
- ({ set }) =>
- () =>
- innerMessageWrapper(message, set),
- [message]
+const useInnerMessageWrapper = () => {
+ return useAtomCallback(
+ useCallback(async (_get, set, message) => {
+ return new Promise((resolve, reject) => {
+ chrome.runtime.sendMessage(message, (vaultOrError: InMemoryVault | Error) => {
+ try {
+ if ('hasSetPassword' in vaultOrError) {
+ const vault = vaultOrError;
+ set(hasRehydratedVaultStore, true);
+ set(hasSetPasswordState, vault.hasSetPassword);
+ set(walletState, vault.wallet);
+ set(secretKeyState, vault.secretKey ? textToBytes(vault.secretKey) : undefined);
+ set(currentAccountIndexState, vault.currentAccountIndex);
+ set(encryptedSecretKeyStore, vault.encryptedSecretKey);
+ resolve(vault);
+ } else {
+ reject(vaultOrError);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ });
+ });
+ }, [])
);
};
export const useVaultMessenger = () => {
- const doSetPassword = useRecoilCallback(({ set }) => (password: string) => {
- const message: SetPassword = {
- method: InternalMethods.setPassword,
- payload: password,
- };
- return innerMessageWrapper(message, set);
- });
+ const innerMessageWrapper = useInnerMessageWrapper();
- const doStoreSeed = useRecoilCallback(({ set }) => (secretKey: string, password?: string) => {
- const message: StoreSeed = {
- method: InternalMethods.storeSeed,
- payload: {
- secretKey,
- password,
- },
- };
- return innerMessageWrapper(message, set);
- });
+ const doSetPassword = useCallback(
+ (payload: string) => {
+ const message: SetPassword = {
+ method: InternalMethods.setPassword,
+ payload,
+ };
+ return innerMessageWrapper(message);
+ },
+ [innerMessageWrapper]
+ );
- const doUnlockWallet = useRecoilCallback(({ set }) => (password: string) => {
- const message: UnlockWallet = {
- method: InternalMethods.unlockWallet,
- payload: password,
- };
- return innerMessageWrapper(message, set);
- });
+ const doStoreSeed = useCallback(
+ (payload: { secretKey: string; password?: string }) => {
+ const message: StoreSeed = {
+ method: InternalMethods.storeSeed,
+ payload,
+ };
+ return innerMessageWrapper(message);
+ },
+ [innerMessageWrapper]
+ );
- const doSwitchAccount = useRecoilCallback(({ set }) => (index: number) => {
- const message: SwitchAccount = {
- method: InternalMethods.switchAccount,
- payload: index,
- };
- return innerMessageWrapper(message, set);
- });
+ const doUnlockWallet = useCallback(
+ payload => {
+ const message: UnlockWallet = {
+ method: InternalMethods.unlockWallet,
+ payload,
+ };
+ return innerMessageWrapper(message);
+ },
+ [innerMessageWrapper]
+ );
- const getWallet = messageWrapper({ method: InternalMethods.getWallet, payload: undefined });
- const doMakeWallet = messageWrapper({ method: InternalMethods.makeWallet, payload: undefined });
- const doCreateNewAccount = messageWrapper({
- method: InternalMethods.createNewAccount,
- payload: undefined,
- });
- const handleSignOut = messageWrapper({ method: InternalMethods.signOut, payload: undefined });
+ const doSwitchAccount = useCallback(
+ payload => {
+ const message: SwitchAccount = {
+ method: InternalMethods.switchAccount,
+ payload,
+ };
+ return innerMessageWrapper(message);
+ },
+ [innerMessageWrapper]
+ );
+
+ const getWallet = () =>
+ innerMessageWrapper({ method: InternalMethods.getWallet, payload: undefined });
+ const doMakeWallet = () =>
+ innerMessageWrapper({ method: InternalMethods.makeWallet, payload: undefined });
+ const doCreateNewAccount = () =>
+ innerMessageWrapper({
+ method: InternalMethods.createNewAccount,
+ payload: undefined,
+ });
+ const handleSignOut = () =>
+ innerMessageWrapper({ method: InternalMethods.signOut, payload: undefined });
const doSignOut = async () => {
await handleSignOut();
localStorage.clear();
};
- const doLockWallet = messageWrapper({ method: InternalMethods.lockWallet, payload: undefined });
+ const doLockWallet = () =>
+ innerMessageWrapper({ method: InternalMethods.lockWallet, payload: undefined });
return {
getWallet,
diff --git a/src/common/hooks/use-wallet.ts b/src/common/hooks/use-wallet.ts
index 4fc525a5..c6de2baf 100644
--- a/src/common/hooks/use-wallet.ts
+++ b/src/common/hooks/use-wallet.ts
@@ -6,28 +6,21 @@ import {
makeAuthResponse,
getAccountDisplayName,
} from '@stacks/wallet-sdk';
-import { useRecoilValue, useRecoilState, useRecoilCallback } from 'recoil';
+
import { gaiaUrl } from '@common/constants';
-import {
- currentNetworkKeyState,
- currentNetworkState,
- networksState,
- latestBlockHeightState,
-} from '@store/networks';
+import { currentNetworkKeyState, currentNetworkState, networksState } from '@store/networks';
import {
walletState,
encryptedSecretKeyStore,
secretKeyState,
hasSetPasswordState,
- walletConfigStore,
hasRehydratedVaultStore,
} from '@store/wallet';
import { useVaultMessenger } from '@common/hooks/use-vault-messenger';
import { useOnboardingState } from './auth/use-onboarding-state';
import { finalizeAuthResponse } from '@common/utils';
-import { apiRevalidation } from '@store/common/api-helpers';
-import { useLoadable } from '@common/hooks/use-loadable';
+
import {
currentAccountIndexState,
currentAccountState,
@@ -36,21 +29,22 @@ import {
import { localNoncesState } from '@store/accounts/nonce';
import { bytesToText } from '@store/common/utils';
import { transactionNetworkVersionState } from '@store/transactions';
+import { useAtom } from 'jotai';
+import { useAtomValue, useAtomCallback } from 'jotai/utils';
export const useWallet = () => {
- const hasRehydratedVault = useRecoilValue(hasRehydratedVaultStore);
- const [wallet, setWallet] = useRecoilState(walletState);
- const secretKey = useRecoilValue(secretKeyState);
- const encryptedSecretKey = useRecoilValue(encryptedSecretKeyStore);
- const currentAccountIndex = useRecoilValue(currentAccountIndexState);
- const hasSetPassword = useRecoilValue(hasSetPasswordState);
- const currentAccount = useRecoilValue(currentAccountState);
- const currentAccountStxAddress = useRecoilValue(currentAccountStxAddressState);
- const transactionVersion = useRecoilValue(transactionNetworkVersionState);
- const networks = useRecoilValue(networksState);
- const currentNetwork = useRecoilValue(currentNetworkState);
- const currentNetworkKey = useRecoilValue(currentNetworkKeyState);
- const walletConfig = useLoadable(walletConfigStore);
+ const hasRehydratedVault = useAtomValue(hasRehydratedVaultStore);
+ const [wallet, setWallet] = useAtom(walletState);
+ const secretKey = useAtomValue(secretKeyState);
+ const encryptedSecretKey = useAtomValue(encryptedSecretKeyStore);
+ const currentAccountIndex = useAtomValue(currentAccountIndexState);
+ const hasSetPassword = useAtomValue(hasSetPasswordState);
+ const currentAccount = useAtomValue(currentAccountState);
+ const currentAccountStxAddress = useAtomValue(currentAccountStxAddressState);
+ const transactionVersion = useAtomValue(transactionNetworkVersionState);
+ const networks = useAtomValue(networksState);
+ const currentNetwork = useAtomValue(currentNetworkState);
+ const currentNetworkKey = useAtomValue(currentNetworkKeyState);
const vaultMessenger = useVaultMessenger();
const currentAccountDisplayName = currentAccount
@@ -61,21 +55,14 @@ export const useWallet = () => {
const isSignedIn = !!wallet;
- const doSetLatestNonce = useRecoilCallback(
- ({ snapshot, set }) =>
- async (newNonce?: number) => {
- if (newNonce !== undefined) {
- set(apiRevalidation, current => (current as number) + 1);
- const blockHeight = await snapshot.getPromise(latestBlockHeightState);
- const network = await snapshot.getPromise(currentNetworkState);
- const address = await snapshot.getPromise(currentAccountStxAddressState);
- set(localNoncesState([network.url, address || '']), () => ({
- blockHeight,
- nonce: newNonce,
- }));
- }
- },
- []
+ const doSetLatestNonce = useAtomCallback(
+ useCallback((get, set, newNonce) => {
+ if (newNonce !== undefined) {
+ const network = get(currentNetworkState);
+ const address = get(currentAccountStxAddressState);
+ set(localNoncesState([network.url, address || '']), newNonce);
+ }
+ }, [])
);
const handleCancelAuthentication = useCallback(() => {
@@ -86,10 +73,10 @@ export const useWallet = () => {
finalizeAuthResponse({ decodedAuthRequest, authRequest, authResponse });
}, [decodedAuthRequest, authRequest]);
- const doFinishSignIn = useRecoilCallback(
- ({ set, snapshot }) =>
- async (accountIndex: number) => {
- const wallet = await snapshot.getPromise(walletState);
+ const doFinishSignIn = useAtomCallback(
+ useCallback(
+ async (get, set, accountIndex) => {
+ const wallet = get(walletState);
const account = wallet?.accounts[accountIndex];
if (!decodedAuthRequest || !authRequest || !account || !wallet) {
console.error('Uh oh! Finished onboarding without auth info.');
@@ -125,7 +112,8 @@ export const useWallet = () => {
set(currentAccountIndexState, accountIndex);
finalizeAuthResponse({ decodedAuthRequest, authRequest, authResponse });
},
- [decodedAuthRequest, authRequest, appName, appIcon]
+ [appName, appIcon, authRequest, decodedAuthRequest]
+ )
);
return {
@@ -138,7 +126,7 @@ export const useWallet = () => {
currentAccountStxAddress,
currentAccountDisplayName,
transactionVersion,
- walletConfig,
+
networks,
currentNetwork,
currentNetworkKey,
diff --git a/src/common/token-utils.ts b/src/common/token-utils.ts
index 1eb6acdd..47575fb6 100644
--- a/src/common/token-utils.ts
+++ b/src/common/token-utils.ts
@@ -11,11 +11,7 @@ function handleErrorMessage(message = 'Error') {
export type SIP010TransferResponse = { okay: true; hasMemo: boolean } | { error: string };
-export async function isSip10Transfer({
- contractInterface,
-}: {
- contractInterface: ContractInterface;
-}): Promise {
+export function isSip10Transfer(contractInterface: ContractInterface): SIP010TransferResponse {
try {
let hasCorrectName = false;
let hasCorrectSender = false;
diff --git a/src/common/transactions/postcondition-utils.ts b/src/common/transactions/postcondition-utils.ts
index 0d7f1adf..f4c3ffc4 100644
--- a/src/common/transactions/postcondition-utils.ts
+++ b/src/common/transactions/postcondition-utils.ts
@@ -33,6 +33,13 @@ export const getSymbolFromPostCondition = (pc: PostCondition) => {
return 'STX';
};
+export const getNameFromPostCondition = (pc: PostCondition) => {
+ if ('assetInfo' in pc) {
+ return pc.assetInfo.assetName.content;
+ }
+ return 'STX';
+};
+
export function getPostConditionCodeMessage(
code: FungibleConditionCode | NonFungibleConditionCode,
isSender: boolean
@@ -142,7 +149,7 @@ export const useAssetInfoFromPostCondition = (pc: PostCondition) => {
const contractAddress = addressToString(pc.assetInfo.address);
const contractName = pc.assetInfo.contractName.content;
- const asset = assets?.value?.find(
+ const asset = assets?.find(
asset =>
asset.contractAddress === contractAddress &&
asset.contractName === contractName &&
diff --git a/src/components/account-avatar.tsx b/src/components/account-avatar.tsx
index 7149e3de..3874402d 100644
--- a/src/components/account-avatar.tsx
+++ b/src/components/account-avatar.tsx
@@ -3,12 +3,11 @@ import React from 'react';
import { Account, getAccountDisplayName } from '@stacks/wallet-sdk';
import { useAccountGradient } from '@common/hooks/account/use-account-gradient';
+import { AccountWithAddress } from '@store/accounts';
-export const AccountAvatar: React.FC<{ account: Account; name?: string } & BoxProps> = ({
- account,
- name,
- ...props
-}) => {
+export const AccountAvatar: React.FC<
+ { account: AccountWithAddress | Account; name?: string } & BoxProps
+> = ({ account, name, ...props }) => {
const displayName = name && name.includes('.') ? name : getAccountDisplayName(account);
const gradient = useAccountGradient(account);
diff --git a/src/components/account-item.tsx b/src/components/account-item.tsx
index e53af915..72d68993 100644
--- a/src/components/account-item.tsx
+++ b/src/components/account-item.tsx
@@ -9,7 +9,7 @@ import { AccountWithAddress } from '@store/accounts';
export const AccountItem = memo(({ account, ...rest }: { account: AccountWithAddress }) => {
const names = useAccountNames();
- const name = names.value?.[account.index]?.names?.[0] || getAccountDisplayName(account);
+ const name = names?.[account.index]?.names?.[0] || getAccountDisplayName(account);
return (
diff --git a/src/components/accounts/index.tsx b/src/components/accounts/index.tsx
index 5b0feaa0..177fef0e 100644
--- a/src/components/accounts/index.tsx
+++ b/src/components/accounts/index.tsx
@@ -9,16 +9,16 @@ import { useOnboardingState } from '@common/hooks/auth/use-onboarding-state';
import { useAccountDisplayName } from '@common/hooks/account/use-account-names';
import { accountsWithAddressState, AccountWithAddress } from '@store/accounts';
-import { useLoadable } from '@common/hooks/use-loadable';
import { AccountAvatar } from '@components/account-avatar';
import { SpaceBetween } from '@components/space-between';
import { cleanUsername, slugify } from '@common/utils';
import { usePressable } from '@components/item-hover';
import { FiPlusCircle } from 'react-icons/fi';
-import { useSetRecoilState } from 'recoil';
+
import { accountDrawerStep, AccountStep, showAccountsStore } from '@store/ui';
import { useLoading } from '@common/hooks/use-loading';
+import { useAtomValue, useUpdateAtom } from 'jotai/utils';
const loadingProps = { color: '#A1A7B3' };
const getLoadingProps = (loading: boolean) => (loading ? loadingProps : {});
@@ -42,18 +42,16 @@ export const AccountItem: React.FC = ({ selectedAddress, accou
}, [setIsLoading, doFinishSignIn, account]);
return (
- handleOnClick()}
{...bind}
{...rest}
>
-
-
-
-
+
+
+
+
= ({ selectedAddress, accou
{truncateMiddle(account.address)}
-
- {isLoading && }
-
+ {isLoading && }
+
+
{component}
-
+
);
};
const AddAccountAction = memo(() => {
- const setAccounts = useSetRecoilState(showAccountsStore);
- const setAccountDrawerStep = useSetRecoilState(accountDrawerStep);
+ const setAccounts = useUpdateAtom(showAccountsStore);
+ const setAccountDrawerStep = useUpdateAtom(accountDrawerStep);
+ const [component, bind] = usePressable(true);
+
return (
-
- {
- setAccounts(true);
- setAccountDrawerStep(AccountStep.Create);
- }}
- >
+ {
+ setAccounts(true);
+ setAccountDrawerStep(AccountStep.Create);
+ }}
+ {...bind}
+ >
+
- Add account
+ Generate new account
-
+ {component}
+
);
});
@@ -108,20 +105,20 @@ interface AccountsProps extends FlexProps {
export const Accounts: React.FC = memo(
({ showAddAccount, accountIndex, next, ...rest }) => {
const { wallet } = useWallet();
- const { value: accounts } = useLoadable(accountsWithAddressState);
+ const accounts = useAtomValue(accountsWithAddressState);
const { decodedAuthRequest } = useOnboardingState();
if (!wallet || !accounts || !decodedAuthRequest) return null;
return (
-
-
+ <>
+
{accounts.map(account => (
))}
+
-
-
+ >
);
}
);
diff --git a/src/components/asset-search/asset-search.tsx b/src/components/asset-search/asset-search.tsx
index c1c1a6a6..598e1b52 100644
--- a/src/components/asset-search/asset-search.tsx
+++ b/src/components/asset-search/asset-search.tsx
@@ -2,11 +2,13 @@ import React, { memo, useMemo, forwardRef } from 'react';
import { Box, Fade, Text, Flex, Input, color, Stack, StackProps } from '@stacks/ui';
import { useCombobox } from 'downshift';
import { searchInputStore } from '@store/assets/asset-search';
-import { useRecoilState } from 'recoil';
+
import { SelectedAsset } from './selected-asset';
import { useTransferableAssets } from '@common/hooks/use-assets';
import { useSelectedAsset } from '@common/hooks/use-selected-asset';
import { AssetRow } from '@components/asset-row';
+import { useAtomValue } from 'jotai/utils';
+import { useAtom } from 'jotai';
interface AssetSearchResultsProps extends StackProps {
isOpen: boolean;
@@ -17,17 +19,15 @@ interface AssetSearchResultsProps extends StackProps {
const AssetSearchResults = forwardRef(
({ isOpen, highlightedIndex, getItemProps, ...props }: AssetSearchResultsProps, ref) => {
const assets = useTransferableAssets();
- const [searchInput] = useRecoilState(searchInputStore);
+ const searchInput = useAtomValue(searchInputStore);
const items = useMemo(
() =>
- assets.value?.filter(item =>
- item.name.toLowerCase().includes(searchInput.toLowerCase() || '')
- ),
- [assets.value, searchInput]
+ assets?.filter(item => item.name.toLowerCase().includes(searchInput.toLowerCase() || '')),
+ [assets, searchInput]
);
- if (!assets.value) return null;
+ if (!assets) return null;
return (
@@ -74,7 +74,7 @@ export const AssetSearchField: React.FC<{
const { selectedAsset, handleUpdateSelectedAsset } = useSelectedAsset();
- const [searchInput, setSearchInput] = useRecoilState(searchInputStore);
+ const [searchInput, setSearchInput] = useAtom(searchInputStore);
const {
isOpen,
@@ -86,7 +86,7 @@ export const AssetSearchField: React.FC<{
getItemProps,
openMenu,
} = useCombobox({
- items: assets.value || [],
+ items: assets || [],
initialIsOpen: true,
inputValue: searchInput,
defaultIsOpen: false,
@@ -103,7 +103,7 @@ export const AssetSearchField: React.FC<{
const labelRef = React.useRef(null);
const comboRef = React.useRef(null);
- if (assets.isLoading) return null;
+ if (!assets) return null;
return (
@@ -152,7 +152,7 @@ export const AssetSearch: React.FC<{
const { selectedAsset } = useSelectedAsset();
const assets = useTransferableAssets();
- if (assets.isLoading) {
+ if (!assets) {
return (
diff --git a/src/components/asset-search/selected-asset.tsx b/src/components/asset-search/selected-asset.tsx
index be6f039f..121b555b 100644
--- a/src/components/asset-search/selected-asset.tsx
+++ b/src/components/asset-search/selected-asset.tsx
@@ -4,13 +4,14 @@ import { Box, ChevronIcon, Text, color, Stack, StackProps, BoxProps } from '@sta
import { searchInputStore } from '@store/assets/asset-search';
import React, { memo, useCallback } from 'react';
-import { useSetRecoilState } from 'recoil';
+
import { Caption } from '@components/typography';
import { useSelectedAsset } from '@common/hooks/use-selected-asset';
+import { useUpdateAtom } from 'jotai/utils';
const SelectedAssetItem = memo(({ hideArrow, ...rest }: { hideArrow?: boolean } & BoxProps) => {
const { selectedAsset, ticker, name, handleUpdateSelectedAsset } = useSelectedAsset();
- const setSearchInput = useSetRecoilState(searchInputStore);
+ const setSearchInput = useUpdateAtom(searchInputStore);
const handleClear = useCallback(() => {
setSearchInput('');
diff --git a/src/components/current-user/current-stx-address.tsx b/src/components/current-user/current-stx-address.tsx
new file mode 100644
index 00000000..25c9785c
--- /dev/null
+++ b/src/components/current-user/current-stx-address.tsx
@@ -0,0 +1,20 @@
+import { Box, BoxProps } from '@stacks/ui';
+import { truncateMiddle } from '@stacks/ui-utils';
+import React from 'react';
+import { useCurrentAccount } from '@common/hooks/account/use-current-account';
+import { memoWithAs } from '@stacks/ui-core';
+import { LoadingRectangle } from '@components/loading-rectangle';
+
+const CurrentStxAddressSuspense = memoWithAs((props: BoxProps) => {
+ const currentAccount = useCurrentAccount();
+ if (!currentAccount) return null;
+ return {truncateMiddle(currentAccount.address, 4)};
+});
+
+export const CurrentStxAddress = memoWithAs((props: BoxProps) => {
+ return (
+ }>
+
+
+ );
+});
diff --git a/src/components/current-user/current-user-avatar.tsx b/src/components/current-user/current-user-avatar.tsx
new file mode 100644
index 00000000..11d50d3b
--- /dev/null
+++ b/src/components/current-user/current-user-avatar.tsx
@@ -0,0 +1,30 @@
+import React, { memo } from 'react';
+import { useCurrentAccount } from '@common/hooks/account/use-current-account';
+import { useAccountNames } from '@common/hooks/account/use-account-names';
+import { getAccountDisplayName } from '@stacks/wallet-sdk';
+import { AccountAvatar } from '@components/account-avatar';
+import { BoxProps } from '@stacks/ui';
+
+const UserAvatarSuspense = memo((props: BoxProps) => {
+ const currentAccount = useCurrentAccount();
+ const names = useAccountNames();
+ if (!currentAccount || typeof currentAccount.index === 'undefined') return null;
+ const name =
+ names?.[currentAccount.index]?.names?.[0] || getAccountDisplayName(currentAccount as any);
+ return ;
+});
+
+export const CurrentUserAvatar = memo((props: BoxProps) => {
+ const currentAccount = useCurrentAccount();
+ const defaultName = getAccountDisplayName(currentAccount as any);
+ if (!currentAccount) return null;
+ return (
+
+ }
+ >
+
+
+ );
+});
diff --git a/src/components/current-user/current-user-name.tsx b/src/components/current-user/current-user-name.tsx
new file mode 100644
index 00000000..9b86f637
--- /dev/null
+++ b/src/components/current-user/current-user-name.tsx
@@ -0,0 +1,50 @@
+import React, { memo } from 'react';
+import { BoxProps } from '@stacks/ui';
+import { useCurrentAccount } from '@common/hooks/account/use-current-account';
+import { useAccountNames } from '@common/hooks/account/use-account-names';
+import { getAccountDisplayName } from '@stacks/wallet-sdk';
+import { truncateString } from '@common/utils';
+import { Tooltip } from '@components/tooltip';
+import { Title } from '@components/typography';
+import { memoWithAs } from '@stacks/ui-core';
+
+const UsernameTitle = (props: BoxProps) => (
+
+);
+
+const UsernameSuspense = memo((props: BoxProps) => {
+ const currentAccount = useCurrentAccount();
+ const names = useAccountNames();
+ if (!currentAccount || typeof currentAccount.index === 'undefined') return null;
+ const nameCharLimit = 18;
+ const name =
+ names?.[currentAccount.index]?.names?.[0] || getAccountDisplayName(currentAccount as any);
+ const isLong = name.length > nameCharLimit;
+ const displayName = truncateString(name, nameCharLimit);
+
+ return (
+
+
+ {displayName}
+
+
+ );
+});
+
+export const CurrentUsername = memoWithAs((props: BoxProps) => {
+ const currentAccount = useCurrentAccount();
+ const defaultName = getAccountDisplayName(currentAccount as any);
+ const fallback = {defaultName};
+ return (
+
+
+
+ );
+});
diff --git a/src/components/debug-observer.tsx b/src/components/debug-observer.tsx
deleted file mode 100644
index 35188b4f..00000000
--- a/src/components/debug-observer.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useRecoilSnapshot } from 'recoil';
-import { useEffect } from 'react';
-
-export function DebugObserver() {
- const snapshot = useRecoilSnapshot();
- useEffect(() => {
- if (process.env.NODE_ENV === 'development' && !!localStorage.getItem('DEBUG')) {
- for (const node of snapshot.getNodes_UNSTABLE({ isModified: true })) {
- console.group(`[state change] ${node.key}`);
- console.log(snapshot.getLoadable(node).contents);
- console.groupEnd();
- }
- }
- }, [snapshot]);
-
- return null;
-}
diff --git a/src/components/devtools.tsx b/src/components/devtools.tsx
new file mode 100644
index 00000000..69a190a4
--- /dev/null
+++ b/src/components/devtools.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { useAtomDevtools } from 'jotai/devtools';
+import { walletState } from '@store/wallet';
+
+export function Devtools() {
+ useAtomDevtools(walletState);
+ return null;
+}
diff --git a/src/components/drawer/accounts/index.tsx b/src/components/drawer/accounts/index.tsx
index 26c60a52..f8051574 100644
--- a/src/components/drawer/accounts/index.tsx
+++ b/src/components/drawer/accounts/index.tsx
@@ -1,23 +1,22 @@
-import React from 'react';
+import React, { useCallback } from 'react';
import { ControlledDrawer } from '../controlled';
import { SwitchAccounts } from './switch-accounts';
import { CreateAccount } from './create-account';
import { AddUsername } from './add-username';
import { useDrawers } from '@common/hooks/use-drawers';
-import { useRecoilCallback } from 'recoil';
+
import { AccountStep, showAccountsStore, accountDrawerStep } from '@store/ui';
+import { useAtomCallback } from 'jotai/utils';
export const AccountsDrawer: React.FC = () => {
const { accountStep } = useDrawers();
- const close = useRecoilCallback(
- ({ set }) =>
- () => {
- set(showAccountsStore, false);
- const drawerAnimationTime = 200;
- setTimeout(() => set(accountDrawerStep, AccountStep.Switch), drawerAnimationTime);
- },
- []
+ const close = useAtomCallback(
+ useCallback((_get, set) => {
+ set(showAccountsStore, false);
+ const drawerAnimationTime = 200;
+ setTimeout(() => set(accountDrawerStep, AccountStep.Switch), drawerAnimationTime);
+ }, [])
);
const getTitle = () => {
diff --git a/src/components/drawer/accounts/switch-accounts.tsx b/src/components/drawer/accounts/switch-accounts.tsx
index c32fa9c5..2ab78aef 100644
--- a/src/components/drawer/accounts/switch-accounts.tsx
+++ b/src/components/drawer/accounts/switch-accounts.tsx
@@ -1,81 +1,135 @@
-import React, { memo } from 'react';
-import { Box, Fade, Button, Stack, color } from '@stacks/ui';
+import React, { memo, useCallback } from 'react';
+import { Box, Fade, Button, Stack, color, BoxProps, Spinner } from '@stacks/ui';
import { Title, Caption } from '@components/typography';
-import { useSetRecoilState } from 'recoil';
+
import { accountDrawerStep, AccountStep } from '@store/ui';
-import { getAccountDisplayName, getStxAddress } from '@stacks/wallet-sdk';
+import { getAccountDisplayName } from '@stacks/wallet-sdk';
import { truncateMiddle } from '@stacks/ui-utils';
import { SpaceBetween } from '@components/space-between';
import { IconCheck } from '@tabler/icons';
import { AccountAvatar } from '@components/account-avatar';
-import { useAccountNames } from '@common/hooks/account/use-account-names';
+import { useAccountDisplayName } from '@common/hooks/account/use-account-names';
import { useSwitchAccount } from '@common/hooks/account/use-switch-account';
+import { useUpdateAtom } from 'jotai/utils';
+import { useAccounts } from '@common/hooks/account/use-accounts';
+import { AccountWithAddress } from '@store/accounts';
+import { useLoading } from '@common/hooks/use-loading';
interface SwitchAccountProps {
close: () => void;
}
-// eslint-disable-next-line no-warning-comments
-// TODO: this page is nearly identical to the network switcher abstract it out into a shared component
-const AccountList: React.FC<{ handleClose: () => void }> = memo(({ handleClose }) => {
- const names = useAccountNames();
- const { accounts, handleSwitchAccount, transactionVersion, getIsActive } =
- useSwitchAccount(handleClose);
- if (!names.value) return null;
- return (
- <>
- {accounts.map((account, index) => {
- const name = names.value?.[index]?.names?.[0] || getAccountDisplayName(account);
+interface WithAccount {
+ account: AccountWithAddress;
+}
- return (
- handleSwitchAccount(index)}
- >
-
-
-
-
- {name}
-
-
- {truncateMiddle(
- getStxAddress({
- account: account,
- transactionVersion,
- }),
- 9
- )}
-
-
-
-
- {styles => (
-
- )}
-
-
- );
- })}
- >
+const AccountNameSuspense = memo(({ account }: WithAccount) => {
+ const name = useAccountDisplayName(account);
+
+ return (
+
+ {name}
+
);
});
+const AccountName = memo(({ account, ...rest }: BoxProps & WithAccount) => {
+ const defaultName = getAccountDisplayName(account);
+ return (
+
+
+ {defaultName}
+
+ }
+ >
+
+
+
+ );
+});
+
+const AccountAvatarSuspense = memo(({ account }: { account: AccountWithAddress }) => {
+ const name = useAccountDisplayName(account);
+ return ;
+});
+
+const AccountAvatarItem = memo(({ account, ...rest }: BoxProps & WithAccount) => {
+ const defaultName = getAccountDisplayName(account);
+ return (
+
+ }>
+
+
+
+ );
+});
+
+const AccountListItem = memo(
+ ({ account, handleClose }: { account: AccountWithAddress; handleClose: () => void }) => {
+ const { isLoading, setIsLoading, setIsIdle } = useLoading('SWITCH_ACCOUNTS' + account.address);
+ const { handleSwitchAccount, getIsActive } = useSwitchAccount(handleClose);
+ const handleClick = useCallback(async () => {
+ setIsLoading();
+ setTimeout(async () => {
+ await handleSwitchAccount(account.index);
+ setIsIdle();
+ }, 80);
+ }, [setIsLoading, setIsIdle, account.index, handleSwitchAccount]);
+ return (
+
+
+
+
+
+ {truncateMiddle(account.address)}
+
+
+
+ {styles => }
+
+
+ {styles => (
+
+ )}
+
+
+ );
+ }
+);
+
+const AccountList: React.FC<{ handleClose: () => void }> = memo(({ handleClose }) => {
+ const accounts = useAccounts();
+ return accounts ? (
+ <>
+ {accounts.map(account => {
+ return (
+
+ );
+ })}
+ >
+ ) : null;
+});
+
export const SwitchAccounts: React.FC = memo(({ close }) => {
- const setAccountDrawerStep = useSetRecoilState(accountDrawerStep);
+ const setAccountDrawerStep = useUpdateAtom(accountDrawerStep);
return (
<>
diff --git a/src/components/drawer/confirm-send-drawer.tsx b/src/components/drawer/confirm-send-drawer.tsx
index 12fb0768..32ca13bc 100644
--- a/src/components/drawer/confirm-send-drawer.tsx
+++ b/src/components/drawer/confirm-send-drawer.tsx
@@ -32,7 +32,7 @@ const TransactionDetails: React.FC<
} & StackProps
> = ({ amount, nonce, fee, recipient, ...rest }) => {
const { ticker } = useSelectedAsset();
- const { stxAddress } = useCurrentAccount();
+ const currentAccount = useCurrentAccount();
const { selectedAsset } = useSelectedAsset();
const gradientString = `${selectedAsset?.contractAddress}.${selectedAsset?.contractName}::${selectedAsset?.name}`;
return (
@@ -49,7 +49,9 @@ const TransactionDetails: React.FC<
icon={selectedAsset?.contractAddress ? gradientString : 'STX'}
ticker={ticker || 'STX'}
title="You will transfer exactly"
- left={stxAddress ? `From ${truncateMiddle(stxAddress)}` : undefined}
+ left={
+ currentAccount?.address ? `From ${truncateMiddle(currentAccount?.address)}` : undefined
+ }
right={`To ${truncateMiddle(recipient)}`}
/>
diff --git a/src/components/drawer/controlled.tsx b/src/components/drawer/controlled.tsx
index 57067d63..81d659d8 100644
--- a/src/components/drawer/controlled.tsx
+++ b/src/components/drawer/controlled.tsx
@@ -1,10 +1,11 @@
import React, { useCallback } from 'react';
-import { RecoilState, useRecoilState } from 'recoil';
-import { BaseDrawer } from '.';
-interface RecoilControlledDrawerProps {
+import { BaseDrawer } from '.';
+import { useAtom, WritableAtom } from 'jotai';
+
+interface ControlledDrawerProps {
/** The Recoil atom used to represent the visibility state of this drawer */
- state: RecoilState;
+ state: WritableAtom;
/** An optional callback that is fired _after_ visibility has been turned off. */
close?: () => void;
title: string;
@@ -14,20 +15,20 @@ interface RecoilControlledDrawerProps {
* `ControlledDrawer` is a wrapper around our `BaseDrawer` component.
* It expects a recoil atom to be used that manages the visibility of this drawer.
*/
-export const ControlledDrawer: React.FC = ({
+export const ControlledDrawer: React.FC = ({
state,
close: _close,
title,
children,
}) => {
- const [showing, setShowing] = useRecoilState(state);
+ const [isShowing, setShowing] = useAtom(state);
const close = useCallback(() => {
setShowing(false);
_close?.();
}, [setShowing, _close]);
return (
-
+
{children}
);
diff --git a/src/components/drawer/networks-drawer.tsx b/src/components/drawer/networks-drawer.tsx
index 5f8ae922..d6e9d7a4 100644
--- a/src/components/drawer/networks-drawer.tsx
+++ b/src/components/drawer/networks-drawer.tsx
@@ -1,5 +1,5 @@
import React, { memo, useCallback } from 'react';
-import { useSetRecoilState } from 'recoil';
+
import { Box, Flex, Button, Stack, color, FlexProps, BoxProps } from '@stacks/ui';
import { ControlledDrawer } from './controlled';
import { useWallet } from '@common/hooks/use-wallet';
@@ -11,11 +11,12 @@ import { showNetworksStore } from '@store/ui';
import { useDrawers } from '@common/hooks/use-drawers';
import { Caption, Title } from '@components/typography';
import { getUrlHostname } from '@common/utils';
+import { useUpdateAtom } from 'jotai/utils';
const NetworkListItem: React.FC<{ item: string } & BoxProps> = memo(({ item, ...props }) => {
const { setShowNetworks } = useDrawers();
const { networks, currentNetworkKey } = useWallet();
- const setCurrentNetworkKey = useSetRecoilState(currentNetworkKeyState);
+ const setCurrentNetworkKey = useUpdateAtom(currentNetworkKeyState);
const network = networks[item];
const delayToShowCheckmarkMotion = 350;
diff --git a/src/components/error-boundary.tsx b/src/components/error-boundary.tsx
index 3a679f69..78c94faa 100644
--- a/src/components/error-boundary.tsx
+++ b/src/components/error-boundary.tsx
@@ -1,13 +1,8 @@
+// @ts-nocheck
+
import React from 'react';
-import { PopupContainer } from '@components/popup/container';
-import { Box, Text, Button } from '@stacks/ui';
+
import { useLoadable } from '@common/hooks/use-loadable';
-import { accountDataState } from '@store/accounts';
-import { walletState } from '@store/wallet';
-import { useRecoilValue } from 'recoil';
-import { Header } from '@components/header';
-import { useTransactionRequest } from '@common/hooks/use-transaction-request';
-import { signedTransactionState } from '@store/transactions';
const openGithubIssue = (loadable: ReturnType) => {
const issueParams = new URLSearchParams();
@@ -53,69 +48,55 @@ type Loadables = ReturnType[];
*
*/
export const ErrorBoundary: React.FC = ({ children }) => {
- const pendingTransaction = useTransactionRequest();
- const wallet = useRecoilValue(walletState);
- let loadables: Loadables = [];
- const walletLoadables: Loadables = [useLoadable(accountDataState)];
- const txLoadables: Loadables = [useLoadable(signedTransactionState)];
-
- if (wallet) {
- loadables = loadables.concat(walletLoadables);
- }
-
- if (pendingTransaction) {
- loadables = loadables.concat(txLoadables);
- }
-
- const errorLoadables = loadables.filter(loadable => !!loadable.errorMaybe());
-
- if (errorLoadables.length > 0) {
- const [loadable] = errorLoadables;
- const error = errorLoadables[0].errorOrThrow();
- return (
- }>
-
-
- {String(error)}
-
-
-
-
- Version:
-
-
- {VERSION}
-
-
-
-
- Branch:
-
-
- {BRANCH}
-
-
-
-
- Commit:
-
-
- {COMMIT_SHA}
-
-
-
-
- If you believe this was caused by a bug in our code, please file an issue on Github.
-
-
-
-
-
-
- );
+ // if (errorLoadables.length > 0) {
+ // const [loadable] = errorLoadables;
+ // const error = errorLoadables[0].errorOrThrow();
+ // return (
+ // }>
+ //
+ //
+ // {String(error)}
+ //
+ //
+ //
+ //
+ // Version:
+ //
+ //
+ // {VERSION}
+ //
+ //
+ //
+ //
+ // Branch:
+ //
+ //
+ // {BRANCH}
+ //
+ //
+ //
+ //
+ // Commit:
+ //
+ //
+ // {COMMIT_SHA}
+ //
+ //
+ //
+ //
+ {
+ /* If you believe this was caused by a bug in our code, please file an issue on Github.*/
}
+ //
+ //
+ //
+ //
+ //
+ //
+ // );
+ // }
return <>{children}>;
};
diff --git a/src/components/home/components/actions.tsx b/src/components/home/components/actions.tsx
new file mode 100644
index 00000000..c2778a8a
--- /dev/null
+++ b/src/components/home/components/actions.tsx
@@ -0,0 +1,74 @@
+import { Box, Button, ButtonProps, Stack, StackProps } from '@stacks/ui';
+import { ScreenPaths } from '@store/common/types';
+import React, { memo, useCallback, useRef } from 'react';
+import { useDoChangeScreen } from '@common/hooks/use-do-change-screen';
+import { IconArrowUp, IconQrcode } from '@tabler/icons';
+import { useTransferableAssets } from '@common/hooks/use-assets';
+
+interface TxButtonProps extends ButtonProps {
+ kind: 'send' | 'receive';
+ path: ScreenPaths.POPUP_SEND | ScreenPaths.POPUP_RECEIVE;
+}
+
+const TxButton: React.FC = memo(({ kind, path, ...rest }) => {
+ const ref = useRef(null);
+ const doChangeScreen = useDoChangeScreen();
+
+ const isSend = kind === 'send';
+
+ const handleClick = useCallback(() => {
+ doChangeScreen(path);
+ }, [path, doChangeScreen]);
+
+ const label = isSend ? 'Send' : 'Receive';
+ return (
+ <>
+
+ >
+ );
+});
+
+const SendButtonSuspense = () => {
+ const assets = useTransferableAssets();
+ const isDisabled = !assets || assets?.length === 0;
+ return ;
+};
+const SendButtonFallback = memo(() => (
+
+));
+
+const SendButton = () => (
+ }>
+
+
+);
+
+export const HomeActions: React.FC = props => {
+ return (
+
+
+
+
+ );
+};
diff --git a/src/components/home/components/user-area.tsx b/src/components/home/components/user-area.tsx
new file mode 100644
index 00000000..4aefd34c
--- /dev/null
+++ b/src/components/home/components/user-area.tsx
@@ -0,0 +1,46 @@
+import { color, Box, Stack, StackProps, useClipboard } from '@stacks/ui';
+import { Caption } from '@components/typography';
+import React, { memo } from 'react';
+import { useCurrentAccount } from '@common/hooks/account/use-current-account';
+import { Tooltip } from '@components/tooltip';
+import { truncateMiddle } from '@stacks/ui-utils';
+import { FiCopy } from 'react-icons/fi';
+import { CurrentUserAvatar } from '@components/current-user/current-user-avatar';
+import { CurrentUsername } from '@components/current-user/current-user-name';
+
+const UserAddress = memo((props: StackProps) => {
+ const currentAccount = useCurrentAccount();
+ const { onCopy, hasCopied } = useClipboard(currentAccount?.address || '');
+ return currentAccount ? (
+
+
+ {truncateMiddle(currentAccount.address)}
+
+
+
+
+ ) : null;
+});
+
+export const UserAccount: React.FC = memo(props => {
+ const currentAccount = useCurrentAccount();
+ if (!currentAccount) {
+ console.error('Error! Homepage rendered without account state, which should never happen.');
+ return null;
+ }
+ return (
+
+
+
+
+
+
+
+ );
+});
diff --git a/src/components/network-mode-badge.tsx b/src/components/network-mode-badge.tsx
index 7aa17e9c..63d94137 100644
--- a/src/components/network-mode-badge.tsx
+++ b/src/components/network-mode-badge.tsx
@@ -1,13 +1,12 @@
import React, { memo, useMemo } from 'react';
import { Box, color, Flex, FlexProps, Text } from '@stacks/ui';
-import { useRecoilValue } from 'recoil';
-import { currentNetworkState } from '@store/networks';
import { ChainID } from '@stacks/transactions';
import { IconFlask } from '@tabler/icons';
import { useDrawers } from '@common/hooks/use-drawers';
+import { useCurrentNetwork } from '@common/hooks/use-current-network';
export const NetworkModeBadge: React.FC = memo(props => {
- const { chainId } = useRecoilValue(currentNetworkState);
+ const { chainId } = useCurrentNetwork();
const isTestnet = useMemo(() => chainId === ChainID.Testnet, [chainId]);
const { setShowNetworks } = useDrawers();
diff --git a/src/components/popup/balances-and-activity.tsx b/src/components/popup/balances-and-activity.tsx
index 81815b96..33a86104 100644
--- a/src/components/popup/balances-and-activity.tsx
+++ b/src/components/popup/balances-and-activity.tsx
@@ -1,5 +1,5 @@
-import React, { useMemo } from 'react';
-import { Box, Stack, SlideFade } from '@stacks/ui';
+import React, { memo, useMemo } from 'react';
+import { Box, Stack, SlideFade, Flex, Spinner, color } from '@stacks/ui';
import type { StackProps } from '@stacks/ui';
import { TokenAssets } from '@components/popup/token-assets';
@@ -26,22 +26,24 @@ function EmptyActivity() {
);
}
-function ActivityList() {
- const { isLoading, value: transactions } = useAccountActivity();
-
+const ActivityList = memo(() => {
+ const transactions = useAccountActivity();
const groupedTxs = useMemo(
() => (transactions ? createTxDateFormatList(transactions) : []),
[transactions]
);
-
- if (isLoading) return null;
-
return !transactions || transactions.length === 0 ? (
) : (
);
-}
+});
+
+const Loading = memo(() => (
+
+
+
+));
export function BalancesAndActivity(props: StackProps) {
const { activeTab, setActiveTab } = useHomeTabs();
@@ -55,20 +57,23 @@ export function BalancesAndActivity(props: StackProps) {
activeTab={activeTab}
onTabClick={setActiveTab}
/>
-
-
- {styles => (
-
- )}
-
-
- {styles => (
-
-
-
- )}
-
-
+
+
+ }>
+
+ {styles => (
+
+ )}
+
+
+ {styles => (
+
+
+
+ )}
+
+
+
);
}
diff --git a/src/components/popup/collectible-assets.tsx b/src/components/popup/collectible-assets.tsx
index 37054e03..83e9d2f5 100644
--- a/src/components/popup/collectible-assets.tsx
+++ b/src/components/popup/collectible-assets.tsx
@@ -8,9 +8,9 @@ import { Stack } from '@stacks/ui';
export const CollectibleAssets = memo((props: StackProps) => {
const accountData = useFetchAccountData();
- if (!accountData.value) return null;
+ if (!accountData) return null;
- const balances = accountData.value.balances;
+ const balances = accountData.balances;
const noCollectibles = Object.keys(balances.non_fungible_tokens).length === 0;
if (noCollectibles) return null;
diff --git a/src/components/popup/container.tsx b/src/components/popup/container.tsx
index 12eda38a..33437e38 100644
--- a/src/components/popup/container.tsx
+++ b/src/components/popup/container.tsx
@@ -1,6 +1,5 @@
-import React, { memo, useCallback, useEffect } from 'react';
-import { Flex, color, Spinner } from '@stacks/ui';
-import { SettingsPopover } from './settings-popover';
+import React, { useCallback, useEffect } from 'react';
+import { Flex, color } from '@stacks/ui';
import { useWallet } from '@common/hooks/use-wallet';
import { useOnCancel } from '@common/hooks/use-on-cancel';
import { usePendingTransaction } from '@pages/transaction-signing/hooks/use-pending-transaction';
@@ -8,40 +7,51 @@ import { useAuthRequest } from '@common/hooks/auth/use-auth-request';
interface PopupHomeProps {
header?: any;
- requestType?: string;
+ // TODO: remove the need for prop drilling this
+ requestType?: 'transaction' | 'auth';
}
-const Loading = memo(() => (
-
-
-
-));
+const UnmountEffectSuspense = ({
+ requestType,
+}: {
+ requestType?: PopupHomeProps['requestType'];
+}) => {
+ const pendingTx = usePendingTransaction();
+ const { authRequest } = useAuthRequest();
+ const handleCancelTransaction = useOnCancel();
+ const { handleCancelAuthentication } = useWallet();
-export const PopupContainer: React.FC = memo(
- ({ children, header, requestType }) => {
- const pendingTx = usePendingTransaction();
- const { authRequest } = useAuthRequest();
- const handleCancelTransaction = useOnCancel();
- const { handleCancelAuthentication, hasRehydratedVault } = useWallet();
+ /*
+ * When the popup is closed, this checks the requestType and forces
+ * the request promise to fail; triggering an onCancel callback function.
+ */
+ const handleUnmount = useCallback(async () => {
+ if (requestType === 'transaction' || !!pendingTx) {
+ await handleCancelTransaction();
+ } else if (requestType === 'auth' || !!authRequest) {
+ handleCancelAuthentication();
+ }
+ }, [requestType, handleCancelAuthentication, authRequest, pendingTx, handleCancelTransaction]);
- /*
- * When the popup is closed, this checks the requestType and forces
- * the request promise to fail; triggering an onCancel callback function.
- */
- const handleUnmount = useCallback(async () => {
- if (requestType === 'transaction' || !!pendingTx) {
- await handleCancelTransaction();
- } else if (requestType === 'auth' || !!authRequest) {
- handleCancelAuthentication();
- }
- }, [requestType, handleCancelAuthentication, authRequest, pendingTx, handleCancelTransaction]);
+ useEffect(() => {
+ window.addEventListener('beforeunload', handleUnmount);
+ return () => window.removeEventListener('beforeunload', handleUnmount);
+ }, [handleUnmount]);
- useEffect(() => {
- window.addEventListener('beforeunload', handleUnmount);
- return () => window.removeEventListener('beforeunload', handleUnmount);
- }, [handleUnmount]);
+ return null;
+};
- return (
+const UnmountEffect = ({ requestType }: { requestType?: PopupHomeProps['requestType'] }) => (
+ >}>
+
+
+);
+
+export const PopupContainer: React.FC = ({ children, header, requestType }) => {
+ const { hasRehydratedVault } = useWallet();
+ return hasRehydratedVault ? (
+ <>
+
= memo(
position="relative"
overflow="auto"
>
- {header && header}
-
+ {header || null}
= memo(
px="loose"
pb="loose"
>
- {hasRehydratedVault ? children : }
+ {children}
- );
- }
-);
+ >
+ ) : null;
+};
diff --git a/src/components/popup/token-assets.tsx b/src/components/popup/token-assets.tsx
index d27b851f..c11f3ced 100644
--- a/src/components/popup/token-assets.tsx
+++ b/src/components/popup/token-assets.tsx
@@ -1,50 +1,34 @@
import React, { memo } from 'react';
-import { Box, Stack, StackProps, Circle, color, Button, useClipboard } from '@stacks/ui';
+import { Stack, StackProps, color, Button, useClipboard } from '@stacks/ui';
import { AssetRow } from '../asset-row';
import { useFungibleTokenState, useStxTokenState } from '@common/hooks/use-assets';
import { Caption } from '@components/typography';
-import { SpaceBetween } from '@components/space-between';
import { CollectibleAssets } from '@components/popup/collectible-assets';
import { useAccountBalances } from '@common/hooks/account/use-account-balances';
import { NoAssetsEmptyIllustration } from '@components/vector/no-assets';
import { useCurrentAccount } from '@common/hooks/account/use-current-account';
-const LoadingAssetRowItem = memo((props: StackProps) => (
-
-
-
-
-
-
-
-
-
-
-));
-
function FungibleAssets(props: StackProps) {
const fungibleTokens = useFungibleTokenState();
const balances = useAccountBalances();
if (!balances) return null;
- const fungibleTokensLoading = !fungibleTokens.value && fungibleTokens.state === 'loading';
-
const ftCount = Object.keys(balances.fungible_tokens);
const noTokens = ftCount.length === 0;
- if (noTokens) return null;
+ if (noTokens || !fungibleTokens) return null;
return (
- {fungibleTokensLoading
- ? ftCount.map(item => )
- : fungibleTokens?.value?.map(asset => )}
+ {fungibleTokens?.map(asset => (
+
+ ))}
);
}
function NoAssets(props: StackProps) {
- const { stxAddress } = useCurrentAccount();
- const { onCopy, hasCopied } = useClipboard(stxAddress || '');
+ const currentAccount = useCurrentAccount();
+ const { onCopy, hasCopied } = useClipboard(currentAccount?.address || '');
return (
= memo(({ ...props }) => {
if (!balances) return null;
const noAssets =
- !stxTokens.value &&
+ !stxTokens &&
Object.keys(balances.fungible_tokens).length === 0 &&
Object.keys(balances.non_fungible_tokens).length === 0;
@@ -84,7 +68,7 @@ export const TokenAssets: React.FC = memo(({ ...props }) => {
) : (
- {stxTokens.value && }
+ {stxTokens && }
diff --git a/src/components/send/amount-field.tsx b/src/components/send/amount-field.tsx
index a57a432b..1660bceb 100644
--- a/src/components/send/amount-field.tsx
+++ b/src/components/send/amount-field.tsx
@@ -45,14 +45,14 @@ export const AmountField = memo(
width="100%"
placeholder={placeholder || 'Select an asset first'}
min="0"
- autoFocus={assets.value?.length === 1}
+ autoFocus={assets?.length === 1}
value={value === 0 ? '' : value}
onKeyDown={handleOnKeyDown}
onChange={onChange}
autoComplete="off"
name="amount"
/>
- {balances.value && selectedAsset ? (
+ {balances && selectedAsset ? (
{
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const doChangeScreen = useDoChangeScreen();
- const setNetworks = useSetRecoilState(networksState);
- const setNetworkKey = useSetRecoilState(currentNetworkKeyState);
+ const setNetworks = useUpdateAtom(networksState);
+ const setNetworkKey = useUpdateAtom(currentNetworkKeyState);
return (
= memo(({ onClick }) => {
- const { currentAccountStxAddress } = useWallet();
- const { onCopy, hasCopied } = useClipboard(currentAccountStxAddress || '');
- return (
- <>
- {
- onCopy();
- onClick?.(event);
- }}
- position="absolute"
- right={0}
- zIndex={3}
- >
-
-
-
-
-
-
- >
- );
-});
-
-const TxButton: React.FC = memo(({ kind, path, ...rest }) => {
- const ref = useRef(null);
- const doChangeScreen = useDoChangeScreen();
- const assets = useAssets();
-
- const isSend = kind === 'send';
- const sendDisabled = !assets.value || (isSend && assets.value?.length === 0);
-
- const handleClick = useCallback(() => {
- doChangeScreen(path);
- }, [path, doChangeScreen]);
-
- const handleFocus = useCallback(() => {
- ref?.current?.focus();
- }, [ref]);
-
- const label = isSend ? 'Send' : 'Receive';
- return (
- <>
-
- >
- );
-});
-
-const UserAccount: React.FC = memo(props => {
- const names = useAccountNames();
- const { currentAccount, currentAccountStxAddress } = useWallet();
- if (!currentAccount || !currentAccountStxAddress) {
- console.error('Error! Homepage rendered without account state, which should never happen.');
- return null;
- }
- const nameCharLimit = 18;
- const name =
- names?.value?.[currentAccount.index]?.names?.[0] || getAccountDisplayName(currentAccount);
- const isLong = name.length > nameCharLimit;
- const displayName = truncateString(name, nameCharLimit);
-
- return (
-
-
-
-
-
-
- {displayName}
-
-
-
- {truncateMiddle(currentAccountStxAddress, 8)}
-
-
- );
-});
-
-const Actions: React.FC = memo(props => {
- return (
-
-
-
-
- );
-});
-
-const PageTop: React.FC = memo(props => (
+const PageTop: React.FC = props => (
-
+
-));
+);
-export const PopupHome: React.FC = memo(() => (
+export const PopupHome: React.FC = () => (
} requestType="auth">
- ;
+
-));
+);
diff --git a/src/pages/send-tokens/send-tokens.tsx b/src/pages/send-tokens/send-tokens.tsx
index d97d7b15..ecd43c39 100644
--- a/src/pages/send-tokens/send-tokens.tsx
+++ b/src/pages/send-tokens/send-tokens.tsx
@@ -12,7 +12,6 @@ import { AssetSearch } from '@components/asset-search/asset-search';
import { Header } from '@components/header';
import { useSelectedAsset } from '@common/hooks/use-selected-asset';
-import { useRevalidateApi } from '@common/hooks/use-revalidate-api';
import { useSendFormValidation } from '@common/hooks/use-send-form-validation';
import { AmountField } from '@components/send/amount-field';
import { RecipientField } from '@components/send/recipient-field';
@@ -40,7 +39,6 @@ const Form = ({
setAssetError: (error: string | undefined) => void;
} & FormikProps) => {
const doChangeScreen = useDoChangeScreen();
- const revalidate = useRevalidateApi();
const { selectedAsset } = useSelectedAsset();
const onChange = useCallback(
@@ -53,10 +51,9 @@ const Form = ({
const onSubmit = useCallback(async () => {
if (values.amount && values.recipient && selectedAsset) {
- await revalidate(); // we want up to date data
handleSubmit();
}
- }, [revalidate, handleSubmit, values, selectedAsset]);
+ }, [handleSubmit, values, selectedAsset]);
const onItemClick = useCallback(() => {
setValues({ ...values, amount: '' });
diff --git a/src/pages/sign-up/create.tsx b/src/pages/sign-up/create.tsx
index c0b1e3fb..de96d80a 100644
--- a/src/pages/sign-up/create.tsx
+++ b/src/pages/sign-up/create.tsx
@@ -5,8 +5,9 @@ import { Screen, ScreenBody, PoweredBy, ScreenFooter, ScreenHeader } from '@scre
import { useAppDetails } from '@common/hooks/auth/use-app-details';
import { useWallet } from '@common/hooks/use-wallet';
-import { useSetRecoilState } from 'recoil';
+
import { onboardingProgressState } from '@store/onboarding';
+import { useUpdateAtom } from 'jotai/utils';
interface ExplainerCardProps {
title: string;
@@ -57,7 +58,7 @@ export const Create: React.FC = props => {
const [cardIndex, setCardIndex] = useState(0);
const { wallet, doMakeWallet } = useWallet();
const { name } = useAppDetails();
- const doSetOnboardingProgress = useSetRecoilState(onboardingProgressState);
+ const doSetOnboardingProgress = useUpdateAtom(onboardingProgressState);
const { next } = props;
const explainerData: ExplainerCardProps[] = useMemo(
diff --git a/src/pages/transaction-signing/components/actions.tsx b/src/pages/transaction-signing/components/actions.tsx
index ef32195e..cb779b5c 100644
--- a/src/pages/transaction-signing/components/actions.tsx
+++ b/src/pages/transaction-signing/components/actions.tsx
@@ -1,21 +1,22 @@
import React, { memo, useCallback } from 'react';
-import { Box, Button, color, Stack, StackProps } from '@stacks/ui';
+import { Box, Button, ButtonProps, color, Stack, StackProps } from '@stacks/ui';
import { LOADING_KEYS, useLoading } from '@common/hooks/use-loading';
import { SpaceBetween } from '@components/space-between';
import { Caption } from '@components/typography';
import { NetworkRowItem } from '@components/network-row-item';
import { FeeComponent } from '@pages/transaction-signing/components/fee';
import { FiAlertTriangle } from 'react-icons/fi';
-import { useRecoilValue } from 'recoil';
+
import { useTransactionBroadcast } from '@pages/transaction-signing/hooks/use-transaction-broadcast';
import { transactionBroadcastErrorState } from '@store/transactions';
import { useTransactionError } from '../hooks/use-transaction-error';
-import { useSignedTransaction } from '../hooks/use-transaction';
import { TransactionErrorReason } from './transaction-error';
+import { useAtomValue } from 'jotai/utils';
+import { LoadingRectangle } from '@components/loading-rectangle';
-const MinimalErrorMessage = memo((props: StackProps) => {
+const MinimalErrorMessageSuspense = memo((props: StackProps) => {
const error = useTransactionError();
- const broadcastError = useRecoilValue(transactionBroadcastErrorState);
+ const broadcastError = useAtomValue(transactionBroadcastErrorState);
if (!error) return null;
@@ -45,13 +46,26 @@ const MinimalErrorMessage = memo((props: StackProps) => {
);
});
-export const TransactionsActions = memo((props: StackProps) => {
+export const MinimalErrorMessage = memo((props: StackProps) => {
+ return (
+ >}>
+
+
+ );
+});
+
+const BaseConfirmButton = (props: ButtonProps) => (
+
+);
+
+const SubmitActionSuspense = (props: ButtonProps) => {
const handleBroadcastTransaction = useTransactionBroadcast();
- const signedTransaction = useSignedTransaction();
const error = useTransactionError();
const { setIsLoading, setIsIdle, isLoading } = useLoading(LOADING_KEYS.SUBMIT_TRANSACTION);
-
- const isDisabled = !!error || !signedTransaction.value;
+ //
+ const isDisabled = !!error;
const handleSubmit = useCallback(async () => {
setIsLoading();
@@ -60,30 +74,61 @@ export const TransactionsActions = memo((props: StackProps) => {
}, [setIsLoading, setIsIdle, handleBroadcastTransaction]);
return (
-
-
-
- Fees
-
-
-
-
-
- Network
-
-
-
-
-
-
+
+ Confirm
+
);
-});
+};
+const SubmitAction = (props: ButtonProps) => {
+ return (
+ }>
+
+
+ );
+};
+
+const FeeRowItemSuspense = () => {
+ return (
+
+ Fees
+
+
+
+
+ );
+};
+const FeeRowItemFallback = () => {
+ return (
+
+ Fees
+
+
+ );
+};
+
+const FeeRowItem = () => {
+ return (
+ }>
+
+
+ );
+};
+
+export const TransactionsActions = () => {
+ return (
+ <>
+
+
+ Network
+
+
+
+
+ >
+ );
+};
diff --git a/src/pages/transaction-signing/components/asset-item.tsx b/src/pages/transaction-signing/components/asset-item.tsx
index cefb311c..50c27dc4 100644
--- a/src/pages/transaction-signing/components/asset-item.tsx
+++ b/src/pages/transaction-signing/components/asset-item.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { Stack, StackProps, Text } from '@stacks/ui';
import { AssetAvatar } from '@components/stx-avatar';
+import { SpaceBetween } from '@components/space-between';
export const AssetItem: React.FC<
StackProps & {
@@ -10,14 +11,16 @@ export const AssetItem: React.FC<
}
> = ({ iconString, amount, ticker, ...rest }) => {
return (
-
-
+
+
+
+
+ {ticker}
+
+
- {ticker}
-
-
{amount}
-
+
);
};
diff --git a/src/pages/transaction-signing/components/contract-call-details.tsx b/src/pages/transaction-signing/components/contract-call-details.tsx
index b33b7724..edfd56ed 100644
--- a/src/pages/transaction-signing/components/contract-call-details.tsx
+++ b/src/pages/transaction-signing/components/contract-call-details.tsx
@@ -7,11 +7,10 @@ import { useExplorerLink } from '@common/hooks/use-explorer-link';
import { Caption, Title } from '@components/typography';
import { ContractPreview } from '@pages/transaction-signing/components/contract-preview';
import { useTransactionRequest } from '@common/hooks/use-transaction-request';
-import { useLoadable } from '@common/hooks/use-loadable';
-import { transactionFunctionsState } from '@store/transactions/contract-call';
import { AttachmentRow } from './attachment-row';
import { RowItem } from './row-item';
+import { useTransactionFunction } from '@pages/transaction-signing/hooks/use-transaction';
interface ArgumentProps {
arg: string;
@@ -19,10 +18,10 @@ interface ArgumentProps {
}
const FunctionArgumentRow: React.FC = memo(({ arg, index, ...rest }) => {
- const payload = useLoadable(transactionFunctionsState);
+ const txFunction = useTransactionFunction();
const argCV = deserializeCV(Buffer.from(arg, 'hex'));
const strValue = cvToString(argCV);
- const name = payload.value?.args[index].name || null;
+ const name = txFunction?.args[index].name || null;
return ;
});
@@ -35,19 +34,25 @@ const FunctionArgumentsList = memo((props: StackProps) => {
}
const hasArgs = transactionRequest.functionArgs.length > 0;
return (
- } spacing="base" {...props}>
+ <>
{hasArgs ? (
- transactionRequest.functionArgs.map((arg, index) => {
- return ;
- })
+ } spacing="base" {...props}>
+ {transactionRequest.functionArgs.map((arg, index) => {
+ return (
+ loading>}>
+
+
+ );
+ })}
+
) : (
There are no additional arguments passed for this function call.
)}
-
+ >
);
});
-export const ContractCallDetails = memo(() => {
+export const ContractCallDetailsSuspense = () => {
const transactionRequest = useTransactionRequest();
const { handleOpenTxLink } = useExplorerLink();
if (!transactionRequest || transactionRequest.txType !== 'contract_call') return null;
@@ -78,4 +83,10 @@ export const ContractCallDetails = memo(() => {
);
-});
+};
+
+export const ContractCallDetails = () => (
+ >}>
+
+
+);
diff --git a/src/pages/transaction-signing/components/fee.tsx b/src/pages/transaction-signing/components/fee.tsx
index 392be342..2a60ee74 100644
--- a/src/pages/transaction-signing/components/fee.tsx
+++ b/src/pages/transaction-signing/components/fee.tsx
@@ -1,11 +1,9 @@
-import React, { memo } from 'react';
-import { LoadingRectangle } from '@components/loading-rectangle';
+import React from 'react';
import { stacksValue } from '@common/stacks-utils';
import { useTransactionFee } from '@pages/transaction-signing/hooks/use-transaction-fee';
-export const FeeComponent = memo(() => {
- const { isLoading, isSponsored, amount } = useTransactionFee();
- if (isLoading) return ;
+export const FeeComponent = () => {
+ const { isSponsored, amount } = useTransactionFee();
if (typeof amount === 'undefined') return null;
return (
<>
@@ -17,4 +15,4 @@ export const FeeComponent = memo(() => {
})}
>
);
-});
+};
diff --git a/src/pages/transaction-signing/components/popup-header.tsx b/src/pages/transaction-signing/components/popup-header.tsx
index 97bfe5b3..1442a488 100644
--- a/src/pages/transaction-signing/components/popup-header.tsx
+++ b/src/pages/transaction-signing/components/popup-header.tsx
@@ -1,52 +1,61 @@
-import { useWallet } from '@common/hooks/use-wallet';
-import { useFetchBalances } from '@common/hooks/account/use-account-info';
-import { Box, color, Stack, Text } from '@stacks/ui';
-import { AccountAvatar } from '@components/account-avatar';
-import { Caption, Title } from '@components/typography';
-import { truncateMiddle } from '@stacks/ui-utils';
-import { stacksValue } from '@common/stacks-utils';
-import { LoadingRectangle } from '@components/loading-rectangle';
-import React from 'react';
-import { useAccountDisplayName } from '@common/hooks/account/use-account-names';
+import { Box, color, Stack } from '@stacks/ui';
+import { Caption } from '@components/typography';
-export function PopupHeader() {
- const { currentAccount, currentAccountStxAddress } = useWallet();
- const balances = useFetchBalances();
- const displayName = useAccountDisplayName();
- if (!currentAccount || !currentAccountStxAddress) return null;
+import { stacksValue } from '@common/stacks-utils';
+import React from 'react';
+
+import { CurrentUserAvatar } from '@components/current-user/current-user-avatar';
+import { CurrentUsername } from '@components/current-user/current-user-name';
+import { CurrentStxAddress } from '@components/current-user/current-stx-address';
+import { useAccountInfo } from '@common/hooks/account/use-account-balances';
+import { LoadingRectangle } from '@components/loading-rectangle';
+
+const Balance = () => {
+ const info = useAccountInfo();
+ return info?.balance ? (
+
+ {stacksValue({
+ value: info.balance.toString(10),
+ withTicker: true,
+ })}
+
+ ) : null;
+};
+
+export function PopupHeaderFallback() {
return (
-
- {displayName}
-
- {truncateMiddle(currentAccountStxAddress, 4)}
-
+
+
+
-
- {balances.value ? (
-
- {stacksValue({
- value: balances.value.stx.balance,
- withTicker: true,
- abbreviate: true,
- })}
-
- ) : (
-
- )}
-
+
);
}
+
+export function PopupHeaderSuspense() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export const PopupHeader = () => {
+ return (
+ }>
+
+
+ );
+};
diff --git a/src/pages/transaction-signing/components/post-conditions/list.tsx b/src/pages/transaction-signing/components/post-conditions/list.tsx
index 53f6b276..1aa04475 100644
--- a/src/pages/transaction-signing/components/post-conditions/list.tsx
+++ b/src/pages/transaction-signing/components/post-conditions/list.tsx
@@ -1,17 +1,17 @@
-import React, { memo, useMemo } from 'react';
+import React, { useMemo } from 'react';
import { Box, Circle, color, Flex, Stack } from '@stacks/ui';
-import { useRecoilValue } from 'recoil';
+
import { TransactionTypes } from '@stacks/connect';
import { stacksValue } from '@common/stacks-utils';
import { IconLock } from '@tabler/icons';
import { Body } from '@components/typography';
import { truncateMiddle } from '@stacks/ui-utils';
-import { useLoadable } from '@common/hooks/use-loadable';
import { postConditionsState } from '@store/transactions';
-import { useTransactionRequest } from '../../../../common/hooks/use-transaction-request';
+import { useTransactionRequest } from '@common/hooks/use-transaction-request';
import { TransactionEventCard } from '../event-card';
import { PostConditionComponent } from './single';
+import { useAtomValue } from 'jotai/utils';
function StxPostcondition() {
const pendingTransaction = useTransactionRequest();
@@ -49,24 +49,24 @@ function NoPostconditions() {
);
}
-const PostConditionsList = memo(() => {
- const postConditions = useRecoilValue(postConditionsState);
+const PostConditionsList = () => {
+ const postConditions = useAtomValue(postConditionsState);
return (
<>
{postConditions?.map((pc, index) => (
))}
>
);
-});
+};
-export const PostConditions: React.FC = memo(() => {
- const { value: postConditions } = useLoadable(postConditionsState);
+export const PostConditionsSuspense: React.FC = () => {
+ const postConditions = useAtomValue(postConditionsState);
const pendingTransaction = useTransactionRequest();
const hasPostConditions = useMemo(
() => postConditions && postConditions?.length > 0,
@@ -84,7 +84,6 @@ export const PostConditions: React.FC = memo(() => {
borderRadius="12px"
width="100%"
flexDirection="column"
- my="loose"
>
{hasPostConditions ? (
@@ -95,4 +94,12 @@ export const PostConditions: React.FC = memo(() => {
)}
);
-});
+};
+
+export const PostConditions = () => {
+ return (
+ >}>
+
+
+ );
+};
diff --git a/src/pages/transaction-signing/components/post-conditions/single.tsx b/src/pages/transaction-signing/components/post-conditions/single.tsx
index efd98e51..1ca5eda7 100644
--- a/src/pages/transaction-signing/components/post-conditions/single.tsx
+++ b/src/pages/transaction-signing/components/post-conditions/single.tsx
@@ -8,6 +8,7 @@ import { useCurrentAccount } from '@common/hooks/account/use-current-account';
import {
getAmountFromPostCondition,
getIconStringFromPostCondition,
+ getNameFromPostCondition,
getPostConditionCodeMessage,
getPostConditionTitle,
getSymbolFromPostCondition,
@@ -21,16 +22,63 @@ interface PostConditionProps {
isLast?: boolean;
}
-export const PostConditionComponent: React.FC = ({ pc, isLast }) => {
- const { stxAddress } = useCurrentAccount();
+export const PostConditionFallbackComponent: React.FC = ({ pc, isLast }) => {
+ const currentAccount = useCurrentAccount();
+ const pendingTransaction = useTransactionRequest();
+ const title = getPostConditionTitle(pc);
+ const iconString = getIconStringFromPostCondition(pc);
+ const ticker = getSymbolFromPostCondition(pc);
+ const amount = getAmountFromPostCondition(pc);
+ const name = getNameFromPostCondition(pc);
+ const address = addressToString(pc.principal.address);
+ const isSending = address === currentAccount?.address;
+
+ const isContractPrincipal =
+ (pendingTransaction?.txType == TransactionTypes.ContractCall &&
+ pendingTransaction.contractAddress === address) ||
+ address.includes('.');
+
+ if (!pendingTransaction) return null;
+
+ const message = pc.conditionCode
+ ? `${getPostConditionCodeMessage(
+ pc.conditionCode,
+ isSending
+ )} ${amount} ${ticker} or the transaction will abort.`
+ : undefined;
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export const PostConditionComponentSuspense: React.FC = ({ pc, isLast }) => {
+ const currentAccount = useCurrentAccount();
const asset = useAssetInfoFromPostCondition(pc);
const pendingTransaction = useTransactionRequest();
const title = getPostConditionTitle(pc);
const iconString = getIconStringFromPostCondition(pc);
const _ticker = getSymbolFromPostCondition(pc);
const _amount = getAmountFromPostCondition(pc);
+ const name = getNameFromPostCondition(pc);
const address = addressToString(pc.principal.address);
- const isSending = address === stxAddress;
+ const isSending = address === currentAccount?.address;
const amount =
typeof asset?.meta?.decimals === 'number' ? ftDecimals(_amount, asset.meta.decimals) : _amount;
@@ -48,7 +96,6 @@ export const PostConditionComponent: React.FC = ({ pc, isLas
? `${getPostConditionCodeMessage(
pc.conditionCode,
isSending
- // TODO: fetch asset info in SIP 10 branch
)} ${amount} ${ticker} or the transaction will abort.`
: undefined;
@@ -58,7 +105,7 @@ export const PostConditionComponent: React.FC = ({ pc, isLas
title={`${
isContractPrincipal ? 'The contract ' : isSending ? 'You ' : 'Another address '
} ${title}`}
- left={asset?.meta?.name}
+ left={asset?.meta?.name || name}
right={`${isSending ? 'From' : 'To'} ${truncateMiddle(
addressToString(pc.principal.address),
4
@@ -72,3 +119,11 @@ export const PostConditionComponent: React.FC = ({ pc, isLas
>
);
};
+
+export const PostConditionComponent = (props: PostConditionProps) => {
+ return (
+ }>
+
+
+ );
+};
diff --git a/src/pages/transaction-signing/components/transaction-error.tsx b/src/pages/transaction-signing/components/transaction-error.tsx
index d008c1ca..11ff01d2 100644
--- a/src/pages/transaction-signing/components/transaction-error.tsx
+++ b/src/pages/transaction-signing/components/transaction-error.tsx
@@ -19,7 +19,7 @@ export enum TransactionErrorReason {
ExpiredRequest = 7,
}
-export const TransactionError = memo(() => {
+export const TransactionErrorSuspense = memo(() => {
const reason = useTransactionError();
if (!reason) return null;
switch (reason) {
@@ -39,3 +39,11 @@ export const TransactionError = memo(() => {
return null;
}
});
+
+export const TransactionError = memo(() => {
+ return (
+ >}>
+
+
+ );
+});
diff --git a/src/pages/transaction-signing/components/transaction-errors.tsx b/src/pages/transaction-signing/components/transaction-errors.tsx
index e9ee74cd..0ac0e781 100644
--- a/src/pages/transaction-signing/components/transaction-errors.tsx
+++ b/src/pages/transaction-signing/components/transaction-errors.tsx
@@ -11,14 +11,15 @@ import { useCurrentNetwork } from '@common/hooks/use-current-network';
import { truncateMiddle } from '@stacks/ui-utils';
import { ErrorMessage } from '@pages/transaction-signing/components/error';
import { useDrawers } from '@common/hooks/use-drawers';
-import { useRecoilValue } from 'recoil';
+
import { transactionBroadcastErrorState } from '@store/transactions';
import { useScrollLock } from '@common/hooks/use-scroll-lock';
+import { useAtomValue } from 'jotai/utils';
export const FeeInsufficientFundsErrorMessage = memo(props => {
const currentAccount = useCurrentAccount();
const { setShowAccounts } = useDrawers();
- const { onCopy, hasCopied } = useClipboard(currentAccount.address || '');
+ const { onCopy, hasCopied } = useClipboard(currentAccount?.address || '');
return (
{
const balances = useFetchBalances();
const currentAccount = useCurrentAccount();
const { setShowAccounts } = useDrawers();
- const { onCopy, hasCopied } = useClipboard(currentAccount.address || '');
+ const { onCopy, hasCopied } = useClipboard(currentAccount?.address || '');
return (
{
Current balance
- {balances?.value?.stx?.balance
+ {balances?.stx?.balance
? stacksValue({
- value: balances?.value?.stx?.balance,
+ value: balances?.stx?.balance,
withTicker: true,
})
: '--'}
@@ -175,7 +176,7 @@ export const ExpiredRequestErrorMessage = memo(props => {
});
export const BroadcastErrorMessage = memo(props => {
- const broadcastError = useRecoilValue(transactionBroadcastErrorState);
+ const broadcastError = useAtomValue(transactionBroadcastErrorState);
if (!broadcastError) return null;
return (
{
const pageTitle = useTransactionPageTitle();
const network = useCurrentNetwork();
if (!transactionRequest) return null;
-
const appName = transactionRequest?.appDetails?.name;
-
- const testnetAddition = network.isTestnet ? (
- <>
- {' '}
- on
- {getUrlHostname(network.url)}
- >
- ) : null;
+ const originAddition = origin ? ` (${getUrlHostname(origin)})` : '';
+ const testnetAddition = network.isTestnet ? ` using ${getUrlHostname(network.url)}` : '';
+ const caption = appName ? `Requested by "${appName}"${originAddition}${testnetAddition}` : null;
return (
{pageTitle}
- {appName ? (
-
- Requested by {appName} {origin ? `(${getUrlHostname(origin)})` : null}
- {testnetAddition}
-
- ) : null}
+ {caption && {caption}}
);
});
diff --git a/src/pages/transaction-signing/hooks/use-asset-transfer.ts b/src/pages/transaction-signing/hooks/use-asset-transfer.ts
index c5ddd957..f200fe9f 100644
--- a/src/pages/transaction-signing/hooks/use-asset-transfer.ts
+++ b/src/pages/transaction-signing/hooks/use-asset-transfer.ts
@@ -10,14 +10,16 @@ import {
noneCV,
PostCondition,
someCV,
+ StacksTransaction,
standardPrincipalCVFromAddress,
uintCV,
} from '@stacks/transactions';
import BN from 'bn.js';
-import { useRecoilCallback } from 'recoil';
import { makeFungibleTokenTransferState } from '@store/transactions/fungible-token-transfer';
import { selectedAssetStore } from '@store/assets/asset-search';
import { ftUnshiftDecimals } from '@common/stacks-utils';
+import { useAtomCallback } from 'jotai/utils';
+import { useCallback } from 'react';
interface PostConditionsOptions {
contractAddress: string;
@@ -39,67 +41,76 @@ function makePostCondition(options: PostConditionsOptions): PostCondition {
);
}
-export function useMakeAssetTransfer() {
- return useRecoilCallback(({ snapshot }) => async ({ amount, recipient, memo }) => {
- const assetTransferState = await snapshot.getPromise(makeFungibleTokenTransferState);
- const selectedAsset = await snapshot.getPromise(selectedAssetStore);
- if (!assetTransferState || !selectedAsset) return;
- const {
- balances,
- network,
- senderKey,
- assetName,
- contractAddress,
- contractName,
- nonce,
- stxAddress,
- } = assetTransferState;
-
- const functionName = 'transfer';
-
- const tokenBalanceKey = Object.keys(balances?.fungible_tokens || {}).find(contract => {
- return contract.startsWith(contractAddress);
- });
-
- const realAmount =
- selectedAsset.type === 'ft'
- ? ftUnshiftDecimals(amount, selectedAsset?.meta?.decimals || 0)
- : amount;
-
- const postConditionOptions = tokenBalanceKey
- ? {
- contractAddress,
- contractName,
- assetName,
- stxAddress,
- amount: realAmount,
- }
- : undefined;
-
- const postConditions = postConditionOptions ? [makePostCondition(postConditionOptions)] : [];
-
- // (transfer (uint principal principal) (response bool uint))
- const functionArgs: ClarityValue[] = [
- uintCV(realAmount),
- standardPrincipalCVFromAddress(createAddress(stxAddress)),
- standardPrincipalCVFromAddress(createAddress(recipient)),
- ];
-
- if (selectedAsset.hasMemo) {
- functionArgs.push(memo !== '' ? someCV(bufferCVFromString(memo)) : noneCV());
- }
- const txOptions = {
- network,
- functionName,
- functionArgs,
- senderKey,
- contractAddress,
- contractName,
- postConditions,
- nonce: new BN(nonce, 10),
- anchorMode: AnchorMode.Any,
- };
-
- return makeContractCall(txOptions);
- });
+interface AssetTransferOptions {
+ amount: number;
+ recipient: string;
+ memo: string;
+}
+
+export function useMakeAssetTransfer() {
+ return useAtomCallback(
+ useCallback(async (get, _set, arg) => {
+ const { amount, recipient, memo } = arg;
+ const assetTransferState = get(makeFungibleTokenTransferState);
+ const selectedAsset = get(selectedAssetStore);
+ if (!assetTransferState || !selectedAsset) return;
+ const {
+ balances,
+ network,
+ senderKey,
+ assetName,
+ contractAddress,
+ contractName,
+ nonce,
+ stxAddress,
+ } = assetTransferState;
+
+ const functionName = 'transfer';
+
+ const tokenBalanceKey = Object.keys(balances?.fungible_tokens || {}).find(contract => {
+ return contract.startsWith(contractAddress);
+ });
+
+ const realAmount =
+ selectedAsset.type === 'ft'
+ ? ftUnshiftDecimals(amount, selectedAsset?.meta?.decimals || 0)
+ : amount;
+
+ const postConditionOptions = tokenBalanceKey
+ ? {
+ contractAddress,
+ contractName,
+ assetName,
+ stxAddress,
+ amount: realAmount,
+ }
+ : undefined;
+
+ const postConditions = postConditionOptions ? [makePostCondition(postConditionOptions)] : [];
+
+ // (transfer (uint principal principal) (response bool uint))
+ const functionArgs: ClarityValue[] = [
+ uintCV(realAmount),
+ standardPrincipalCVFromAddress(createAddress(stxAddress)),
+ standardPrincipalCVFromAddress(createAddress(recipient)),
+ ];
+
+ if (selectedAsset.hasMemo) {
+ functionArgs.push(memo !== '' ? someCV(bufferCVFromString(memo)) : noneCV());
+ }
+ const txOptions = {
+ network,
+ functionName,
+ functionArgs,
+ senderKey,
+ contractAddress,
+ contractName,
+ postConditions,
+ nonce: new BN(nonce, 10),
+ anchorMode: AnchorMode.Any,
+ };
+
+ return makeContractCall(txOptions);
+ }, [])
+ );
}
diff --git a/src/pages/transaction-signing/hooks/use-make-stx-transfer.ts b/src/pages/transaction-signing/hooks/use-make-stx-transfer.ts
index 9cf731b3..e833a840 100644
--- a/src/pages/transaction-signing/hooks/use-make-stx-transfer.ts
+++ b/src/pages/transaction-signing/hooks/use-make-stx-transfer.ts
@@ -1,5 +1,4 @@
-import { useRecoilCallback, waitForAll } from 'recoil';
-
+import { useAtomCallback, waitForAll } from 'jotai/utils';
import { currentStacksNetworkState } from '@store/networks';
import { currentAccountState } from '@store/accounts';
import { correctNonceState } from '@store/accounts/nonce';
@@ -7,7 +6,7 @@ import { AnchorMode, makeSTXTokenTransfer, StacksTransaction } from '@stacks/tra
import BN from 'bn.js';
import { stxToMicroStx } from '@stacks/ui-utils';
import { useLoading } from '@common/hooks/use-loading';
-import { useEffect } from 'react';
+import { useCallback, useEffect } from 'react';
import { useMakeAssetTransfer } from '@pages/transaction-signing/hooks/use-asset-transfer';
import { useSelectedAsset } from '@common/hooks/use-selected-asset';
@@ -18,28 +17,30 @@ interface TokenTransferParams {
}
export function useMakeStxTransfer() {
- return useRecoilCallback(({ snapshot }) => async (params: TokenTransferParams) => {
- const { amount, recipient, memo } = params;
- const { network, account, nonce } = await snapshot.getPromise(
- waitForAll({
- network: currentStacksNetworkState,
- account: currentAccountState,
- nonce: correctNonceState,
- })
- );
+ return useAtomCallback(
+ useCallback(async (get, _set, arg) => {
+ const { amount, recipient, memo } = arg;
+ const { network, account, nonce } = get(
+ waitForAll({
+ network: currentStacksNetworkState,
+ account: currentAccountState,
+ nonce: correctNonceState,
+ })
+ );
- if (!account) return;
+ if (!account) return;
- return makeSTXTokenTransfer({
- recipient,
- amount: new BN(stxToMicroStx(amount).toString(), 10),
- memo,
- senderKey: account.stxPrivateKey,
- network,
- nonce: new BN(nonce.toString(), 10),
- anchorMode: AnchorMode.Any,
- });
- });
+ return makeSTXTokenTransfer({
+ recipient,
+ amount: new BN(stxToMicroStx(amount).toString(), 10),
+ memo,
+ senderKey: account.stxPrivateKey,
+ network,
+ nonce: new BN(nonce.toString(), 10),
+ anchorMode: AnchorMode.Any,
+ });
+ }, [])
+ );
}
export function useMakeTransferEffect({
@@ -65,38 +66,20 @@ export function useMakeTransferEffect({
const handleMakeFtTransaction = useMakeAssetTransfer();
const isActive = isShowing && !!amount && !!recipient;
const notLoaded = selectedAsset && !transaction && !isLoading;
+ const method = selectedAsset?.type === 'stx' ? handleMakeStxTransaction : handleMakeFtTransaction;
+
+ const handleGenerateTransfer = useCallback(async () => {
+ setIsLoading();
+ const tx = await method({
+ amount,
+ recipient,
+ memo,
+ });
+ if (tx) setTransaction(tx);
+ setIsIdle();
+ }, [amount, recipient, memo, method, setTransaction, setIsLoading, setIsIdle]);
useEffect(() => {
- const method =
- selectedAsset?.type === 'stx' ? handleMakeStxTransaction : handleMakeFtTransaction;
- if (isActive) {
- if (notLoaded) {
- setIsLoading();
- void method({
- amount,
- recipient,
- memo,
- }).then(tx => {
- if (tx) {
- setTransaction(tx);
- }
- setIsIdle();
- });
- }
- }
- }, [
- memo,
- selectedAsset?.type,
- handleMakeFtTransaction,
- isActive,
- notLoaded,
- setTransaction,
- amount,
- recipient,
- setIsLoading,
- setIsIdle,
- handleMakeStxTransaction,
- transaction,
- isLoading,
- ]);
+ if (isActive && notLoaded) void handleGenerateTransfer();
+ }, [isActive, notLoaded, setIsLoading, handleGenerateTransfer]);
}
diff --git a/src/pages/transaction-signing/hooks/use-pending-transaction.ts b/src/pages/transaction-signing/hooks/use-pending-transaction.ts
index 45dc3586..1826c0bc 100644
--- a/src/pages/transaction-signing/hooks/use-pending-transaction.ts
+++ b/src/pages/transaction-signing/hooks/use-pending-transaction.ts
@@ -1,6 +1,6 @@
-import { useRecoilValue } from 'recoil';
import { pendingTransactionState } from '@store/transactions';
+import { useAtomValue } from 'jotai/utils';
export function usePendingTransaction() {
- return useRecoilValue(pendingTransactionState);
+ return useAtomValue(pendingTransactionState);
}
diff --git a/src/pages/transaction-signing/hooks/use-submit-stx-transaction.ts b/src/pages/transaction-signing/hooks/use-submit-stx-transaction.ts
index e85403d4..61a66630 100644
--- a/src/pages/transaction-signing/hooks/use-submit-stx-transaction.ts
+++ b/src/pages/transaction-signing/hooks/use-submit-stx-transaction.ts
@@ -6,13 +6,12 @@ import {
import { useDoChangeScreen } from '@common/hooks/use-do-change-screen';
import { useWallet } from '@common/hooks/use-wallet';
import { useLoading } from '@common/hooks/use-loading';
-import { useRecoilValue } from 'recoil';
import { currentStacksNetworkState } from '@store/networks';
import { useCallback } from 'react';
import { ScreenPaths } from '@store/common/types';
-import { useRevalidateApi } from '@common/hooks/use-revalidate-api';
import { toast } from 'react-hot-toast';
import { useHomeTabs } from '@common/hooks/use-home-tabs';
+import { useAtomValue } from 'jotai/utils';
function getErrorMessage(
reason: TxBroadcastResultRejected['reason'] | 'ConflictingNonceInMempool'
@@ -43,8 +42,7 @@ export function useHandleSubmitTransaction({
const doChangeScreen = useDoChangeScreen();
const { doSetLatestNonce } = useWallet();
const { setIsLoading, setIsIdle } = useLoading(loadingKey);
- const stacksNetwork = useRecoilValue(currentStacksNetworkState);
- const revalidate = useRevalidateApi();
+ const stacksNetwork = useAtomValue(currentStacksNetworkState);
const { setActiveTabActivity } = useHomeTabs();
return useCallback(async () => {
@@ -56,7 +54,6 @@ export function useHandleSubmitTransaction({
toast.error(getErrorMessage(response.reason));
} else {
await doSetLatestNonce(transaction.auth.spendingCondition?.nonce.toNumber());
- await revalidate();
toast.success('Transaction submitted!');
}
} catch (e) {
@@ -70,7 +67,6 @@ export function useHandleSubmitTransaction({
doChangeScreen(ScreenPaths.HOME);
}, [
setActiveTabActivity,
- revalidate,
doChangeScreen,
doSetLatestNonce,
setIsLoading,
diff --git a/src/pages/transaction-signing/hooks/use-transaction-broadcast.ts b/src/pages/transaction-signing/hooks/use-transaction-broadcast.ts
index c665d146..3f11ea4b 100644
--- a/src/pages/transaction-signing/hooks/use-transaction-broadcast.ts
+++ b/src/pages/transaction-signing/hooks/use-transaction-broadcast.ts
@@ -1,5 +1,5 @@
import { useWallet } from '@common/hooks/use-wallet';
-import { useRecoilCallback, waitForAll } from 'recoil';
+import { useAtomCallback, waitForAll } from 'jotai/utils';
import { currentAccountState } from '@store/accounts';
import {
transactionAttachmentState,
@@ -10,22 +10,22 @@ import { requestTokenState } from '@store/transactions/requests';
import { currentNetworkState } from '@store/networks';
import { finalizeTxSignature } from '@common/utils';
import { handleBroadcastTransaction } from '@common/transactions/transactions';
+import { useCallback } from 'react';
export function useTransactionBroadcast() {
const { doSetLatestNonce } = useWallet();
- return useRecoilCallback(
- ({ snapshot, set }) =>
- async () => {
- const { account, signedTransaction, attachment, requestToken, network } =
- await snapshot.getPromise(
- waitForAll({
- signedTransaction: signedTransactionState,
- account: currentAccountState,
- attachment: transactionAttachmentState,
- requestToken: requestTokenState,
- network: currentNetworkState,
- })
- );
+ return useAtomCallback(
+ useCallback(
+ async (get, set) => {
+ const { account, signedTransaction, attachment, requestToken, network } = get(
+ waitForAll({
+ signedTransaction: signedTransactionState,
+ account: currentAccountState,
+ attachment: transactionAttachmentState,
+ requestToken: requestTokenState,
+ network: currentNetworkState,
+ })
+ );
if (!account || !requestToken || !signedTransaction) {
set(transactionBroadcastErrorState, 'No pending transaction found.');
@@ -41,13 +41,14 @@ export function useTransactionBroadcast() {
attachment,
networkUrl: network.url,
});
- await doSetLatestNonce(nonce);
+ typeof nonce !== 'undefined' && (await doSetLatestNonce(nonce));
finalizeTxSignature(requestToken, result);
} catch (error) {
console.error(error);
set(transactionBroadcastErrorState, error.message);
}
},
- [doSetLatestNonce]
+ [doSetLatestNonce]
+ )
);
}
diff --git a/src/pages/transaction-signing/hooks/use-transaction-error.ts b/src/pages/transaction-signing/hooks/use-transaction-error.ts
index fa593778..88ef0618 100644
--- a/src/pages/transaction-signing/hooks/use-transaction-error.ts
+++ b/src/pages/transaction-signing/hooks/use-transaction-error.ts
@@ -1,6 +1,5 @@
import { useTransactionContractInterface } from '@pages/transaction-signing/hooks/use-transaction';
import { useTransactionRequest } from '@common/hooks/use-transaction-request';
-import { useRecoilValue } from 'recoil';
import { useWallet } from '@common/hooks/use-wallet';
import { useFetchBalances } from '@common/hooks/account/use-account-info';
import { useMemo } from 'react';
@@ -10,42 +9,43 @@ import { TransactionTypes } from '@stacks/connect';
import { useTransactionFee } from '@pages/transaction-signing/hooks/use-transaction-fee';
import { transactionBroadcastErrorState } from '@store/transactions';
import { useOrigin } from '@common/hooks/use-origin';
-import { useLoadable } from '@common/hooks/use-loadable';
import { transactionRequestValidationState } from '@store/transactions/requests';
+import { useAtomValue } from 'jotai/utils';
export function useTransactionError() {
const transactionRequest = useTransactionRequest();
- const fee = useTransactionFee();
const contractInterface = useTransactionContractInterface();
- const broadcastError = useRecoilValue(transactionBroadcastErrorState);
- const isValidTransaction = useLoadable(transactionRequestValidationState);
+ const fee = useTransactionFee();
+ const broadcastError = useAtomValue(transactionBroadcastErrorState);
+ const isValidTransaction = useAtomValue(transactionRequestValidationState);
const origin = useOrigin();
const { currentAccount } = useWallet();
const balances = useFetchBalances();
+
+ // return null;
return useMemo(() => {
if (origin === false) return TransactionErrorReason.ExpiredRequest;
- if (isValidTransaction.contents === false && !isValidTransaction.isLoading)
- return TransactionErrorReason.Unauthorized;
+ if (isValidTransaction === false) return TransactionErrorReason.Unauthorized;
- if (!transactionRequest || balances.errorMaybe() || !currentAccount) {
+ if (!transactionRequest || !balances || !currentAccount) {
return TransactionErrorReason.Generic;
}
- if (
- transactionRequest.txType === TransactionTypes.ContractCall &&
- !contractInterface.isLoading &&
- !contractInterface.contents
- )
+ if (transactionRequest.txType === TransactionTypes.ContractCall && !contractInterface)
return TransactionErrorReason.NoContract;
if (broadcastError) return TransactionErrorReason.BroadcastError;
- if (balances.value) {
- const stxBalance = new BigNumber(balances.value.stx.balance);
+ if (balances) {
+ const stxBalance = new BigNumber(balances.stx.balance);
+ const zeroBalance = stxBalance.toNumber() === 0;
if (transactionRequest.txType === TransactionTypes.STXTransfer) {
+ if (zeroBalance) return TransactionErrorReason.StxTransferInsufficientFunds;
+
const transferAmount = new BigNumber(transactionRequest.amount);
if (transferAmount.gte(stxBalance))
return TransactionErrorReason.StxTransferInsufficientFunds;
}
+ if (zeroBalance) return TransactionErrorReason.FeeInsufficientFunds;
if (fee && !fee.isSponsored && fee.amount) {
const feeAmount = new BigNumber(fee.amount);
if (feeAmount.gte(stxBalance)) return TransactionErrorReason.FeeInsufficientFunds;
diff --git a/src/pages/transaction-signing/hooks/use-transaction-fee.ts b/src/pages/transaction-signing/hooks/use-transaction-fee.ts
index 4a07cfc0..9833e7f2 100644
--- a/src/pages/transaction-signing/hooks/use-transaction-fee.ts
+++ b/src/pages/transaction-signing/hooks/use-transaction-fee.ts
@@ -1,11 +1,12 @@
-import { useSignedTransaction } from './use-transaction';
+import { useAtomValue } from 'jotai/utils';
+import { transactionFeeState, transactionSponsoredState } from '@store/transactions';
export function useTransactionFee() {
- const signedTransaction = useSignedTransaction();
+ const amount = useAtomValue(transactionFeeState);
+ const isSponsored = useAtomValue(transactionSponsoredState);
return {
- amount: signedTransaction?.value?.fee,
- isSponsored: signedTransaction?.value?.isSponsored,
- isLoading: signedTransaction.isLoading,
+ amount,
+ isSponsored,
};
}
diff --git a/src/pages/transaction-signing/hooks/use-transaction.ts b/src/pages/transaction-signing/hooks/use-transaction.ts
index f995b3bd..16cc6552 100644
--- a/src/pages/transaction-signing/hooks/use-transaction.ts
+++ b/src/pages/transaction-signing/hooks/use-transaction.ts
@@ -1,4 +1,4 @@
-import { useLoadable } from '@common/hooks/use-loadable';
+import { useAtomValue } from 'jotai/utils';
import { postConditionsState, signedTransactionState } from '@store/transactions';
import {
transactionContractInterfaceState,
@@ -7,23 +7,21 @@ import {
} from '@store/transactions/contract-call';
export function useTransactionContractInterface() {
- return useLoadable(transactionContractInterfaceState);
+ return useAtomValue(transactionContractInterfaceState);
}
export function useTransactionContractSource() {
- return useLoadable(transactionContractSourceState);
+ return useAtomValue(transactionContractSourceState);
}
export function useTransactionFunction() {
- const payload = useLoadable(transactionFunctionsState);
- return payload?.value;
+ return useAtomValue(transactionFunctionsState);
}
export function useTransactionPostConditions() {
- const payload = useLoadable(postConditionsState);
- return payload?.value;
+ return useAtomValue(postConditionsState);
}
export function useSignedTransaction() {
- return useLoadable(signedTransactionState);
+ return useAtomValue(signedTransactionState);
}
diff --git a/src/pages/transaction-signing/transaction-signing.tsx b/src/pages/transaction-signing/transaction-signing.tsx
index c2aec1e1..f288d8a8 100644
--- a/src/pages/transaction-signing/transaction-signing.tsx
+++ b/src/pages/transaction-signing/transaction-signing.tsx
@@ -9,19 +9,22 @@ import { ContractDeployDetails } from '@pages/transaction-signing/components/con
import { PostConditions } from '@pages/transaction-signing/components/post-conditions/list';
import { StxTransferDetails } from '@pages/transaction-signing/components/stx-transfer-details';
import { useTransactionRequest } from '@common/hooks/use-transaction-request';
+import { Stack } from '@stacks/ui';
export const TransactionPage = memo(() => {
const transactionRequest = useTransactionRequest();
if (!transactionRequest) return null;
return (
}>
-
-
-
-
-
-
-
+
+
+
+
+ {transactionRequest.txType === 'contract_call' && }
+ {transactionRequest.txType === 'token_transfer' && }
+ {transactionRequest.txType === 'smart_contract' && }
+
+
);
});
diff --git a/src/routes.tsx b/src/routes.tsx
index e0ceb301..d3d31509 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react';
-import { useSetRecoilState } from 'recoil';
+
import { Route as RouterRoute, Routes as RoutesDom, useLocation } from 'react-router-dom';
import { MagicRecoveryCode } from '@pages/install/magic-recovery-code';
@@ -26,6 +26,7 @@ import { AccountGateRoute } from '@components/account-gate-route';
import { lastSeenStore } from '@store/wallet';
import { ErrorBoundary } from '@components/error-boundary';
import { Unlock } from '@components/unlock';
+import { useUpdateAtom } from 'jotai/utils';
interface RouteProps {
path: ScreenPaths;
@@ -40,7 +41,7 @@ export const Routes: React.FC = () => {
const { isSignedIn: signedIn, encryptedSecretKey } = useWallet();
const { isOnboardingInProgress } = useOnboardingState();
const { pathname } = useLocation();
- const setLastSeen = useSetRecoilState(lastSeenStore);
+ const setLastSeen = useUpdateAtom(lastSeenStore);
const doChangeScreen = useDoChangeScreen();
useSaveAuthRequest();
@@ -98,7 +99,14 @@ export const Routes: React.FC = () => {
} />
} />;
- } />
+ >}>
+
+
+ }
+ />
{/* Transactions */}
diff --git a/src/store/accounts/index.ts b/src/store/accounts/index.ts
index 673a6936..40ae3180 100644
--- a/src/store/accounts/index.ts
+++ b/src/store/accounts/index.ts
@@ -1,18 +1,18 @@
import { MempoolTransaction, Transaction } from '@blockstack/stacks-blockchain-api-types';
import { Account, getStxAddress } from '@stacks/wallet-sdk';
-import { atom, selector, waitForAll } from 'recoil';
+import { atomWithDefault, waitForAll } from 'jotai/utils';
+import { atom } from 'jotai';
import BN from 'bn.js';
import type { AllAccountData } from '@common/api/accounts';
import { fetchAllAccountData } from '@common/api/accounts';
-import { apiRevalidation, intervalState } from '@store/common/api-helpers';
-import { fetcher } from '@common/api/wrapped-fetch';
import { transactionRequestStxAddressState } from '@store/transactions/requests';
import { currentNetworkState } from '@store/networks';
-import { DEFAULT_POLLING_INTERVAL } from '@store/common/constants';
import { walletState } from '@store/wallet';
import { transactionNetworkVersionState } from '@store/transactions';
+import { atomFamilyWithQuery } from '@store/query';
+import { accountsApiClientState } from '@store/common/api-clients';
/**
* --------------------------------------
@@ -31,48 +31,28 @@ import { transactionNetworkVersionState } from '@store/transactions';
* accountInfoStore - external API data from the `v2/accounts` endpoint, should be the most up-to-date
*/
-enum ACCOUNT_KEYS {
- ALL_ACCOUNTS = 'accounts/ALL_ACCOUNTS',
- ALL_ACCOUNTS_WITH_ADDRESSES = 'accounts/ACCOUNTS_WITH_ADDRESSES',
- HAS_SWITCHED_ACCOUNTS = 'accounts/HAS_SWITCHED_ACCOUNTS',
- TRANSACTION_ACCOUNT_INDEX = 'accounts/TRANSACTION_ACCOUNT_INDEX',
- CURRENT_ACCOUNT_INDEX = 'accounts/CURRENT_ACCOUNT_INDEX',
- CURRENT_ACCOUNT = 'accounts/CURRENT_ACCOUNT',
- CURRENT_ACCOUNT_ADDRESS = 'accounts/CURRENT_ACCOUNT_ADDRESS',
- CURRENT_ACCOUNT_DATA = 'accounts/CURRENT_ACCOUNT_DATA',
- CURRENT_ACCOUNT_BALANCES = 'accounts/CURRENT_ACCOUNT_BALANCES',
- CURRENT_ACCOUNT_INFO = 'accounts/CURRENT_ACCOUNT_INFO',
- CURRENT_ACCOUNT_TRANSACTIONS = 'accounts/CURRENT_ACCOUNT_TRANSACTIONS',
-}
-
//--------------------------------------
// All accounts
//--------------------------------------
-export const accountsState = selector({
- key: ACCOUNT_KEYS.ALL_ACCOUNTS,
- get: ({ get }) => {
- const wallet = get(walletState);
- if (!wallet) return undefined;
- return wallet.accounts;
- },
+export const accountsState = atomWithDefault(get => {
+ const wallet = get(walletState);
+ if (!wallet) return undefined;
+ return wallet.accounts;
});
export type AccountWithAddress = Account & { address: string };
// map through the accounts and get the address for the current network mode (testnet|mainnet)
-export const accountsWithAddressState = selector({
- key: ACCOUNT_KEYS.ALL_ACCOUNTS_WITH_ADDRESSES,
- get: ({ get }) => {
- const accounts = get(accountsState);
- const transactionVersion = get(transactionNetworkVersionState);
- if (!accounts) return undefined;
- return accounts.map(account => {
- const address = getStxAddress({ account, transactionVersion });
- return {
- ...account,
- address,
- };
- });
- },
+export const accountsWithAddressState = atom(get => {
+ const accounts = get(accountsState);
+ const transactionVersion = get(transactionNetworkVersionState);
+ if (!accounts) return undefined;
+ return accounts.map(account => {
+ const address = getStxAddress({ account, transactionVersion });
+ return {
+ ...account,
+ address,
+ };
+ });
});
//--------------------------------------
@@ -81,127 +61,118 @@ export const accountsWithAddressState = selector({
- key: ACCOUNT_KEYS.CURRENT_ACCOUNT_INDEX,
- default: undefined,
-});
+export const currentAccountIndexState = atom(0);
// This is only used when there is a pending transaction request and
// the user switches accounts during the signing process
-export const hasSwitchedAccountsState = atom({
- key: ACCOUNT_KEYS.HAS_SWITCHED_ACCOUNTS,
- default: false,
-});
+export const hasSwitchedAccountsState = atom(false);
// if there is a pending transaction that has a stxAccount param
// find the index from the accounts atom and return it
-export const transactionAccountIndexState = selector({
- key: ACCOUNT_KEYS.TRANSACTION_ACCOUNT_INDEX,
- get: ({ get }) => {
- const { accounts, txAddress } = get(
- waitForAll({
- accounts: accountsWithAddressState,
- txAddress: transactionRequestStxAddressState,
- })
- );
+export const transactionAccountIndexState = atom(get => {
+ const { accounts, txAddress } = get(
+ waitForAll({
+ accounts: accountsWithAddressState,
+ txAddress: transactionRequestStxAddressState,
+ })
+ );
- if (txAddress && accounts) {
- const selectedAccount = accounts.findIndex(account => account.address === txAddress);
- if (typeof selectedAccount === 'number') return selectedAccount;
- }
- return undefined;
- },
+ if (txAddress && accounts) {
+ return accounts.findIndex(account => account.address === txAddress); // selected account
+ }
+ return undefined;
});
// This contains the state of the current account:
// could be the account associated with an in-process transaction request
// or the last selected / first account of the user
-export const currentAccountState = selector({
- key: ACCOUNT_KEYS.CURRENT_ACCOUNT,
- get: ({ get }) => {
- const { accountIndex, txIndex, hasSwitched, accounts } = get(
- waitForAll({
- accountIndex: currentAccountIndexState,
- txIndex: transactionAccountIndexState,
- hasSwitched: hasSwitchedAccountsState,
- accounts: accountsWithAddressState,
- })
- );
- if (!accounts) return undefined;
- if (typeof txIndex === 'number' && !hasSwitched) return accounts[txIndex];
- if (typeof accountIndex === 'number') return accounts[accountIndex];
- return undefined;
- },
- dangerouslyAllowMutability: true,
+export const currentAccountState = atom(get => {
+ const { accountIndex, txIndex, hasSwitched, accounts } = get(
+ waitForAll({
+ accountIndex: currentAccountIndexState,
+ txIndex: transactionAccountIndexState,
+ hasSwitched: hasSwitchedAccountsState,
+ accounts: accountsWithAddressState,
+ })
+ );
+ if (!accounts) return undefined;
+ if (typeof txIndex === 'number' && !hasSwitched) return accounts[txIndex];
+ return accounts[accountIndex];
});
// gets the address of the current account (in the current network mode)
-export const currentAccountStxAddressState = selector({
- key: ACCOUNT_KEYS.CURRENT_ACCOUNT_ADDRESS,
- get: ({ get }) => get(currentAccountState)?.address,
-});
+export const currentAccountStxAddressState = atom(
+ get => get(currentAccountState)?.address
+);
+// gets the private key of the current account
+export const currentAccountPrivateKeyState = atom(
+ get => get(currentAccountState)?.stxPrivateKey
+);
-// external API data associated with the current account's address
-export const accountDataState = selector({
- key: ACCOUNT_KEYS.CURRENT_ACCOUNT_DATA,
- get: async ({ get }) => {
- const { network, address } = get(
- waitForAll({
- apiRevalidation,
- interval: intervalState(DEFAULT_POLLING_INTERVAL),
- network: currentNetworkState,
- address: currentAccountStxAddressState,
- })
- );
- if (!address) return;
+const accountDataResponseState = atomFamilyWithQuery<[string, string], AllAccountData | undefined>(
+ `ALL_ACCOUNT_DATA`,
+ async (_get, [address, networkUrl]) => {
try {
- return fetchAllAccountData(network.url)(address);
+ return fetchAllAccountData(networkUrl)(address);
} catch (error) {
console.error(error);
- console.error(`Unable to fetch account data from ${network.url}`);
+ console.error(`Unable to fetch account data from ${networkUrl}`);
return;
}
- },
-});
-
-// the balances of the current account's address
-export const accountBalancesState = selector({
- key: ACCOUNT_KEYS.CURRENT_ACCOUNT_BALANCES,
- get: ({ get }) => get(accountDataState)?.balances,
+ }
+);
+// external API data associated with the current account's address
+export const accountDataState = atom(get => {
+ const { network, address } = get(
+ waitForAll({
+ network: currentNetworkState,
+ address: currentAccountStxAddressState,
+ })
+ );
+ if (!address) return;
+ return get(accountDataResponseState([address, network.url]));
});
// the raw account info from the `v2/accounts` endpoint, should be most up-to-date info (compared to the extended API)
-export const accountInfoState = selector({
- key: ACCOUNT_KEYS.CURRENT_ACCOUNT_INFO,
- get: async ({ get }) => {
- const { address, network } = get(
- waitForAll({
- revalidation: apiRevalidation,
- interval: intervalState(DEFAULT_POLLING_INTERVAL),
- address: currentAccountStxAddressState,
- network: currentNetworkState,
- })
- );
- if (!address) return;
- const url = `${network.url}/v2/accounts/${address}`;
- const error = new Error(`Unable to fetch account info from ${url}`);
- const response = await fetcher(url);
- if (!response.ok) throw error;
- const data = await response.json();
+export const accountInfoResponseState = atomFamilyWithQuery(
+ 'ACCOUNT_INFO_STATE_ATOM',
+ async (get, principal) => {
+ const client = get(accountsApiClientState);
+ const data = await client.getAccountInfo({
+ principal,
+ proof: 0,
+ });
return {
balance: new BN(data.balance.slice(2), 16),
nonce: data.nonce,
};
- },
+ }
+);
+export const accountInfoState = atom(get => {
+ const principal = get(currentAccountStxAddressState);
+ if (!principal) return;
+ return get(accountInfoResponseState(principal));
});
+// the balances of the current account's address
+export const accountBalancesState = atom(
+ get => get(accountDataState)?.balances
+);
// combo of pending and confirmed transactions for the current address
-export const accountTransactionsState = selector<(MempoolTransaction | Transaction)[]>({
- key: ACCOUNT_KEYS.CURRENT_ACCOUNT_TRANSACTIONS,
- get: async ({ get }) => {
- const data = get(accountDataState);
- const transactions = data?.transactions?.results || [];
- const pending = data?.pendingTransactions || [];
- return [...pending, ...transactions];
- },
+export const accountTransactionsState = atom<(MempoolTransaction | Transaction)[]>(get => {
+ const data = get(accountDataState);
+ const transactions = data?.transactions?.results || [];
+ const pending = data?.pendingTransactions || [];
+ return [...pending, ...transactions];
});
+
+accountsState.debugLabel = 'accountsState';
+accountsWithAddressState.debugLabel = 'accountsWithAddressState';
+currentAccountIndexState.debugLabel = 'currentAccountIndexState';
+hasSwitchedAccountsState.debugLabel = 'hasSwitchedAccountsState';
+transactionAccountIndexState.debugLabel = 'transactionAccountIndexState';
+currentAccountState.debugLabel = 'currentAccountState';
+currentAccountStxAddressState.debugLabel = 'currentAccountStxAddressState';
+currentAccountPrivateKeyState.debugLabel = 'currentAccountPrivateKeyState';
+accountBalancesState.debugLabel = 'accountBalancesState';
+accountTransactionsState.debugLabel = 'accountTransactionsState';
diff --git a/src/store/accounts/names.ts b/src/store/accounts/names.ts
index 501336e5..ab9c832a 100644
--- a/src/store/accounts/names.ts
+++ b/src/store/accounts/names.ts
@@ -1,4 +1,4 @@
-import { selector } from 'recoil';
+import { atom } from 'jotai';
import { getStxAddress } from '@stacks/wallet-sdk';
import { accountsState } from './index';
@@ -32,61 +32,57 @@ type AccountNameState = AccountName[] | null;
const STALE_TIME = 30 * 60 * 1000; // 30 min
-enum KEYS {
- NAMES = 'account/NAMES',
-}
+export const accountNameState = atom(async get => {
+ const accounts = get(accountsState);
+ const network = get(currentNetworkState);
+ const transactionVersion = get(transactionNetworkVersionState);
-export const accountNameState = selector({
- key: KEYS.NAMES,
- get: async ({ get }) => {
- const accounts = get(accountsState);
- const network = get(currentNetworkState);
- const transactionVersion = get(transactionNetworkVersionState);
+ if (!network || !accounts) return null;
- if (!network || !accounts) return null;
-
- const promises = accounts.map(async account => {
- const address = getStxAddress({
- account: account,
- transactionVersion,
- });
-
- // let's try to find any saved names first
- const local = getLocalNames(network.url, address);
-
- if (local) {
- const [names, timestamp] = local;
- const now = Date.now();
- const isStale = now - timestamp > STALE_TIME;
- if (!isStale)
- return {
- address,
- index: account.index,
- names,
- };
- }
-
- try {
- const names = await fetchNamesByAddress(network.url, address);
- if (names?.length) {
- // persist them for next time
- setLocalNames(network.url, address, [names, Date.now()]);
- }
- return {
- address,
- index: account.index,
- names: names || [],
- };
- } catch (e) {
- console.error(e);
- return {
- address,
- index: account.index,
- names: [],
- };
- }
+ const promises = accounts.map(async account => {
+ const address = getStxAddress({
+ account: account,
+ transactionVersion,
});
- if (!promises) return null;
- return Promise.all(promises);
- },
+
+ // let's try to find any saved names first
+ const local = getLocalNames(network.url, address);
+
+ if (local) {
+ const [names, timestamp] = local;
+ const now = Date.now();
+ const isStale = now - timestamp > STALE_TIME;
+ if (!isStale)
+ return {
+ address,
+ index: account.index,
+ names,
+ };
+ }
+
+ try {
+ const names = await fetchNamesByAddress(network.url, address);
+ if (names?.length) {
+ // persist them for next time
+ setLocalNames(network.url, address, [names, Date.now()]);
+ }
+ return {
+ address,
+ index: account.index,
+ names: names || [],
+ };
+ } catch (e) {
+ console.error(e);
+ return {
+ address,
+ index: account.index,
+ names: [],
+ };
+ }
+ });
+ if (!promises) return null;
+ const value = await Promise.all(promises);
+ return value;
});
+
+accountNameState.debugLabel = 'accountNameState';
diff --git a/src/store/accounts/nonce.ts b/src/store/accounts/nonce.ts
index 7b087c78..ff9cc82f 100644
--- a/src/store/accounts/nonce.ts
+++ b/src/store/accounts/nonce.ts
@@ -1,118 +1,89 @@
-import { atomFamily, selector, waitForAll } from 'recoil';
+import { atom } from 'jotai';
+import { atomFamily, atomWithStorage, waitForAll } from 'jotai/utils';
import { accountDataState, currentAccountStxAddressState, accountInfoState } from '@store/accounts';
import { currentNetworkState } from '@store/networks';
-import { apiRevalidation } from '@store/common/api-helpers';
-import { localStorageEffect } from '@store/common/utils';
+import deepEqual from 'fast-deep-equal';
-enum NONCE_KEYS {
- LOCAL_NONCES = 'account/LOCAL_NONCES',
- LATEST_LOCAL_NONCE = 'account/LATEST_LOCAL_NONCE',
- CORRECT_NONCE = 'account/CORRECT_NONCE',
-}
+export const localNoncesState = atomFamily<[string, string], number, number>(
+ _params => atomWithStorage('LOCAL_NONCE_STATE', 0),
+ deepEqual
+);
-export const localNoncesState = atomFamily<
- { nonce: number; blockHeight: number },
- [string, string]
->({
- key: NONCE_KEYS.LOCAL_NONCES,
- default: () => ({
- nonce: 0,
- blockHeight: 0,
- }),
- effects_UNSTABLE: [localStorageEffect()],
+export const latestNonceState = atom(get => {
+ const { network, address } = get(
+ waitForAll({
+ network: currentNetworkState,
+ address: currentAccountStxAddressState,
+ })
+ );
+ return get(localNoncesState([network.url, address || '']));
});
-export const latestNonceState = selector({
- key: NONCE_KEYS.LATEST_LOCAL_NONCE,
- get: ({ get }) => {
- const { network, address } = get(
- waitForAll({
- network: currentNetworkState,
- address: currentAccountStxAddressState,
- })
- );
- return get(localNoncesState([network.url, address || '']));
- },
-});
-
-export const correctNonceState = selector({
- key: NONCE_KEYS.CORRECT_NONCE,
- get: ({ get }) => {
- get(apiRevalidation);
-
- const { account, accountData, address, latestLocalNonce } = get(
- waitForAll({
- account: accountInfoState,
- accountData: accountDataState,
- address: currentAccountStxAddressState,
- latestLocalNonce: latestNonceState,
- })
- );
-
- const lastLocalNonce = latestLocalNonce.nonce;
-
- // most recent confirmed transactions sent by current address
- const lastConfirmedTx = accountData?.transactions.results?.filter(
- tx => tx.sender_address === address
- )?.[0];
-
- // most recent pending transactions sent by current address
- const latestPendingTx = accountData?.pendingTransactions?.filter(
- tx => tx.sender_address === address
- )?.[0];
-
- // oldest pending transactions sent by current address
- const oldestPendingTx = accountData?.pendingTransactions?.length
- ? accountData?.pendingTransactions?.filter(tx => tx.sender_address === address)?.[
- accountData?.pendingTransactions?.length - 1
- ]
- : undefined;
-
- // they have any pending or confirmed transactions
- const hasTransactions = !!latestPendingTx || !!lastConfirmedTx;
-
- if (!hasTransactions || !account || (!hasTransactions && account.nonce === 0)) return 0;
-
- // if the oldest pending tx is more than 1 above the account nonce, it's likely there was
- // a race condition such that the client didn't have the most up to date pending tx
- // if this is true, we should rely on the account nonce
- const hasNonceMismatch =
- oldestPendingTx && lastConfirmedTx
- ? oldestPendingTx.nonce > lastConfirmedTx.nonce + 1
- : false;
-
- // if they do have a miss match, let's use the account nonce
- if (hasNonceMismatch) return account.nonce;
-
- // otherwise, without micro-blocks, the account nonce will likely be out of date compared
- // and not be incremented based on pending transactions
- const pendingNonce = latestPendingTx?.nonce || 0;
- const lastConfirmedTxNonce = lastConfirmedTx?.nonce || 0;
-
- // lastLocalNonce can be set when the user sends transactions
- // and can often be faster that waiting for a new response from the API
- const useLocalNonce = lastLocalNonce > pendingNonce && lastLocalNonce > lastConfirmedTxNonce;
-
- const usePendingNonce =
- !useLocalNonce &&
- ((lastConfirmedTx && pendingNonce > lastConfirmedTx.nonce) ||
- pendingNonce + 1 > account.nonce);
-
- // if they have a last confirmed transaction (but no pending)
- // and it's greater than account nonce, we should use that one
- // else we will use the account nonce
- const useLastTxNonce =
- hasTransactions && lastConfirmedTx && lastConfirmedTx.nonce + 1 > account.nonce;
- const lastConfirmedNonce =
- useLastTxNonce && lastConfirmedTx ? lastConfirmedTx.nonce + 1 : account.nonce;
-
- return useLocalNonce
- ? lastLocalNonce
- : usePendingNonce
- ? // if pending nonce is greater, use that
- pendingNonce + 1
- : // else we use the last confirmed nonce
- lastConfirmedNonce;
- },
+export const correctNonceState = atom(get => {
+ const account = get(accountInfoState);
+ const accountData = get(accountDataState);
+ const address = get(currentAccountStxAddressState);
+ const lastLocalNonce = get(latestNonceState);
+
+ // most recent confirmed transactions sent by current address
+ const lastConfirmedTx = accountData?.transactions.results?.filter(
+ tx => tx.sender_address === address
+ )?.[0];
+
+ // most recent pending transactions sent by current address
+ const latestPendingTx = accountData?.pendingTransactions?.filter(
+ tx => tx.sender_address === address
+ )?.[0];
+
+ // oldest pending transactions sent by current address
+ const oldestPendingTx = accountData?.pendingTransactions?.length
+ ? accountData?.pendingTransactions?.filter(tx => tx.sender_address === address)?.[
+ accountData?.pendingTransactions?.length - 1
+ ]
+ : undefined;
+
+ // they have any pending or confirmed transactions
+ const hasTransactions = !!latestPendingTx || !!lastConfirmedTx;
+
+ if (!hasTransactions || !account || (!hasTransactions && account.nonce === 0)) return 0;
+
+ // if the oldest pending tx is more than 1 above the account nonce, it's likely there was
+ // a race condition such that the client didn't have the most up to date pending tx
+ // if this is true, we should rely on the account nonce
+ const hasNonceMismatch =
+ oldestPendingTx && lastConfirmedTx ? oldestPendingTx.nonce > lastConfirmedTx.nonce + 1 : false;
+
+ // if they do have a miss match, let's use the account nonce
+ if (hasNonceMismatch) return account.nonce;
+
+ // otherwise, without micro-blocks, the account nonce will likely be out of date compared
+ // and not be incremented based on pending transactions
+ const pendingNonce = latestPendingTx?.nonce || 0;
+ const lastConfirmedTxNonce = lastConfirmedTx?.nonce || 0;
+
+ // lastLocalNonce can be set when the user sends transactions
+ // and can often be faster that waiting for a new response from the API
+ const useLocalNonce = lastLocalNonce > pendingNonce && lastLocalNonce > lastConfirmedTxNonce;
+
+ const usePendingNonce =
+ !useLocalNonce &&
+ ((lastConfirmedTx && pendingNonce > lastConfirmedTx.nonce) || pendingNonce + 1 > account.nonce);
+
+ // if they have a last confirmed transaction (but no pending)
+ // and it's greater than account nonce, we should use that one
+ // else we will use the account nonce
+ const useLastTxNonce =
+ hasTransactions && lastConfirmedTx && lastConfirmedTx.nonce + 1 > account.nonce;
+ const lastConfirmedNonce =
+ useLastTxNonce && lastConfirmedTx ? lastConfirmedTx.nonce + 1 : account.nonce;
+
+ return useLocalNonce
+ ? lastLocalNonce
+ : usePendingNonce
+ ? // if pending nonce is greater, use that
+ pendingNonce + 1
+ : // else we use the last confirmed nonce
+ lastConfirmedNonce;
});
+correctNonceState.debugLabel = 'correctNonceState';
diff --git a/src/store/assets/asset-search.ts b/src/store/assets/asset-search.ts
index 12f5e2bc..7e934c64 100644
--- a/src/store/assets/asset-search.ts
+++ b/src/store/assets/asset-search.ts
@@ -1,39 +1,23 @@
-import { atom, selector } from 'recoil';
+import { atom } from 'jotai';
import { assetsState } from '@store/assets/tokens';
import { getFullyQualifiedAssetName } from '@common/hooks/use-selected-asset';
import { AssetWithMeta } from '@store/assets/types';
+import { atomWithDefault } from 'jotai/utils';
-enum ASSET_SEARCH_KEYS {
- ASSET_ID = 'asset-search/ASSET_ID',
- ASSET = 'asset-search/ASSET',
- INPUT = 'asset-search/INPUT',
-}
+export const selectedAssetIdState = atom(undefined);
-export const selectedAssetIdState = atom({
- key: ASSET_SEARCH_KEYS.ASSET_ID,
- default: undefined,
+export const selectedAssetStore = atom(get => {
+ const fqn = get(selectedAssetIdState);
+ const assets = get(assetsState);
+ return assets?.find(asset => getFullyQualifiedAssetName(asset) === fqn);
});
+selectedAssetStore.debugLabel = 'selectedAssetStore';
-export const selectedAssetStore = selector({
- key: ASSET_SEARCH_KEYS.ASSET,
- get: ({ get }) => {
- const fqn = get(selectedAssetIdState);
- const assets = get(assetsState);
- return assets?.find(asset => getFullyQualifiedAssetName(asset) === fqn);
- },
-});
+export const searchInputStore = atom('');
+searchInputStore.debugLabel = 'searchInputStore';
-export const searchInputStore = atom({
- key: ASSET_SEARCH_KEYS.INPUT,
- default: '',
-});
-
-const defaultSearchResultState = selector({
- key: 'asset-search.results',
- get: async ({ get }) => get(assetsState),
-});
-export const searchResultState = atom({
- key: 'asset-search.results',
- default: defaultSearchResultState,
-});
+export const searchResultState = atomWithDefault(get =>
+ get(assetsState)
+);
+searchResultState.debugLabel = 'searchResultState';
diff --git a/src/store/assets/fungible-tokens.ts b/src/store/assets/fungible-tokens.ts
new file mode 100644
index 00000000..1a243f98
--- /dev/null
+++ b/src/store/assets/fungible-tokens.ts
@@ -0,0 +1,100 @@
+import { atomFamily } from 'jotai/utils';
+import { ContractPrincipal, FtMeta, MetaDataMethodNames } from '@store/assets/types';
+import { atom } from 'jotai';
+import { currentNetworkState } from '@store/networks';
+import { getLocalData, setLocalData } from '@store/common/utils';
+import { fetchFungibleTokenMetaData, getMatchingFunction } from '@store/assets/utils';
+import { contractInterfaceState } from '@store/contracts';
+import deepEqual from 'fast-deep-equal';
+import { debugLabelWithContractPrincipal } from '@common/atom-utils';
+
+enum FungibleTokensQueryKeys {
+ SIP_10_COMPLIANT = 'SIP_10_COMPLIANT',
+ META_DATA_METHODS = 'META_DATA_METHODS',
+ ASSET_META_DATA = 'ASSET_META_DATA',
+}
+
+const assetMetaDataMethodsResponseState = atomFamily<
+ Readonly,
+ MetaDataMethodNames | null
+>(({ contractName, contractAddress }) => {
+ const anAtom = atom(get => {
+ const network = get(currentNetworkState);
+ const keyParams = [
+ network.url,
+ contractAddress,
+ contractName,
+ FungibleTokensQueryKeys.META_DATA_METHODS,
+ ];
+ const contractInterface = get(contractInterfaceState({ contractName, contractAddress }));
+ if (!contractInterface) return null;
+ const decimalsFunction = contractInterface.functions.find(getMatchingFunction('decimals'));
+ const symbolFunction = contractInterface.functions.find(getMatchingFunction('symbol'));
+ const nameFunction = contractInterface.functions.find(getMatchingFunction('name'));
+
+ if (decimalsFunction && symbolFunction && nameFunction) {
+ const data = {
+ decimals: decimalsFunction.name,
+ symbol: symbolFunction.name,
+ name: nameFunction.name,
+ };
+ return setLocalData(keyParams, data);
+ }
+ return null;
+ });
+ anAtom.debugLabel = `assetMetaDataMethodsResponseState/${contractAddress}.${contractName}`;
+ return anAtom;
+}, deepEqual);
+
+const assetMetaDataMethods = atomFamily, MetaDataMethodNames | null>(
+ ({ contractName, contractAddress }) => {
+ const anAtom = atom(get => {
+ const network = get(currentNetworkState);
+ const keyParams = [
+ network.url,
+ contractAddress,
+ contractName,
+ FungibleTokensQueryKeys.META_DATA_METHODS,
+ ];
+ const local = getLocalData(keyParams);
+ return local || get(assetMetaDataMethodsResponseState({ contractName, contractAddress }));
+ });
+ debugLabelWithContractPrincipal(anAtom, 'assetMetaDataMethods', {
+ contractName,
+ contractAddress,
+ });
+ return anAtom;
+ },
+ deepEqual
+);
+
+export const assetMetaDataState = atomFamily(
+ ({ contractAddress, contractName }) => {
+ const anAtom = atom(async get => {
+ const methods = get(assetMetaDataMethods({ contractName, contractAddress }));
+ if (!methods) return null;
+ const network = get(currentNetworkState);
+ const keyParams = [
+ network.url,
+ contractAddress,
+ contractName,
+ FungibleTokensQueryKeys.ASSET_META_DATA,
+ ];
+ const localData = getLocalData(keyParams);
+ if (localData) return localData;
+ const data = await fetchFungibleTokenMetaData({
+ contractName,
+ contractAddress,
+ network: network.url,
+ methods,
+ });
+ return setLocalData(keyParams, data);
+ });
+ debugLabelWithContractPrincipal(anAtom, 'assetMetaDataState', {
+ contractName,
+ contractAddress,
+ });
+ return anAtom;
+ },
+ deepEqual
+);
diff --git a/src/store/assets/tokens.ts b/src/store/assets/tokens.ts
index 7b5f5059..19912f73 100644
--- a/src/store/assets/tokens.ts
+++ b/src/store/assets/tokens.ts
@@ -1,280 +1,93 @@
-import { selector, selectorFamily, waitForAll } from 'recoil';
-import { currentNetworkState } from '@store/networks';
+import { atom } from 'jotai';
+import deepEqual from 'fast-deep-equal';
+import { atomFamily, waitForAll } from 'jotai/utils';
import { accountBalancesState } from '@store/accounts';
-import { ChainID } from '@stacks/transactions';
-import { ContractInterface } from '@stacks/rpc-client';
-import { isSip10Transfer, SIP010TransferResponse } from '@common/token-utils';
-import { smartContractClientState } from '@store/common/api-clients';
-import {
- fetchFungibleTokenMetaData,
- getLocalData,
- getMatchingFunction,
- getSip10Status,
- setLocalData,
- transformAssets,
-} from '@store/assets/utils';
-import {
- Asset,
- AssetWithMeta,
- ContractPrincipal,
- FtMeta,
- MetaDataMethodNames,
-} from '@store/assets/types';
+import { transformAssets } from '@store/assets/utils';
+import { Asset, AssetWithMeta, ContractPrincipal } from '@store/assets/types';
+import { assetMetaDataState } from '@store/assets/fungible-tokens';
+import { contractInterfaceState } from '@store/contracts';
+import { isSip10Transfer } from '@common/token-utils';
-export const assetSip10ImplementationState = selectorFamily<
- boolean | null,
- { contractName: string; contractAddress: string }
->({
- key: 'asset.sip-010-compliant',
- get:
- ({ contractName, contractAddress }) =>
- async ({ get }) => {
- const network = get(currentNetworkState);
- const chain = network.url.includes('regtest')
- ? 'regtest'
- : network.chainId === ChainID.Testnet
- ? 'testnet'
- : 'mainnet';
- const local = getLocalData({
- networkUrl: network.url,
- address: contractAddress,
- name: contractName,
- key: 'asset.sip-010-compliant',
- });
- if (local || typeof local === 'boolean') return local;
- try {
- const data = await getSip10Status({
- networkUrl: network.url,
- contractAddress,
+const transferDataState = atomFamily(
+ ({ contractAddress, contractName }) => {
+ const anAtom = atom(get => {
+ const contractInterface = get(
+ contractInterfaceState({
contractName,
- chain,
- });
- if (typeof data === 'boolean')
- setLocalData({
- networkUrl: network.url,
- address: contractAddress,
- name: contractName,
- data,
- key: 'asset.sip-010-compliant',
- });
- return data;
- } catch (e) {
- console.log(e);
- return null;
- }
- },
-});
-
-const assetContractInterface = selectorFamily>({
- key: 'asset.contract-interface',
- get:
- ({ contractName, contractAddress }) =>
- async ({ get }) => {
- const network = get(currentNetworkState);
- const client = get(smartContractClientState);
-
- const local = getLocalData({
- networkUrl: network.url,
- address: contractAddress,
- name: contractName,
- key: 'asset.contract-interface',
- });
-
- if (local) return local;
- const data = await client.getContractInterface({
- contractName,
- contractAddress,
- });
- setLocalData({
- networkUrl: network.url,
- address: contractAddress,
- name: contractName,
- data,
- key: 'asset.contract-interface',
- });
- return data;
- },
-});
-
-const assetMetaDataMethods = selectorFamily<
- MetaDataMethodNames | null,
- Readonly
->({
- key: 'asset.meta-data-methods',
- get:
- ({ contractName, contractAddress }) =>
- async ({ get }) => {
- const network = get(currentNetworkState);
-
- const local = getLocalData({
- networkUrl: network.url,
- address: contractAddress,
- name: contractName,
- key: 'asset.meta-data-methods',
- });
-
- if (local) return local;
-
- const contractInterface = get(assetContractInterface({ contractName, contractAddress }));
- const decimalsFunction = contractInterface.functions.find(getMatchingFunction('decimals'));
- const symbolFunction = contractInterface.functions.find(getMatchingFunction('symbol'));
- const nameFunction = contractInterface.functions.find(getMatchingFunction('name'));
-
- if (decimalsFunction && symbolFunction && nameFunction) {
- const data = {
- decimals: decimalsFunction.name,
- symbol: symbolFunction.name,
- name: nameFunction.name,
- };
- setLocalData({
- networkUrl: network.url,
- address: contractAddress,
- name: contractName,
- data,
- key: 'asset.meta-data-methods',
- });
- return data;
- }
-
- return null;
- },
-});
-
-export const assetMetaDataState = selectorFamily<
- FtMeta | null,
- { contractName: string; contractAddress: string }
->({
- key: 'asset.meta-data',
- get:
- ({ contractName, contractAddress }) =>
- async ({ get }) => {
- const methods = get(assetMetaDataMethods({ contractName, contractAddress }));
- if (!methods) return null;
-
- const isImplemented = get(
- assetSip10ImplementationState({
contractAddress,
- contractName,
})
);
- const network = get(currentNetworkState);
- const localData = getLocalData({
- networkUrl: network.url,
- address: contractAddress,
- name: contractName,
- key: 'asset.meta-data',
- });
- if (localData) {
- return {
- ...localData,
- ftTrait: isImplemented,
- };
- }
- const data = await fetchFungibleTokenMetaData({
- contractName,
- contractAddress,
- network: network.url,
- methods,
- });
- if (data) {
- setLocalData({
- networkUrl: network.url,
- address: contractAddress,
- name: contractName,
- data,
- key: 'asset.meta-data',
- });
- return {
- ...data,
- ftTrait: isImplemented,
- };
- }
-
- return null;
- },
-});
-
-export const canTransferAssetState = selectorFamily<
- undefined | SIP010TransferResponse,
- Readonly
->({
- key: 'assets.can-transfer',
- get:
- ({ contractName, contractAddress }) =>
- async ({ get }) => {
- if (!contractAddress || !contractName) return;
- const contractInterface = get(assetContractInterface({ contractName, contractAddress }));
- return isSip10Transfer({ contractInterface });
- },
-});
-
-const assetItemState = selectorFamily>({
- key: 'assets.item',
- get:
- asset =>
- ({ get }) => {
- if (asset.type === 'ft') {
- const { transferData, meta } = get(
- waitForAll({
- transferData: canTransferAssetState({
- contractAddress: asset.contractAddress,
- contractName: asset.contractName,
- }),
- meta: assetMetaDataState({
- contractAddress: asset.contractAddress,
- contractName: asset.contractName,
- }),
- })
- );
-
- const canTransfer = !(!transferData || 'error' in transferData);
- const hasMemo = transferData && !('error' in transferData) && transferData.hasMemo;
- return { ...asset, meta, canTransfer, hasMemo } as AssetWithMeta;
- }
- return asset as AssetWithMeta;
- },
-});
-
-export const assetsState = selector({
- key: 'assets.base',
- get: async ({ get }) => {
- const balance = get(accountBalancesState);
- if (!balance) return;
- const assets = transformAssets(balance);
- return get(waitForAll(assets.map(asset => assetItemState(asset))));
+ if (!contractInterface) return;
+ return isSip10Transfer(contractInterface);
+ });
+ anAtom.debugLabel = `transferDataState/${contractAddress}.${contractName}`;
+ return anAtom;
},
+ deepEqual
+);
+
+const assetItemState = atomFamily(asset => {
+ const anAtom = atom(get => {
+ if (asset.type === 'ft') {
+ const transferData = get(
+ transferDataState({
+ contractName: asset.contractName,
+ contractAddress: asset.contractAddress,
+ })
+ );
+ const meta = get(
+ assetMetaDataState({
+ contractAddress: asset.contractAddress,
+ contractName: asset.contractName,
+ })
+ );
+ const canTransfer = !(!transferData || 'error' in transferData);
+ const hasMemo = transferData && !('error' in transferData) && transferData.hasMemo;
+ return { ...asset, meta, canTransfer, hasMemo } as AssetWithMeta;
+ }
+ return asset as AssetWithMeta;
+ });
+ anAtom.debugLabel = `assetItemState/${asset.contractAddress}.${asset.contractName}::${asset.name}`;
+ return anAtom;
+}, deepEqual);
+
+export const baseAssetsState = atom(get => {
+ const balance = get(accountBalancesState);
+ return balance ? transformAssets(balance) : undefined;
});
-export const transferableAssetsState = selector({
- key: 'assets.transferable',
- get: ({ get }) => get(assetsState)?.filter(asset => asset.canTransfer),
+export const assetsState = atom(get => {
+ const assets = get(baseAssetsState) || [];
+ return get(waitForAll(assets.map(assetItemState)));
});
-export const fungibleTokensState = selector({
- key: 'assets.ft',
- get: ({ get }) => {
- const assets = get(assetsState);
- return assets?.filter(asset => asset.type === 'ft');
- },
+export const transferableAssetsState = atom(get =>
+ get(assetsState)?.filter(asset => asset.canTransfer)
+);
+
+export const fungibleTokensState = atom(get => {
+ const assets = get(assetsState);
+ return assets?.filter(asset => asset.type === 'ft');
});
-export const nonFungibleTokensState = selector({
- key: 'assets.nft',
- get: ({ get }) => {
- const assets = get(assetsState);
- return assets?.filter(asset => asset.type !== 'nft');
- },
+export const nonFungibleTokensState = atom(get => {
+ const assets = get(assetsState);
+ return assets?.filter(asset => asset.type !== 'nft');
});
-export const stxTokenState = selector({
- key: 'assets.stx',
- get: ({ get }) => {
- const balances = get(accountBalancesState);
- if (!balances || balances.stx.balance === '0') return;
- return {
- type: 'stx',
- contractAddress: '',
- balance: balances.stx.balance,
- subtitle: 'STX',
- name: 'Stacks Token',
- } as AssetWithMeta;
- },
+export const stxTokenState = atom(get => {
+ const balances = get(accountBalancesState);
+ if (!balances || balances.stx.balance === '0') return;
+ return {
+ type: 'stx',
+ contractAddress: '',
+ balance: balances.stx.balance,
+ subtitle: 'STX',
+ name: 'Stacks Token',
+ } as AssetWithMeta;
});
+
+baseAssetsState.debugLabel = 'baseAssetsState';
+assetsState.debugLabel = 'assetsState';
+transferableAssetsState.debugLabel = 'transferableAssetsState';
+fungibleTokensState.debugLabel = 'fungibleTokensState';
+nonFungibleTokensState.debugLabel = 'nonFungibleTokensState';
+stxTokenState.debugLabel = 'stxTokenState';
diff --git a/src/store/assets/utils.ts b/src/store/assets/utils.ts
index 728747dd..54e89e04 100644
--- a/src/store/assets/utils.ts
+++ b/src/store/assets/utils.ts
@@ -5,7 +5,7 @@ import { SIP_010 } from '@common/constants';
import { fetcher } from '@common/api/wrapped-fetch';
import { AddressBalanceResponse } from '@blockstack/stacks-blockchain-api-types';
import { getAssetStringParts, truncateMiddle } from '@stacks/ui-utils';
-import { Asset, FungibleTokenOptions, MetaDataNames } from '@store/assets/types';
+import { Asset, FtMeta, FungibleTokenOptions, MetaDataNames } from '@store/assets/types';
async function callReadOnlyFunction({
contractName,
@@ -69,21 +69,17 @@ export async function fetchFungibleTokenMetaData({
name: string;
symbol: string;
};
-}) {
- try {
- const [name, symbol, decimals] = await Promise.all([
- fetchName({ ...options, functionName: methods.name }),
- fetchSymbol({ ...options, functionName: methods.symbol }),
- fetchDecimals({ ...options, functionName: methods.decimals }),
- ]);
- return {
- name,
- symbol,
- decimals,
- };
- } catch (e) {
- return null;
- }
+}): Promise {
+ const [name, symbol, decimals] = await Promise.all([
+ fetchName({ ...options, functionName: methods.name }),
+ fetchSymbol({ ...options, functionName: methods.symbol }),
+ fetchDecimals({ ...options, functionName: methods.decimals }),
+ ]);
+ return {
+ name,
+ symbol,
+ decimals,
+ } as FtMeta;
}
function makeKey(networkUrl: string, address: string, name: string, key: string): string {
diff --git a/src/store/common/api-clients.ts b/src/store/common/api-clients.ts
index b9bb61aa..970aae72 100644
--- a/src/store/common/api-clients.ts
+++ b/src/store/common/api-clients.ts
@@ -1,4 +1,4 @@
-import { selector } from 'recoil';
+import { atom } from 'jotai';
import { currentNetworkState } from '@store/networks';
import {
Configuration,
@@ -9,50 +9,27 @@ import {
} from '@stacks/blockchain-api-client';
import { fetcher } from '@common/api/wrapped-fetch';
-enum API_CLIENT_KEYS {
- CONFIG = 'clients/CONFIG',
- SMART_CONTRACTS = 'clients/SMART_CONTRACTS',
- ACCOUNTS = 'clients/ACCOUNTS',
- INFO = 'clients/INFO',
- BLOCKS = 'clients/BLOCKS',
-}
-
-export const apiClientConfiguration = selector({
- key: API_CLIENT_KEYS.CONFIG,
- get: ({ get }) => {
- const network = get(currentNetworkState);
- return new Configuration({ basePath: network.url, fetchApi: fetcher });
- },
+export const apiClientConfiguration = atom(get => {
+ const network = get(currentNetworkState);
+ return new Configuration({ basePath: network.url, fetchApi: fetcher });
});
-export const smartContractClientState = selector({
- key: API_CLIENT_KEYS.SMART_CONTRACTS,
- get: ({ get }) => {
- const config = get(apiClientConfiguration);
- return new SmartContractsApi(config);
- },
+export const smartContractClientState = atom(get => {
+ const config = get(apiClientConfiguration);
+ return new SmartContractsApi(config);
});
-export const accountsApiClientState = selector({
- key: API_CLIENT_KEYS.ACCOUNTS,
- get: ({ get }) => {
- const config = get(apiClientConfiguration);
- return new AccountsApi(config);
- },
+export const accountsApiClientState = atom(get => {
+ const config = get(apiClientConfiguration);
+ return new AccountsApi(config);
});
-export const infoApiClientState = selector({
- key: API_CLIENT_KEYS.INFO,
- get: ({ get }) => {
- const config = get(apiClientConfiguration);
- return new InfoApi(config);
- },
+export const infoApiClientState = atom(get => {
+ const config = get(apiClientConfiguration);
+ return new InfoApi(config);
});
-export const blocksApiClientState = selector({
- key: API_CLIENT_KEYS.BLOCKS,
- get: ({ get }) => {
- const config = get(apiClientConfiguration);
- return new BlocksApi(config);
- },
+export const blocksApiClientState = atom(get => {
+ const config = get(apiClientConfiguration);
+ return new BlocksApi(config);
});
diff --git a/src/store/common/api-helpers.ts b/src/store/common/api-helpers.ts
deleted file mode 100644
index 14ad359c..00000000
--- a/src/store/common/api-helpers.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { atom, atomFamily } from 'recoil';
-
-enum KEYS {
- REVALIDATION_INDEX = 'api/REVALIDATION_INDEX',
- INTERVAL = 'clients/INTERVAL',
-}
-
-export const apiRevalidation = atom({
- key: KEYS.REVALIDATION_INDEX,
- default: 0,
-});
-
-export const intervalState = atomFamily({
- key: KEYS.INTERVAL,
- default: 0,
- effects_UNSTABLE: (intervalMilliseconds: number) => [
- ({ setSelf }) => {
- const interval = setInterval(() => {
- setSelf(current => {
- if (typeof current === 'number') {
- return current + 1;
- }
- return 1;
- });
- }, intervalMilliseconds);
- return () => {
- clearInterval(interval);
- };
- },
- ],
-});
diff --git a/src/store/common/utils.ts b/src/store/common/utils.ts
index 11acc02a..76546c35 100644
--- a/src/store/common/utils.ts
+++ b/src/store/common/utils.ts
@@ -1,67 +1,5 @@
-import { AtomEffect, DefaultValue } from 'recoil';
+import hash from 'object-hash';
-export const ATOM_LOCALSTORAGE_PREFIX = '__hiro-recoil-v2__';
-
-export const localStorageKey = (atomKey: string): string => {
- return `${ATOM_LOCALSTORAGE_PREFIX}${atomKey}`;
-};
-
-interface LocalStorageTransformer {
- serialize: (atom: T | DefaultValue) => string;
- deserialize: (serialized: string) => T;
-}
-
-export const guardRecoilDefaultValue = (
- candidate: DefaultValue | T
-): candidate is DefaultValue => {
- if (candidate instanceof DefaultValue) return true;
- return false;
-};
-
-interface LocalStorageEffectOptions {
- transformer?: LocalStorageTransformer;
-}
-export const localStorageEffect =
- ({ transformer }: LocalStorageEffectOptions = {}): AtomEffect =>
- ({ setSelf, onSet, node }) => {
- const key = localStorageKey(node.key);
- if (typeof window !== 'undefined') {
- const savedValue = localStorage.getItem(key);
- if (savedValue) {
- try {
- if (transformer) {
- setSelf(transformer.deserialize(savedValue));
- } else {
- window.requestAnimationFrame(() => {
- setSelf(JSON.parse(savedValue));
- });
- }
- } catch (error) {
- console.error(`Error when saving the recoil state ${key}.`, error);
- console.error('Recoil value:', savedValue);
- }
- }
-
- onSet(newValue => {
- if (transformer) {
- const serialized = transformer.serialize(newValue);
- if (serialized) {
- localStorage.setItem(key, serialized);
- } else {
- localStorage.removeItem(key);
- }
- } else {
- const doClear =
- guardRecoilDefaultValue(newValue) || newValue === null || newValue === undefined;
- if (doClear) {
- localStorage.removeItem(key);
- } else {
- localStorage.setItem(key, JSON.stringify(newValue));
- }
- }
- });
- }
- };
export function textToBytes(content: string) {
return new TextEncoder().encode(content);
}
@@ -69,3 +7,29 @@ export function textToBytes(content: string) {
export function bytesToText(buffer: Uint8Array) {
return new TextDecoder().decode(buffer);
}
+
+export function bytesToHex(uint8a: Uint8Array): string {
+ // pre-caching chars could speed this up 6x.
+ let hex = '';
+ for (let i = 0; i < uint8a.length; i++) {
+ hex += uint8a[i].toString(16).padStart(2, '0');
+ }
+ return hex;
+}
+
+export function makeLocalDataKey(params: string[]): string {
+ return hash(params);
+}
+
+export function getLocalData(params: string[]) {
+ const key = makeLocalDataKey(params);
+ const value = localStorage.getItem(key);
+ if (!value) return null;
+ return JSON.parse(value) as Data;
+}
+
+export function setLocalData(params: string[], data: Data): Data {
+ const key = makeLocalDataKey(params);
+ localStorage.setItem(key, JSON.stringify(data));
+ return data;
+}
diff --git a/src/store/contracts.ts b/src/store/contracts.ts
new file mode 100644
index 00000000..b6d27b05
--- /dev/null
+++ b/src/store/contracts.ts
@@ -0,0 +1,95 @@
+import { atomFamilyWithQuery } from '@store/query';
+import { ContractPrincipal } from '@store/assets/types';
+import { smartContractClientState } from '@store/common/api-clients';
+import { atomFamily } from 'jotai/utils';
+import { atom } from 'jotai';
+import { currentNetworkState } from '@store/networks';
+import { getLocalData, setLocalData } from '@store/common/utils';
+import { ContractInterface } from '@stacks/rpc-client';
+import deepEqual from 'fast-deep-equal';
+import { ContractSourceResponse } from '@stacks/blockchain-api-client';
+
+enum ContractQueryKeys {
+ ContractInterface = 'queries/ContractInterface',
+ ContractSource = 'queries/ContractSource',
+}
+
+export const contractInterfaceResponseState = atomFamilyWithQuery<
+ ContractPrincipal,
+ ContractInterface | null
+>(ContractQueryKeys.ContractInterface, async (get, { contractAddress, contractName }) => {
+ const client = get(smartContractClientState);
+ const network = get(currentNetworkState);
+ try {
+ const data = (await client.getContractInterface({
+ contractAddress,
+ contractName,
+ })) as ContractInterface;
+ const keyParams = [
+ network.url,
+ contractAddress,
+ contractName,
+ ContractQueryKeys.ContractInterface,
+ ];
+ // for a given contract interface, it does not change once deployed so we should cache it
+ return setLocalData(keyParams, data);
+ } catch (e) {
+ console.log('contractInterfaceResponseState error', e);
+ return null;
+ }
+});
+
+export const contractInterfaceState = atomFamily(
+ ({ contractAddress, contractName }) => {
+ const anAtom = atom(get => {
+ const network = get(currentNetworkState);
+ const keyParams = [
+ network.url,
+ contractAddress,
+ contractName,
+ ContractQueryKeys.ContractInterface,
+ ];
+ const localData = getLocalData(keyParams);
+ if (localData) return localData;
+ return get(contractInterfaceResponseState({ contractAddress, contractName }));
+ });
+ anAtom.debugLabel = `contractInterfaceState/${contractAddress}.${contractName}`;
+ return anAtom;
+ },
+ deepEqual
+);
+
+export const contractSourceResponseState = atomFamilyWithQuery<
+ ContractPrincipal,
+ ContractSourceResponse
+>(ContractQueryKeys.ContractSource, async (get, { contractAddress, contractName }) => {
+ const client = get(smartContractClientState);
+ const network = get(currentNetworkState);
+ const data = await client.getContractSource({
+ contractAddress,
+ contractName,
+ });
+ const keyParams = [network.url, contractAddress, contractName, ContractQueryKeys.ContractSource];
+ // for a given contract source, it does not change once deployed so we should cache it
+ return setLocalData(keyParams, data);
+});
+
+export const contractSourceState = atomFamily(
+ ({ contractAddress, contractName }) => {
+ const anAtom = atom(get => {
+ const network = get(currentNetworkState);
+ const keyParams = [
+ network.url,
+ contractAddress,
+ contractName,
+ ContractQueryKeys.ContractSource,
+ ];
+ const localData = getLocalData(keyParams);
+ if (localData) return localData;
+ return get(contractSourceResponseState({ contractAddress, contractName }));
+ });
+ anAtom.debugLabel = `contractSourceState/${contractAddress}.${contractName}`;
+ return anAtom;
+ },
+ deepEqual
+);
diff --git a/src/store/networks.ts b/src/store/networks.ts
index 0b6f9b18..2c4918c6 100644
--- a/src/store/networks.ts
+++ b/src/store/networks.ts
@@ -1,111 +1,76 @@
-import { atom, DefaultValue, selector, waitForAll } from 'recoil';
-import { localStorageEffect } from './common/utils';
+import { atomWithStorage, waitForAll } from 'jotai/utils';
+import { atom } from 'jotai';
import { ChainID } from '@stacks/transactions';
import { StacksMainnet, StacksNetwork, StacksTestnet } from '@stacks/network';
-import { apiRevalidation } from '@store/common/api-helpers';
import { transactionRequestNetwork } from '@store/transactions/requests';
import { findMatchingNetworkKey } from '@common/utils';
import { defaultNetworks, Networks } from '@common/constants';
import { blocksApiClientState, infoApiClientState } from '@store/common/api-clients';
-enum KEYS {
- NETWORKS = 'network/NETWORKS',
- CURRENT_KEY = 'network/CURRENT_KEY',
- CURRENT_KEY_DEFAULT = 'network/CURRENT_KEY_DEFAULT',
- CURRENT_NETWORK = 'network/CURRENT_NETWORK',
- STACKS_NETWORK = 'network/STACKS_NETWORK',
- LATEST_BLOCK_HEIGHT = 'network/LATEST_BLOCK_HEIGHT',
- INFO = 'network/INFO',
-}
-
// Our root networks list, users can add to this list and it will persist to localstorage
-export const networksState = atom({
- key: KEYS.NETWORKS,
- default: defaultNetworks,
- effects_UNSTABLE: [localStorageEffect()],
-});
+export const networksState = atomWithStorage('networks', defaultNetworks);
// the current key selected
// if there is a pending transaction request, it will default to the network passed (if included)
// else it will default to the persisted key or default (mainnet)
-export const currentNetworkKeyState = atom({
- key: KEYS.CURRENT_KEY,
- default: selector({
- key: KEYS.CURRENT_KEY_DEFAULT,
- get: ({ get }) => {
- const { networks, txNetwork } = get(
- waitForAll({
- networks: networksState,
- txNetwork: transactionRequestNetwork,
- })
- );
- if (txNetwork) {
- const newKey = findMatchingNetworkKey(txNetwork, networks);
- if (newKey) return newKey;
- }
- const savedValue = localStorage.getItem(KEYS.CURRENT_KEY);
- if (savedValue) {
- try {
- return JSON.parse(savedValue);
- } catch (e) {}
- }
- return 'mainnet';
- },
- }),
- effects_UNSTABLE: [
- ({ onSet }) => {
- onSet(newValue => {
- if (newValue instanceof DefaultValue) {
- localStorage.removeItem(KEYS.CURRENT_KEY);
- } else {
- localStorage.setItem(KEYS.CURRENT_KEY, JSON.stringify(newValue));
- }
- });
- },
- ],
-});
-// the `Network` object for the current key selected
-export const currentNetworkState = selector({
- key: KEYS.CURRENT_NETWORK,
- get: ({ get }) => {
- const { networks, key } = get(
+const localCurrentNetworkKeyState = atomWithStorage('networkKey', 'mainnet');
+export const currentNetworkKeyState = atom(
+ get => {
+ const { networks, txNetwork } = get(
waitForAll({
networks: networksState,
- key: currentNetworkKeyState,
+ txNetwork: transactionRequestNetwork,
})
);
- return networks[key];
+ // if txNetwork, default to this always, users cannot currently change networks when signing a transaction
+ // @see https://github.com/blockstack/stacks-wallet-web/issues/1281
+ if (txNetwork) {
+ const newKey = findMatchingNetworkKey(txNetwork, networks);
+ if (newKey) return newKey;
+ }
+ // otherwise default to the locally saved network key state
+ return get(localCurrentNetworkKeyState);
},
+ (_get, set, update) => {
+ if (update) set(localCurrentNetworkKeyState, update);
+ }
+);
+
+// the `Network` object for the current key selected
+export const currentNetworkState = atom(get => {
+ const { networks, key } = get(
+ waitForAll({
+ networks: networksState,
+ key: currentNetworkKeyState,
+ })
+ );
+ return networks[key];
});
// a `StacksNetwork` instance using the current network
-export const currentStacksNetworkState = selector({
- key: KEYS.STACKS_NETWORK,
- get: ({ get }) => {
- const network = get(currentNetworkState);
- const stacksNetwork =
- network.chainId === ChainID.Testnet ? new StacksTestnet() : new StacksMainnet();
- stacksNetwork.coreApiUrl = network.url;
- stacksNetwork.bnsLookupUrl = network.url;
- return stacksNetwork;
- },
+export const currentStacksNetworkState = atom(get => {
+ const network = get(currentNetworkState);
+ const stacksNetwork =
+ network.chainId === ChainID.Testnet ? new StacksTestnet() : new StacksMainnet();
+ stacksNetwork.coreApiUrl = network.url;
+ stacksNetwork.bnsLookupUrl = network.url;
+ return stacksNetwork;
});
// external data, the most recent block height of the selected network
-export const latestBlockHeightState = selector({
- key: KEYS.LATEST_BLOCK_HEIGHT,
- get: async ({ get }) => {
- const client = get(blocksApiClientState);
- return (await client.getBlockList({}))?.results?.[0]?.height;
- },
+export const latestBlockHeightState = atom(async get => {
+ const client = get(blocksApiClientState);
+ return (await client.getBlockList({}))?.results?.[0]?.height;
});
// external data, `v2/info` endpoint of the selected network
-export const networkInfoState = selector({
- key: KEYS.INFO,
- get: async ({ get }) => {
- get(apiRevalidation);
- return get(infoApiClientState).getCoreApiInfo();
- },
-});
+export const networkInfoState = atom(get => get(infoApiClientState).getCoreApiInfo());
+
+networksState.debugLabel = 'networksState';
+localCurrentNetworkKeyState.debugLabel = 'localCurrentNetworkKeyState';
+currentNetworkKeyState.debugLabel = 'currentNetworkKeyState';
+currentNetworkState.debugLabel = 'currentNetworkState';
+currentStacksNetworkState.debugLabel = 'currentStacksNetworkState';
+latestBlockHeightState.debugLabel = 'latestBlockHeightState';
+networkInfoState.debugLabel = 'networkInfoState';
diff --git a/src/store/onboarding.ts b/src/store/onboarding.ts
index 2b9cc4ec..3107650f 100644
--- a/src/store/onboarding.ts
+++ b/src/store/onboarding.ts
@@ -1,4 +1,6 @@
-import { atom } from 'recoil';
+import { atom } from 'jotai';
+import { atomWithDefault } from 'jotai/utils';
+
import { ScreenPaths } from '@store/common/types';
import { DecodedAuthRequest } from '@common/dev/types';
@@ -16,50 +18,6 @@ export interface OnboardingState {
onboardingPath?: ScreenPaths;
}
-export const magicRecoveryCodePasswordState = atom({
- key: 'seed.magic-recovery-code.password',
- default: '',
-});
-
-export const seedInputState = atom({
- key: 'seed.input',
- default: '',
-});
-
-export const seedInputErrorState = atom({
- key: 'seed.input.error',
- default: undefined,
-});
-
-export const currentScreenState = atom({
- key: 'onboarding.screen',
- default: ScreenPaths.GENERATION,
-});
-
-export const secretKeyState = atom({
- key: 'onboarding.secretKey',
- default: null,
-});
-
-export const magicRecoveryCodeState = atom({
- key: 'onboarding.magicRecoveryCode',
- default: null,
-});
-
-export const onboardingProgressState = atom({
- key: 'onboarding.progress',
- default: false,
-});
-
-export const usernameState = atom({
- key: 'onboarding.username',
- default: null,
-});
-export const onboardingPathState = atom({
- key: 'onboarding.path',
- default: null,
-});
-
interface AuthRequestState {
authRequest?: string;
decodedAuthRequest?: DecodedAuthRequest;
@@ -68,13 +26,30 @@ interface AuthRequestState {
appURL?: URL;
}
+export const magicRecoveryCodePasswordState = atom('');
+export const seedInputState = atom('');
+export const seedInputErrorState = atom(undefined);
+export const secretKeyState = atomWithDefault(() => null);
+export const currentScreenState = atom(ScreenPaths.GENERATION);
+export const magicRecoveryCodeState = atomWithDefault(() => null);
+export const onboardingProgressState = atom(false);
+export const usernameState = atomWithDefault(() => null);
+export const onboardingPathState = atomWithDefault(() => null);
export const authRequestState = atom({
- key: 'onboarding.authRequest',
- default: {
- authRequest: undefined,
- decodedAuthRequest: undefined,
- appName: undefined,
- appIcon: undefined,
- appURL: undefined,
- },
+ authRequest: undefined,
+ decodedAuthRequest: undefined,
+ appName: undefined,
+ appIcon: undefined,
+ appURL: undefined,
});
+
+magicRecoveryCodePasswordState.debugLabel = 'magicRecoveryCodePasswordState';
+seedInputState.debugLabel = 'seedInputState';
+seedInputErrorState.debugLabel = 'seedInputErrorState';
+secretKeyState.debugLabel = 'secretKeyState';
+currentScreenState.debugLabel = 'currentScreenState';
+magicRecoveryCodeState.debugLabel = 'magicRecoveryCodeState';
+onboardingProgressState.debugLabel = 'onboardingProgressState';
+usernameState.debugLabel = 'usernameState';
+onboardingPathState.debugLabel = 'onboardingPathState';
+authRequestState.debugLabel = 'authRequestState';
diff --git a/src/store/query.ts b/src/store/query.ts
new file mode 100644
index 00000000..39dc780a
--- /dev/null
+++ b/src/store/query.ts
@@ -0,0 +1,63 @@
+import { Atom, Getter } from 'jotai';
+import { atomFamily } from 'jotai/utils';
+import { atomWithQuery } from 'jotai/query';
+import deepEqual from 'fast-deep-equal';
+import { QueryObserverOptions } from 'react-query';
+
+const withInterval = (enabled: boolean) =>
+ enabled
+ ? {
+ refetchInterval: 10000,
+ }
+ : {};
+
+export const queryAtom = (
+ key: string,
+ queryFn: (get: Getter) => Data | Promise,
+ enableInterval = false,
+ equalityFn: (a: Data, b: Data) => boolean = Object.is
+) => {
+ const anAtom = atomWithQuery(
+ get => ({
+ queryKey: key,
+ queryFn: () => queryFn(get),
+ keepPreviousData: true,
+ refetchOnReconnect: true,
+ refetchOnWindowFocus: true,
+ refetchOnMount: true,
+ ...withInterval(enableInterval),
+ }),
+ equalityFn
+ );
+ anAtom.debugLabel = `queryAtom/${key}`;
+ return anAtom;
+};
+
+export const atomFamilyWithQuery = (
+ key: string,
+ queryFn: (get: Getter, param: Param) => Promise,
+ options: {
+ enableInterval?: boolean;
+ equalityFn?: (a: Data, b: Data) => boolean;
+ onError?: QueryObserverOptions['onError'];
+ } = {}
+): ((param: Param) => Atom) => {
+ const { enableInterval = false, equalityFn = deepEqual } = options;
+ return atomFamily(param => {
+ const anAtom = atomWithQuery(
+ get => ({
+ queryKey: [key, param],
+ queryFn: () => queryFn(get, param),
+ useErrorBoundary: true,
+ keepPreviousData: true,
+ refetchOnReconnect: true,
+ refetchOnWindowFocus: true,
+ refetchOnMount: true,
+ ...withInterval(enableInterval),
+ }),
+ equalityFn
+ );
+ anAtom.debugLabel = `atomFamilyWithQuery/${key}`;
+ return anAtom;
+ }, deepEqual);
+};
diff --git a/src/store/transactions/contract-call.ts b/src/store/transactions/contract-call.ts
index 679289e0..95bfcb94 100644
--- a/src/store/transactions/contract-call.ts
+++ b/src/store/transactions/contract-call.ts
@@ -1,89 +1,71 @@
-import { selector, waitForAll } from 'recoil';
+import { atom } from 'jotai';
+import { waitForAll } from 'jotai/utils';
import { requestTokenPayloadState } from '@store/transactions/requests';
-import { smartContractClientState } from '@store/common/api-clients';
import { TransactionTypes } from '@stacks/connect';
import { ContractInterfaceResponse } from '@stacks/blockchain-api-client';
import { ContractInterfaceFunction } from '@stacks/rpc-client';
-
-enum KEYS {
- CONTRACT_INTERFACE = 'transactions/CONTRACT_INTERFACE',
- CONTRACT_SOURCE = 'transactions/CONTRACT_SOURCE',
- FUNCTION = 'transactions/FUNCTION',
-}
+import { contractInterfaceState, contractSourceState } from '@store/contracts';
type ContractInterfaceResponseWithFunctions = Omit & {
functions: ContractInterfaceFunction[];
};
-export const transactionContractInterfaceState = selector<
+export const transactionContractInterfaceState = atom<
undefined | ContractInterfaceResponseWithFunctions
->({
- key: KEYS.CONTRACT_INTERFACE,
- get: async ({ get }) => {
- const { payload, client } = get(
- waitForAll({
- payload: requestTokenPayloadState,
- client: smartContractClientState,
- })
- );
-
- if (payload?.txType !== TransactionTypes.ContractCall) return;
- try {
- const data = await client.getContractInterface({
+>(async get => {
+ const payload = get(requestTokenPayloadState);
+ if (payload?.txType !== TransactionTypes.ContractCall) return;
+ try {
+ const data = get(
+ contractInterfaceState({
contractName: payload.contractName,
contractAddress: payload.contractAddress,
- });
- if (!data) return undefined;
- return data as ContractInterfaceResponseWithFunctions;
- } catch (e) {
- return undefined;
- }
- },
-});
-
-export const transactionContractSourceState = selector({
- key: KEYS.CONTRACT_SOURCE,
- get: async ({ get }) => {
- const { payload, client } = get(
- waitForAll({
- payload: requestTokenPayloadState,
- client: smartContractClientState,
})
);
+ if (!data) return undefined;
+ return data as ContractInterfaceResponseWithFunctions;
+ } catch (e) {
+ return undefined;
+ }
+});
- if (payload?.txType !== TransactionTypes.ContractCall) return;
-
- try {
- return client.getContractSource({
+export const transactionContractSourceState = atom(get => {
+ const payload = get(requestTokenPayloadState);
+ if (payload?.txType !== TransactionTypes.ContractCall) return;
+ try {
+ return get(
+ contractSourceState({
contractName: payload.contractName,
contractAddress: payload.contractAddress,
- });
- } catch (e) {
- return undefined;
- }
- },
-});
-export const transactionFunctionsState = selector({
- key: KEYS.FUNCTION,
- get: ({ get }) => {
- const { payload, contractInterface } = get(
- waitForAll({
- payload: requestTokenPayloadState,
- contractInterface: transactionContractInterfaceState,
})
);
-
- if (!payload || payload.txType !== 'contract_call' || !contractInterface) return undefined;
-
- const selectedFunction = contractInterface.functions.find(func => {
- return func.name === payload.functionName;
- });
- if (!selectedFunction) {
- throw new Error(
- `Attempting to call a function (\`${payload.functionName}\`) that ` +
- `does not exist on contract ${payload.contractAddress}.${payload.contractName}`
- );
- }
- return selectedFunction;
- },
+ } catch (e) {
+ return undefined;
+ }
});
+
+export const transactionFunctionsState = atom(get => {
+ const { payload, contractInterface } = get(
+ waitForAll({
+ payload: requestTokenPayloadState,
+ contractInterface: transactionContractInterfaceState,
+ })
+ );
+
+ if (!payload || payload.txType !== 'contract_call' || !contractInterface) return undefined;
+
+ const selectedFunction = contractInterface.functions.find(func => {
+ return func.name === payload.functionName;
+ });
+ if (!selectedFunction) {
+ throw new Error(
+ `Attempting to call a function (\`${payload.functionName}\`) that ` +
+ `does not exist on contract ${payload.contractAddress}.${payload.contractName}`
+ );
+ }
+ return selectedFunction;
+});
+
+transactionContractInterfaceState.debugLabel = 'transactionContractInterfaceState';
+transactionContractSourceState.debugLabel = 'transactionContractSourceState';
+transactionFunctionsState.debugLabel = 'transactionFunctionsState';
diff --git a/src/store/transactions/fungible-token-transfer.ts b/src/store/transactions/fungible-token-transfer.ts
index b160555b..dfccf2ac 100644
--- a/src/store/transactions/fungible-token-transfer.ts
+++ b/src/store/transactions/fungible-token-transfer.ts
@@ -1,4 +1,4 @@
-import { selector, waitForAll } from 'recoil';
+import { atom } from 'jotai';
import { selectedAssetStore } from '@store/assets/asset-search';
import {
accountBalancesState,
@@ -7,39 +7,35 @@ import {
} from '@store/accounts';
import { currentStacksNetworkState } from '@store/networks';
import { correctNonceState } from '@store/accounts/nonce';
+import { waitForAll } from 'jotai/utils';
-enum KEYS {
- ASSET_STATE = 'transaction/ASSET_STATE',
-}
+export const makeFungibleTokenTransferState = atom(get => {
+ const { asset, currentAccount, network, balances, stxAddress, nonce } = get(
+ waitForAll({
+ asset: selectedAssetStore,
+ currentAccount: currentAccountState,
+ network: currentStacksNetworkState,
+ balances: accountBalancesState,
+ stxAddress: currentAccountStxAddressState,
+ nonce: correctNonceState,
+ })
+ );
-export const makeFungibleTokenTransferState = selector({
- key: KEYS.ASSET_STATE,
- get: ({ get }) => {
- const { asset, currentAccount, network, balances, stxAddress, nonce } = get(
- waitForAll({
- asset: selectedAssetStore,
- currentAccount: currentAccountState,
- network: currentStacksNetworkState,
- balances: accountBalancesState,
- stxAddress: currentAccountStxAddressState,
- nonce: correctNonceState,
- })
- );
-
- if (asset && currentAccount && stxAddress) {
- const { contractName, contractAddress, name: assetName } = asset;
- return {
- asset,
- stxAddress,
- nonce,
- balances,
- network,
- senderKey: currentAccount.stxPrivateKey,
- assetName,
- contractAddress,
- contractName,
- };
- }
- return;
- },
+ if (asset && currentAccount && stxAddress) {
+ const { contractName, contractAddress, name: assetName } = asset;
+ return {
+ asset,
+ stxAddress,
+ nonce,
+ balances,
+ network,
+ senderKey: currentAccount.stxPrivateKey,
+ assetName,
+ contractAddress,
+ contractName,
+ };
+ }
+ return;
});
+
+makeFungibleTokenTransferState.debugLabel = 'makeFungibleTokenTransferState';
diff --git a/src/store/transactions/index.ts b/src/store/transactions/index.ts
index 07c3ec61..4dce9379 100644
--- a/src/store/transactions/index.ts
+++ b/src/store/transactions/index.ts
@@ -1,4 +1,6 @@
-import { atom, selector, waitForAll } from 'recoil';
+import { atom } from 'jotai';
+import { waitForAll } from 'jotai/utils';
+
import { AuthType, ChainID, TransactionVersion } from '@stacks/transactions';
import { currentNetworkState, currentStacksNetworkState } from '@store/networks';
@@ -11,104 +13,94 @@ import { getPostCondition, handlePostConditions } from '@common/transactions/pos
import { TransactionPayload } from '@stacks/connect';
import { stacksTransactionToHex } from '@common/transactions/transaction-utils';
-enum KEYS {
- POST_CONDITIONS = 'transactions/POST_CONDITIONS',
- PENDING_TRANSACTION = 'transactions/PENDING_TRANSACTION',
- ATTACHMENT = 'transactions/ATTACHMENT',
- SIGNED_TRANSACTION = 'transactions/SIGNED_TRANSACTION',
- TX_VERSION = 'transactions/TX_VERSION',
- ERROR_IS_UNAUTHORIZED = 'transactions/ERROR_IS_UNAUTHORIZED',
- ERROR_BROADCAST_FAILURE = 'transactions/ERROR_BROADCAST_FAILURE',
-}
+export const postConditionsState = atom(get => {
+ const { payload, address } = get(
+ waitForAll({
+ payload: requestTokenPayloadState,
+ address: currentAccountStxAddressState,
+ })
+ );
-export const postConditionsState = selector({
- key: KEYS.POST_CONDITIONS,
- get: ({ get }) => {
- const { payload, address } = get(
- waitForAll({
- payload: requestTokenPayloadState,
- address: currentAccountStxAddressState,
- })
- );
+ if (!payload || !address) return;
- if (!payload || !address) return;
+ if (payload.postConditions) {
+ if (payload.stxAddress)
+ return handlePostConditions(payload.postConditions, payload.stxAddress, address);
- if (payload.postConditions) {
- if (payload.stxAddress)
- return handlePostConditions(payload.postConditions, payload.stxAddress, address);
-
- return payload.postConditions.map(getPostCondition);
- }
- return [];
- },
+ return payload.postConditions.map(getPostCondition);
+ }
+ return [];
});
-export const pendingTransactionState = selector({
- key: KEYS.PENDING_TRANSACTION,
- get: ({ get }) => {
- const { payload, postConditions, network } = get(
- waitForAll({
- payload: requestTokenPayloadState,
- postConditions: postConditionsState,
- network: currentStacksNetworkState,
- })
- );
- if (!payload) return;
- return { ...payload, postConditions, network };
- },
+export const pendingTransactionState = atom(get => {
+ const { payload, postConditions, network } = get(
+ waitForAll({
+ payload: requestTokenPayloadState,
+ postConditions: postConditionsState,
+ network: currentStacksNetworkState,
+ })
+ );
+ if (!payload) return;
+ return { ...payload, postConditions, network };
});
-export const transactionAttachmentState = selector({
- key: KEYS.ATTACHMENT,
- get: ({ get }) => get(pendingTransactionState)?.attachment,
+export const transactionAttachmentState = atom(get => get(pendingTransactionState)?.attachment);
+
+export const signedStacksTransactionState = atom(get => {
+ const { account, txData, nonce } = get(
+ waitForAll({
+ account: currentAccountState,
+ txData: pendingTransactionState,
+ nonce: correctNonceState,
+ })
+ );
+ if (!account || !txData) return;
+ return generateSignedTransaction({
+ senderKey: account.stxPrivateKey,
+ nonce,
+ txData,
+ });
});
-export const signedTransactionState = selector({
- key: KEYS.SIGNED_TRANSACTION,
- get: async ({ get }) => {
- const { account, pendingTransaction, nonce } = get(
- waitForAll({
- account: currentAccountState,
- pendingTransaction: pendingTransactionState,
- nonce: correctNonceState,
- })
- );
-
- if (!account || !pendingTransaction) return;
-
- const signedTransaction = await generateSignedTransaction({
- senderKey: account.stxPrivateKey,
- nonce,
- txData: pendingTransaction,
- });
- const serialized = signedTransaction?.serialize();
- const txRaw = stacksTransactionToHex(signedTransaction);
- return {
- serialized,
- isSponsored: signedTransaction?.auth?.authType === AuthType.Sponsored,
- nonce: signedTransaction?.auth.spendingCondition?.nonce.toNumber(),
- fee: signedTransaction?.auth.spendingCondition?.fee?.toNumber(),
- txRaw,
- };
- },
+export const signedTransactionState = atom(get => {
+ const signedTransaction = get(signedStacksTransactionState);
+ console.log(signedTransaction);
+ if (!signedTransaction) return;
+ const serialized = signedTransaction.serialize();
+ const txRaw = stacksTransactionToHex(signedTransaction);
+ return {
+ serialized,
+ isSponsored: signedTransaction?.auth?.authType === AuthType.Sponsored,
+ nonce: signedTransaction?.auth.spendingCondition?.nonce.toNumber(),
+ fee: signedTransaction?.auth.spendingCondition?.fee?.toNumber(),
+ txRaw,
+ };
});
-export const transactionNetworkVersionState = selector({
- key: KEYS.TX_VERSION,
- get: ({ get }) =>
- get(currentNetworkState).chainId === ChainID.Mainnet
- ? TransactionVersion.Mainnet
- : TransactionVersion.Testnet,
+export const transactionFeeState = atom(get => {
+ return get(signedTransactionState)?.fee;
});
+export const transactionSponsoredState = atom(get => {
+ return get(signedTransactionState)?.isSponsored;
+});
+export const transactionNetworkVersionState = atom(get =>
+ get(currentNetworkState)?.chainId === ChainID.Mainnet
+ ? TransactionVersion.Mainnet
+ : TransactionVersion.Testnet
+);
export type TransactionPayloadWithAttachment = TransactionPayload & {
attachment?: string;
};
-export const isUnauthorizedTransactionState = atom({
- key: KEYS.ERROR_IS_UNAUTHORIZED,
- default: false,
-});
-export const transactionBroadcastErrorState = atom({
- key: KEYS.ERROR_BROADCAST_FAILURE,
- default: null,
-});
+export const isUnauthorizedTransactionState = atom(false);
+export const transactionBroadcastErrorState = atom(null);
+
+// dev tooling
+postConditionsState.debugLabel = 'postConditionsState';
+pendingTransactionState.debugLabel = 'pendingTransactionState';
+transactionAttachmentState.debugLabel = 'transactionAttachmentState';
+signedStacksTransactionState.debugLabel = 'signedStacksTransactionState';
+signedTransactionState.debugLabel = 'signedTransactionState';
+transactionNetworkVersionState.debugLabel = 'transactionNetworkVersionState';
+isUnauthorizedTransactionState.debugLabel = 'isUnauthorizedTransactionState';
+transactionBroadcastErrorState.debugLabel = 'transactionBroadcastErrorState';
diff --git a/src/store/transactions/requests.ts b/src/store/transactions/requests.ts
index 177a0363..6eec6e07 100644
--- a/src/store/transactions/requests.ts
+++ b/src/store/transactions/requests.ts
@@ -1,89 +1,58 @@
-import { atom, selector, waitForAll } from 'recoil';
+import { atom } from 'jotai';
+import { waitForAll } from 'jotai/utils';
import { getPayloadFromToken } from '@store/transactions/utils';
import { walletState } from '@store/wallet';
import { verifyTxRequest } from '@common/transactions/requests';
import { getRequestOrigin, StorageKey } from '@common/storage';
+import { atomWithParam } from '@common/atom-with-params';
-enum KEYS {
- REQUEST_TOKEN = 'requests/REQUEST_TOKEN',
- REQUEST_TOKEN_ORIGIN = 'requests/REQUEST_TOKEN_ORIGIN',
- REQUEST_TOKEN_VALIDATION = 'requests/REQUEST_TOKEN_VALIDATION',
- REQUEST_TOKEN_PAYLOAD = 'requests/REQUEST_TOKEN_PAYLOAD',
- ADDRESS = 'requests/ADDRESS',
- NETWORK = 'requests/NETWORK',
-}
+export const requestTokenState = atomWithParam('transaction?request', null);
-export const requestTokenState = atom({
- key: KEYS.REQUEST_TOKEN,
- default: null,
- effects_UNSTABLE: [
- ({ setSelf, trigger }) => {
- if (trigger === 'get') {
- const requestToken = window.location.href?.split('?request=')[1];
- if (requestToken) {
- setSelf(requestToken);
- }
- }
-
- return () => {
- setSelf(null);
- };
- },
- ],
+export const requestTokenPayloadState = atom(get => {
+ const token = get(requestTokenState);
+ return token ? getPayloadFromToken(token) : null;
});
-export const requestTokenPayloadState = selector({
- key: KEYS.REQUEST_TOKEN_PAYLOAD,
- get: ({ get }) => {
- const token = get(requestTokenState);
- return token ? getPayloadFromToken(token) : null;
- },
+export const requestTokenOriginState = atom(get => {
+ const token = get(requestTokenState);
+ if (!token) return;
+ try {
+ return getRequestOrigin(StorageKey.transactionRequests, token);
+ } catch (e) {
+ console.error(e);
+ return false;
+ }
});
-export const requestTokenOriginState = selector({
- key: KEYS.REQUEST_TOKEN_ORIGIN,
- get: ({ get }) => {
- const token = get(requestTokenState);
- if (!token) return;
- try {
- return getRequestOrigin(StorageKey.transactionRequests, token);
- } catch (e) {
- console.error(e);
- return false;
- }
- },
+export const transactionRequestValidationState = atom(async get => {
+ const { requestToken, wallet, origin } = get(
+ waitForAll({
+ requestToken: requestTokenState,
+ wallet: walletState,
+ origin: requestTokenOriginState,
+ })
+ );
+ if (!origin || !wallet || !requestToken) return;
+ try {
+ const valid = await verifyTxRequest({
+ requestToken,
+ wallet,
+ appDomain: origin,
+ });
+ return !!valid;
+ } catch (e) {
+ return false;
+ }
});
-export const transactionRequestValidationState = selector({
- key: KEYS.REQUEST_TOKEN_VALIDATION,
- get: async ({ get }) => {
- const { requestToken, wallet, origin } = get(
- waitForAll({
- requestToken: requestTokenState,
- wallet: walletState,
- origin: requestTokenOriginState,
- })
- );
- if (!origin || !wallet || !requestToken) return;
- try {
- const valid = await verifyTxRequest({
- requestToken,
- wallet,
- appDomain: origin,
- });
- return !!valid;
- } catch (e) {
- return false;
- }
- },
-});
+export const transactionRequestStxAddressState = atom(
+ get => get(requestTokenPayloadState)?.stxAddress
+);
-export const transactionRequestStxAddressState = selector({
- key: KEYS.ADDRESS,
- get: ({ get }) => get(requestTokenPayloadState)?.stxAddress,
-});
+export const transactionRequestNetwork = atom(get => get(requestTokenPayloadState)?.network);
-export const transactionRequestNetwork = selector({
- key: KEYS.NETWORK,
- get: ({ get }) => get(requestTokenPayloadState)?.network,
-});
+requestTokenPayloadState.debugLabel = 'requestTokenPayloadState';
+requestTokenOriginState.debugLabel = 'requestTokenOriginState';
+transactionRequestValidationState.debugLabel = 'transactionRequestValidationState';
+transactionRequestStxAddressState.debugLabel = 'transactionRequestStxAddressState';
+transactionRequestNetwork.debugLabel = 'transactionRequestNetwork';
diff --git a/src/store/ui.ts b/src/store/ui.ts
index 5d8fddf3..bf021811 100644
--- a/src/store/ui.ts
+++ b/src/store/ui.ts
@@ -1,4 +1,5 @@
-import { atomFamily, DefaultValue, AtomEffect, atom } from 'recoil';
+import { atomFamily, atomWithStorage } from 'jotai/utils';
+import { atom } from 'jotai';
export enum AccountStep {
Switch = 'switch',
@@ -6,49 +7,30 @@ export enum AccountStep {
Username = 'username',
}
-const localStorageEffect =
- (key: string): AtomEffect =>
- ({ setSelf, onSet }) => {
- const savedValue = localStorage.getItem(key);
- if (savedValue != null) {
- setSelf(JSON.parse(savedValue));
- }
- onSet(newValue => {
- if (newValue instanceof DefaultValue) {
- localStorage.removeItem(key);
- } else {
- localStorage.setItem(key, JSON.stringify(newValue));
- }
- });
- };
-
-export const tabState = atomFamily({
- key: 'tabs',
- default: _param => 0,
- effects_UNSTABLE: param => [localStorageEffect(`TABS__${param}`)],
+export const tabState = atomFamily(param => {
+ const anAtom = atomWithStorage(`TABS__${param}`, 0);
+ anAtom.debugLabel = `TABS__${param}`;
+ return anAtom;
});
-export const loadingState = atomFamily<'idle' | 'loading', string>({
- key: 'ui.loading',
- default: () => 'idle',
+type LoadingState = 'idle' | 'loading';
+
+export const loadingState = atomFamily(_param => {
+ const anAtom = atom('idle');
+ anAtom.debugLabel = `loading-atom/${_param}`;
+ return anAtom;
});
-export const accountDrawerStep = atom({
- key: 'drawers.accounts.visibility',
- default: AccountStep.Switch,
-});
+export const accountDrawerStep = atom(AccountStep.Switch);
-export const showAccountsStore = atom({
- key: 'drawers.switch-account',
- default: false,
-});
+// TODO: refactor into atom family
+export const showAccountsStore = atom(false);
-export const showNetworksStore = atom({
- key: 'drawers.show-networks',
- default: false,
-});
+export const showNetworksStore = atom(false);
-export const showSettingsStore = atom({
- key: 'drawers.show-settings',
- default: false,
-});
+export const showSettingsStore = atom(false);
+
+accountDrawerStep.debugLabel = 'accountDrawerStep';
+showAccountsStore.debugLabel = 'showAccountsStore';
+showNetworksStore.debugLabel = 'showNetworksStore';
+showSettingsStore.debugLabel = 'showSettingsStore';
diff --git a/src/store/wallet.ts b/src/store/wallet.ts
index 520d02d4..f538ed63 100644
--- a/src/store/wallet.ts
+++ b/src/store/wallet.ts
@@ -1,5 +1,5 @@
-import { atom, selector } from 'recoil';
-import { localStorageEffect } from './common/utils';
+import { atom } from 'jotai';
+import { atomWithStorage } from 'jotai/utils';
import {
Wallet,
WalletConfig,
@@ -8,44 +8,24 @@ import {
} from '@stacks/wallet-sdk';
import { gaiaUrl } from '@common/constants';
-export const secretKeyState = atom({
- key: 'wallet.secret-key',
- default: undefined,
-});
+export const secretKeyState = atom(undefined);
+export const hasSetPasswordState = atom(false);
+export const walletState = atom(undefined);
+export const encryptedSecretKeyStore = atom(undefined);
+export const lastSeenStore = atomWithStorage('wallet.last-seen', new Date().getTime());
+export const walletConfigStore = atom(async get => {
+ const wallet = get(walletState);
+ if (!wallet) return null;
-export const hasSetPasswordState = atom({
- key: 'wallet.has-set-password',
- default: false,
+ const gaiaHubConfig = await createWalletGaiaConfig({ wallet, gaiaHubUrl: gaiaUrl });
+ return fetchWalletConfig({ wallet, gaiaHubConfig });
});
+export const hasRehydratedVaultStore = atom(false);
-export const walletState = atom({
- key: 'wallet',
- default: undefined,
- dangerouslyAllowMutability: true,
-});
-
-export const encryptedSecretKeyStore = atom({
- key: 'wallet.encrypted-key',
- default: undefined,
-});
-
-export const lastSeenStore = atom({
- key: 'wallet.last-seen',
- default: new Date().getTime(),
- effects_UNSTABLE: [localStorageEffect()],
-});
-
-export const walletConfigStore = selector({
- key: 'wallet.wallet-config',
- get: async ({ get }) => {
- const wallet = get(walletState);
- if (!wallet) return null;
- const gaiaHubConfig = await createWalletGaiaConfig({ wallet, gaiaHubUrl: gaiaUrl });
- return fetchWalletConfig({ wallet, gaiaHubConfig });
- },
-});
-
-export const hasRehydratedVaultStore = atom({
- key: 'wallet.has-rehydrated',
- default: false,
-});
+secretKeyState.debugLabel = 'secretKeyState';
+hasSetPasswordState.debugLabel = 'hasSetPasswordState';
+walletState.debugLabel = 'walletState';
+encryptedSecretKeyStore.debugLabel = 'encryptedSecretKeyStore';
+lastSeenStore.debugLabel = 'lastSeenStore';
+walletConfigStore.debugLabel = 'walletConfigStore';
+hasRehydratedVaultStore.debugLabel = 'hasRehydratedVaultStore';
diff --git a/webpack/webpack.config.base.js b/webpack/webpack.config.base.js
index 79b6a8e1..84dfa05d 100755
--- a/webpack/webpack.config.base.js
+++ b/webpack/webpack.config.base.js
@@ -130,6 +130,7 @@ const config = {
'@babel/preset-react',
],
plugins: [
+ '@emotion',
['@babel/plugin-proposal-class-properties', { loose: false }],
'@babel/plugin-transform-runtime',
'@babel/plugin-proposal-nullish-coalescing-operator',
diff --git a/yarn.lock b/yarn.lock
index 863073f3..6163977c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -991,6 +991,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.12.5":
+ version "7.14.6"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d"
+ integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.12.13", "@babel/template@^7.3.3":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327"
@@ -2849,10 +2856,8 @@
type-fest "^0.21.3"
typescript "^4.1.2"
-"@stacks/ui@7.8.0":
+"@stacks/ui@file:.yalc/@stacks/ui":
version "7.8.0"
- resolved "https://registry.yarnpkg.com/@stacks/ui/-/ui-7.8.0.tgz#03bc42b9c95f432bef6650781bbabd81e19272cb"
- integrity sha512-A8l1SKqMYBZ1t13V3yQcXySxv5RkPrjPovjlPoQswaNceS1Eh+UI3r5jMAwqHWDL9LE9G3wjSl4ect7whURkyg==
dependencies:
"@reach/alert" "^0.13.2"
"@reach/auto-id" "^0.13.2"
@@ -3438,6 +3443,11 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
+"@types/object-hash@^2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-2.1.0.tgz#284353e535481690a72bf748619d77577bd23317"
+ integrity sha512-RW3VRiuQIMo5PJ4Q1IwBtdLHL/t8ACpzUY40norN9ejE6CUBwKetmSxJnITJ0NlzN/ymF1nvPvlpvegtns7yOg==
+
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -4631,6 +4641,11 @@ better-path-resolve@1.0.0:
dependencies:
is-windows "^1.0.0"
+big-integer@^1.6.16:
+ version "1.6.48"
+ resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
+ integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
+
big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
@@ -4775,6 +4790,11 @@ boolbase@^1.0.0, boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+boring-avatars@^1.5.8:
+ version "1.5.8"
+ resolved "https://registry.yarnpkg.com/boring-avatars/-/boring-avatars-1.5.8.tgz#736e4e99c2390724f31dbc9175a18ac6b52f8c55"
+ integrity sha512-Xy9+6UcdwLZ0JDrzY2r0hYZXnQS9ZJRHcW8TdfnBfFcjTq7UfCC7N8g7tu7GD90RCp6XHbzTuJ1QKoK4djcWPA==
+
boxen@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
@@ -4840,6 +4860,20 @@ breakword@^1.0.5:
dependencies:
wcwidth "^1.0.1"
+broadcast-channel@^3.4.1:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937"
+ integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==
+ dependencies:
+ "@babel/runtime" "^7.7.2"
+ detect-node "^2.1.0"
+ js-sha3 "0.8.0"
+ microseconds "0.2.0"
+ nano-time "1.0.0"
+ oblivious-set "1.0.0"
+ rimraf "3.0.2"
+ unload "2.2.0"
+
brorand@^1.0.1, brorand@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@@ -6441,7 +6475,7 @@ detect-newline@^3.0.0, detect-newline@^3.1.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
-detect-node@^2.0.4:
+detect-node@^2.0.4, detect-node@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
@@ -9695,6 +9729,11 @@ jest@26.6.3:
import-local "^3.0.2"
jest-cli "^26.6.3"
+jotai@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.0.0.tgz#d5e7f7044d89e543a9c7429a9723e11528b2760e"
+ integrity sha512-6hZGy3hqIlBlLSKppTrxDc1Vb7mi3I8eEQOIu7Kj6ceX1PSzjxdsEVC9TjAqaio8gZJEz+2ufNUf4afvbs0RXg==
+
joycon@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.0.1.tgz#9074c9b08ccf37a6726ff74a18485f85efcaddaf"
@@ -9705,6 +9744,11 @@ jpeg-js@^0.4.2:
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b"
integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==
+js-sha3@0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
+ integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
+
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -10354,6 +10398,14 @@ marky@^1.2.0:
resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.2.tgz#4456765b4de307a13d263a69b0c79bf226e68323"
integrity sha512-k1dB2HNeaNyORco8ulVEhctyEGkKHb2YWAhDsxeFlW2nROIirsctBYzKwwS3Vza+sKTS1zO4Z+n9/+9WbGLIxQ==
+match-sorter@^6.0.2:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.0.tgz#454a1b31ed218cddbce6231a0ecb5fdc549fed01"
+ integrity sha512-efYOf/wUpNb8FgNY+cOD2EIJI1S5I7YPKsw0LBp7wqPh5pmMS6i/wr3ZWwfwrAw1NvqTA2KUReVRWDX84lUcOQ==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+ remove-accents "0.4.2"
+
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@@ -10502,6 +10554,11 @@ micromatch@^4.0.0, micromatch@^4.0.2:
braces "^3.0.1"
picomatch "^2.2.3"
+microseconds@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39"
+ integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==
+
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -10699,6 +10756,13 @@ nan@^2.12.1, nan@^2.13.2, nan@^2.14.0:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
+nano-time@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
+ integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=
+ dependencies:
+ big-integer "^1.6.16"
+
nanoclone@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
@@ -10907,6 +10971,11 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
+object-hash@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5"
+ integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==
+
object-inspect@^1.10.3, object-inspect@^1.9.0:
version "1.10.3"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
@@ -10949,6 +11018,11 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
+oblivious-set@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566"
+ integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==
+
obuf@^1.0.0, obuf@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
@@ -11005,6 +11079,11 @@ opn@^5.5.0:
dependencies:
is-wsl "^1.1.0"
+optics-ts@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/optics-ts/-/optics-ts-2.1.0.tgz#dcfd20297ff77ed169d326792733a71e933e801e"
+ integrity sha512-SJIwf16fwyo9+O+x303BifrWZ/H6Du/hS1zz4UERYt9BaRvxBID9XCYZlQ3c2aIrqeG64AxLcRy8VC/OYUkS9A==
+
optionator@^0.8.1:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
@@ -11971,7 +12050,7 @@ react-dev-utils@11.0.4:
strip-ansi "6.0.0"
text-table "0.2.0"
-react-dom@17.0.2:
+react-dom@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
@@ -12019,6 +12098,15 @@ react-is@^16.7.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+react-query@^3.17.0:
+ version "3.17.0"
+ resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.17.0.tgz#461c0a030044760cd874c7ea8aa9d55c2dceb15d"
+ integrity sha512-/qUNb6ESCz75Z/bR5p/ztp5ipRj8IQSiIpHK3AkCLTT4IqZsceAoD+9B+wbitA0LkxsR3snGrpgKUc9MMYQ/Ow==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ broadcast-channel "^3.4.1"
+ match-sorter "^6.0.2"
+
react-refresh@0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.10.0.tgz#2f536c9660c0b9b1d500684d9e52a65e7404f7e3"
@@ -12067,14 +12155,6 @@ react-transition-group@^4.4.1:
loose-envify "^1.4.0"
prop-types "^15.6.2"
-react@17.0.2:
- version "17.0.2"
- resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
- integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
- dependencies:
- loose-envify "^1.1.0"
- object-assign "^4.1.1"
-
react@^16.13.1:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
@@ -12084,6 +12164,14 @@ react@^16.13.1:
object-assign "^4.1.1"
prop-types "^15.6.2"
+react@^17.0.2:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
+ integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
read-pkg-up@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
@@ -12318,6 +12406,11 @@ relaxed-json@1.0.3:
chalk "^2.4.2"
commander "^2.6.0"
+remove-accents@0.4.2:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
+ integrity sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=
+
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -12486,6 +12579,13 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ dependencies:
+ glob "^7.1.3"
+
rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@@ -12493,13 +12593,6 @@ rimraf@^2.6.3:
dependencies:
glob "^7.1.3"
-rimraf@^3.0.0, rimraf@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
- integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
- dependencies:
- glob "^7.1.3"
-
rimraf@~2.4.0:
version "2.4.5"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da"
@@ -14198,6 +14291,14 @@ universalify@^2.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
+unload@2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7"
+ integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==
+ dependencies:
+ "@babel/runtime" "^7.6.2"
+ detect-node "^2.0.4"
+
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"