mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-04-28 12:45:30 +08:00
refactor: improve state, errors, etc
This commit is contained in:
16
public/html/devtool.html
Normal file
16
public/html/devtool.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="mode__devtool">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><%= htmlWebpackPlugin.options.title %> Devtools</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="/assets/connect-logo/Stacks128w.png"/>
|
||||
<link href="/assets/base.css" rel="stylesheet"/>
|
||||
<script defer src="devtools.js"></script>
|
||||
</body>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="actions-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ChainID } from '@stacks/transactions';
|
||||
|
||||
export const gaiaUrl = 'https://hub.blockstack.org';
|
||||
|
||||
export const transition = 'all .2s cubic-bezier(.215,.61,.355,1)';
|
||||
@@ -31,3 +33,36 @@ export const SIP_010 = {
|
||||
trait: 'ft-trait',
|
||||
},
|
||||
};
|
||||
|
||||
export interface Network {
|
||||
url: string;
|
||||
name: string;
|
||||
chainId: ChainID;
|
||||
}
|
||||
|
||||
export interface Networks {
|
||||
[key: string]: Network;
|
||||
}
|
||||
|
||||
export const defaultNetworks: Networks = {
|
||||
mainnet: {
|
||||
url: 'https://stacks-node-api.mainnet.stacks.co',
|
||||
name: 'Mainnet',
|
||||
chainId: ChainID.Mainnet,
|
||||
},
|
||||
testnet: {
|
||||
url: 'https://stacks-node-api.testnet.stacks.co',
|
||||
name: 'Testnet',
|
||||
chainId: ChainID.Testnet,
|
||||
},
|
||||
regtest: {
|
||||
url: 'https://stacks-node-api.regtest.stacks.co',
|
||||
name: 'Regtest',
|
||||
chainId: ChainID.Testnet,
|
||||
},
|
||||
localnet: {
|
||||
url: 'http://localhost:3999',
|
||||
name: 'Localnet',
|
||||
chainId: ChainID.Testnet,
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { CallbackInterface, useRecoilCallback } from 'recoil';
|
||||
import { postConditionsHasSetStore, transactionPayloadStore } from '@store/transaction';
|
||||
import { walletState } from '@store/wallet';
|
||||
import { currentAccountIndexStore } from '@store/accounts';
|
||||
import { getStxAddress } from '@stacks/wallet-sdk';
|
||||
|
||||
function accountSwitchCallback({ snapshot, set }: CallbackInterface) {
|
||||
return async () => {
|
||||
const payload = await snapshot.getPromise(transactionPayloadStore);
|
||||
if (!payload?.stxAddress || !payload.network) return;
|
||||
const wallet = await snapshot.getPromise(walletState);
|
||||
if (!wallet) return;
|
||||
const transactionVersion = payload.network.version;
|
||||
let foundIndex: number | undefined = undefined;
|
||||
wallet.accounts.forEach((account, index) => {
|
||||
const address = getStxAddress({ account, transactionVersion });
|
||||
if (address === payload.stxAddress) {
|
||||
foundIndex = index;
|
||||
}
|
||||
});
|
||||
if (foundIndex !== undefined) {
|
||||
console.debug('switching to index', foundIndex);
|
||||
set(currentAccountIndexStore, foundIndex);
|
||||
set(postConditionsHasSetStore, false);
|
||||
} else {
|
||||
console.warn(
|
||||
'No account matches the STX address provided in transaction request:',
|
||||
payload.stxAddress
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apps can specify a `stxAddress` in a transaction request.
|
||||
* If the user has a matching account, use that account by default.
|
||||
*/
|
||||
export function useAccountSwitchCallback() {
|
||||
return useRecoilCallback(accountSwitchCallback, []);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isUnauthorizedTransactionStore, requestTokenStore } from '@store/transaction';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { getRequestOrigin, StorageKey } from 'storage';
|
||||
import { walletState } from '@store/wallet';
|
||||
|
||||
import { verifyTxRequest } from '@common/transaction-utils';
|
||||
|
||||
export function useDecodeRequestCallback() {
|
||||
const location = useLocation();
|
||||
return useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async () => {
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
const requestToken = urlParams.get('request');
|
||||
if (!requestToken) {
|
||||
throw 'Invalid transaction request parameter';
|
||||
}
|
||||
|
||||
const wallet = await snapshot.getPromise(walletState);
|
||||
const origin = getRequestOrigin(StorageKey.transactionRequests, requestToken);
|
||||
if (!wallet || !origin) return;
|
||||
|
||||
try {
|
||||
// This function throws if tx is invalid
|
||||
await verifyTxRequest({
|
||||
requestToken,
|
||||
wallet,
|
||||
appDomain: origin,
|
||||
});
|
||||
set(requestTokenStore, requestToken);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
set(isUnauthorizedTransactionStore, true);
|
||||
}
|
||||
},
|
||||
[location.search]
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { transactionPayloadStore } from '@store/transaction';
|
||||
import {
|
||||
currentNetworkKeyStore,
|
||||
currentNetworkStore,
|
||||
Network,
|
||||
networksStore,
|
||||
} from '@store/networks';
|
||||
|
||||
export function useNetworkSwitchCallback() {
|
||||
return useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async () => {
|
||||
const payload = await snapshot.getPromise(transactionPayloadStore);
|
||||
if (!payload || !payload.network) return;
|
||||
|
||||
let foundNetwork = false;
|
||||
|
||||
const [currentNetwork, networks] = await Promise.all([
|
||||
snapshot.getPromise(currentNetworkStore),
|
||||
snapshot.getPromise(networksStore),
|
||||
]);
|
||||
|
||||
// try to find an exact url match
|
||||
if (payload.network.coreApiUrl !== currentNetwork.url) {
|
||||
const newNetworkKey = Object.keys(networks).find(key => {
|
||||
const network = networks[key] as Network;
|
||||
return network.url === payload.network?.coreApiUrl;
|
||||
});
|
||||
|
||||
if (newNetworkKey) {
|
||||
console.debug('Changing to new network to match node URL', newNetworkKey);
|
||||
set(currentNetworkKeyStore, newNetworkKey);
|
||||
foundNetwork = true;
|
||||
}
|
||||
}
|
||||
// try to find a network that matches chain id
|
||||
if (!foundNetwork && payload.network.chainId !== currentNetwork.chainId) {
|
||||
const newNetworkKey = Object.keys(networks).find(key => {
|
||||
const network = networks[key] as Network;
|
||||
return network.chainId === payload.network?.chainId;
|
||||
});
|
||||
|
||||
if (newNetworkKey) {
|
||||
console.debug('Changing to new network from chainID', newNetworkKey);
|
||||
set(currentNetworkKeyStore, newNetworkKey);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { CallbackInterface, useRecoilCallback } from 'recoil';
|
||||
import {
|
||||
postConditionsHasSetStore,
|
||||
postConditionsStore,
|
||||
transactionPayloadStore,
|
||||
} from '@store/transaction';
|
||||
import { getPostCondition, handlePostConditions } from '@common/post-condition-utils';
|
||||
import { currentAccountStxAddressStore } from '@store/accounts';
|
||||
|
||||
export function usePostConditionsCallback() {
|
||||
return useRecoilCallback(postConditionsCallback, []);
|
||||
}
|
||||
|
||||
function postConditionsCallback({ snapshot, set }: CallbackInterface) {
|
||||
return async (useCurrentAddress?: boolean) => {
|
||||
const [payload, existingPostConditions, hasSet, currentAddress] = await Promise.all([
|
||||
snapshot.getPromise(transactionPayloadStore),
|
||||
snapshot.getPromise(postConditionsStore),
|
||||
snapshot.getPromise(postConditionsHasSetStore),
|
||||
snapshot.getPromise(currentAccountStxAddressStore),
|
||||
]);
|
||||
if (!payload) return;
|
||||
|
||||
const { stxAddress, postConditions } = payload;
|
||||
|
||||
if (hasSet || !currentAddress) return;
|
||||
|
||||
if (!hasSet && postConditions && postConditions.length) {
|
||||
if (stxAddress && useCurrentAddress) {
|
||||
// we have yet to set the post conditions to the store
|
||||
// let's ensure the principal(s) are set correctly
|
||||
const newConditions = handlePostConditions(postConditions, stxAddress, currentAddress);
|
||||
set(postConditionsStore, newConditions);
|
||||
set(postConditionsHasSetStore, true);
|
||||
return;
|
||||
}
|
||||
// there is no `stxAddress` set, but it might be
|
||||
// the case that a post condition is of string type
|
||||
// so let's map over them and deserialize them
|
||||
const newConditions = postConditions.map(getPostCondition);
|
||||
set(postConditionsStore, newConditions);
|
||||
set(postConditionsHasSetStore, true);
|
||||
return;
|
||||
} else if (!hasSet && existingPostConditions.length) {
|
||||
if (stxAddress && useCurrentAddress) {
|
||||
// we have existing post conditions, lets ensure
|
||||
// the principal(s) are set correctly
|
||||
const newConditions = handlePostConditions(
|
||||
existingPostConditions,
|
||||
stxAddress,
|
||||
currentAddress
|
||||
);
|
||||
set(postConditionsStore, newConditions);
|
||||
set(postConditionsHasSetStore, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { accountBalancesStore, accountDataStore } from '@store/accounts';
|
||||
import { accountBalancesState, accountDataState } from '@store/accounts';
|
||||
import { useLoadable } from './use-loadable';
|
||||
|
||||
export const useFetchAccountData = () => {
|
||||
return useLoadable(accountDataStore);
|
||||
return useLoadable(accountDataState);
|
||||
};
|
||||
|
||||
export const useFetchBalances = () => {
|
||||
return useLoadable(accountBalancesStore);
|
||||
return useLoadable(accountBalancesState);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { currentAccountStore, currentAccountStxAddressStore } from '@store/accounts';
|
||||
import { currentAccountState, currentAccountStxAddressState } from '@store/accounts';
|
||||
|
||||
export function useCurrentAccount() {
|
||||
const accountInfo = useRecoilValue(currentAccountStore);
|
||||
const stxAddress = useRecoilValue(currentAccountStxAddressStore);
|
||||
const accountInfo = useRecoilValue(currentAccountState);
|
||||
const stxAddress = useRecoilValue(currentAccountStxAddressState);
|
||||
return {
|
||||
...accountInfo,
|
||||
stxAddress,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { currentNetworkStore } from '@store/networks';
|
||||
import { currentNetworkState } from '@store/networks';
|
||||
import { useMemo } from 'react';
|
||||
import { ChainID } from '@stacks/transactions';
|
||||
|
||||
type Modes = 'testnet' | 'mainnet';
|
||||
|
||||
export function useCurrentNetwork() {
|
||||
const network = useRecoilValue(currentNetworkStore);
|
||||
const network = useRecoilValue(currentNetworkState);
|
||||
const isTestnet = useMemo(() => network.chainId === ChainID.Testnet, [network.chainId]);
|
||||
const mode = (isTestnet ? 'testnet' : 'mainnet') as Modes;
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { currentPostConditionIndexStore, postConditionsStore } from '@store/transaction';
|
||||
import { selectedAssetIdState } from '@store/assets/asset-search';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValueLoadable, RecoilValue } from 'recoil';
|
||||
import { useCurrentNetwork } from '@common/hooks/use-current-network';
|
||||
|
||||
/**
|
||||
* Wrap a Recoil loadable and add a `value` property, which
|
||||
@@ -8,7 +7,6 @@ import { useCurrentNetwork } from '@common/hooks/use-current-network';
|
||||
*/
|
||||
export const useLoadable = <T>(recoilValue: RecoilValue<T>) => {
|
||||
const loadable = useRecoilValueLoadable(recoilValue);
|
||||
const network = useCurrentNetwork();
|
||||
const [value, setValue] = useState<T | undefined>(loadable.valueMaybe() || undefined);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -17,11 +15,6 @@ export const useLoadable = <T>(recoilValue: RecoilValue<T>) => {
|
||||
}
|
||||
}, [loadable, value]);
|
||||
|
||||
useEffect(() => {
|
||||
// we want to revalidate on network change
|
||||
setValue(undefined);
|
||||
}, [network.url]);
|
||||
|
||||
return {
|
||||
...loadable,
|
||||
key: recoilValue.key,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { loadingState } from '@store/ui';
|
||||
|
||||
export enum LOADING_KEYS {
|
||||
SUBMIT_TRANSACTION = 'loading/SUBMIT_TRANSACTION',
|
||||
}
|
||||
|
||||
export function useLoading(key: string) {
|
||||
const [state, setState] = useRecoilState(loadingState(key));
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRecoilCallback, waitForAll } from 'recoil';
|
||||
import { stacksNetworkStore } from '@store/networks';
|
||||
import { currentAccountStore } from '@store/accounts';
|
||||
import { currentAccountState } from '@store/accounts';
|
||||
import { correctNonceState } from '@store/accounts/nonce';
|
||||
import { makeSTXTokenTransfer, StacksTransaction } from '@stacks/transactions';
|
||||
import BN from 'bn.js';
|
||||
@@ -22,10 +22,11 @@ export function useMakeStxTransfer() {
|
||||
const { network, account, nonce } = await snapshot.getPromise(
|
||||
waitForAll({
|
||||
network: stacksNetworkStore,
|
||||
account: currentAccountStore,
|
||||
account: currentAccountState,
|
||||
nonce: correctNonceState,
|
||||
})
|
||||
);
|
||||
|
||||
if (!account) return;
|
||||
|
||||
return makeSTXTokenTransfer({
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { requestTokenStore } from '@store/transaction';
|
||||
import { getRequestOrigin, StorageKey } from '../../storage';
|
||||
import { requestTokenState } from '@store/transactions/requests';
|
||||
|
||||
export function useOrigin() {
|
||||
const requestToken = useRecoilValue(requestTokenStore);
|
||||
if (!requestToken) return null;
|
||||
try {
|
||||
return getRequestOrigin(StorageKey.transactionRequests, requestToken);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
const requestToken = useRecoilValue(requestTokenState);
|
||||
return requestToken ? getRequestOrigin(StorageKey.transactionRequests, requestToken) : null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { pendingTransactionStore } from '@store/transaction';
|
||||
import { pendingTransactionState } from '@store/transactions';
|
||||
|
||||
export function usePendingTransaction() {
|
||||
return useRecoilValue(pendingTransactionStore);
|
||||
return useRecoilValue(pendingTransactionState);
|
||||
}
|
||||
|
||||
6
src/common/hooks/use-postconditions.ts
Normal file
6
src/common/hooks/use-postconditions.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useLoadable } from '@common/hooks/use-loadable';
|
||||
import { postConditionsState } from '@store/transactions';
|
||||
|
||||
export function usePostconditions() {
|
||||
return useLoadable(postConditionsState);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { apiRevalidation } from '@store/common/api';
|
||||
import { apiRevalidation } from '@store/common/api-helpers';
|
||||
|
||||
export function useRevalidateApi() {
|
||||
return useRecoilCallback(({ snapshot, set }) => async () => {
|
||||
|
||||
@@ -2,17 +2,12 @@ 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';
|
||||
|
||||
import { authRequestState, currentScreenState } from '@store/onboarding';
|
||||
|
||||
import { getRequestOrigin, StorageKey } from '../../../storage';
|
||||
|
||||
import { getRequestOrigin, StorageKey } from 'storage';
|
||||
import { ScreenPaths } from '@store/common/types';
|
||||
|
||||
import { useOnboardingState } from '../use-onboarding-state';
|
||||
import { useOnboardingState } from './use-onboarding-state';
|
||||
import { authRequestState, currentScreenState } from '@store/onboarding';
|
||||
|
||||
export function useSaveAuthRequest() {
|
||||
const { wallet } = useWallet();
|
||||
@@ -1,78 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { usePrevious } from '@stacks/ui';
|
||||
|
||||
import { requestTokenStore, transactionPayloadStore } from '@store/transaction';
|
||||
import { currentAccountStxAddressStore } from '@store/accounts';
|
||||
import { hasRehydratedVaultStore } from '@store/wallet';
|
||||
|
||||
import { useAccountSwitchCallback } from '@common/hooks/callbacks/use-account-switch-callback';
|
||||
import { usePostConditionsCallback } from '@common/hooks/callbacks/use-post-conditions-callback';
|
||||
import { useNetworkSwitchCallback } from '@common/hooks/callbacks/use-network-switch-callback';
|
||||
import { useDecodeRequestCallback } from '@common/hooks/callbacks/use-decode-request-callback';
|
||||
|
||||
export const useSetupTx = () => {
|
||||
const hasRehydratedVault = useRecoilValue(hasRehydratedVaultStore);
|
||||
|
||||
const [hasMounted, setHasMounted] = useState(false);
|
||||
const currentAccountStxAddress = useRecoilValue(currentAccountStxAddressStore);
|
||||
const previousAccountStxAddress = usePrevious(currentAccountStxAddress);
|
||||
const requestToken = useRecoilValue(requestTokenStore);
|
||||
const payload = useRecoilValue(transactionPayloadStore);
|
||||
|
||||
const handleDecodeRequest = useDecodeRequestCallback();
|
||||
const handleNetworkSwitch = useNetworkSwitchCallback();
|
||||
const handleAccountSwitch = useAccountSwitchCallback();
|
||||
const handlePostConditions = usePostConditionsCallback();
|
||||
|
||||
const handleInit = useCallback(async () => {
|
||||
if (!hasRehydratedVault) return;
|
||||
if (!requestToken) {
|
||||
await handleDecodeRequest();
|
||||
}
|
||||
if (payload) {
|
||||
await Promise.all([handleNetworkSwitch(), handleAccountSwitch()]);
|
||||
if (requestToken && currentAccountStxAddress) {
|
||||
if (
|
||||
!hasMounted ||
|
||||
!previousAccountStxAddress ||
|
||||
previousAccountStxAddress !== currentAccountStxAddress
|
||||
) {
|
||||
if (!hasMounted) {
|
||||
setHasMounted(true);
|
||||
}
|
||||
await handlePostConditions(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
hasRehydratedVault,
|
||||
hasMounted,
|
||||
payload,
|
||||
requestToken,
|
||||
previousAccountStxAddress,
|
||||
setHasMounted,
|
||||
currentAccountStxAddress,
|
||||
handlePostConditions,
|
||||
handleDecodeRequest,
|
||||
handleNetworkSwitch,
|
||||
handleAccountSwitch,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
void handleInit();
|
||||
}, [
|
||||
payload,
|
||||
handleInit,
|
||||
hasMounted,
|
||||
previousAccountStxAddress,
|
||||
currentAccountStxAddress,
|
||||
handlePostConditions,
|
||||
requestToken,
|
||||
handleDecodeRequest,
|
||||
handleNetworkSwitch,
|
||||
handleAccountSwitch,
|
||||
]);
|
||||
|
||||
return !!(requestToken && payload && hasMounted);
|
||||
};
|
||||
33
src/common/hooks/use-switch-account.ts
Normal file
33
src/common/hooks/use-switch-account.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useWallet } from '@common/hooks/use-wallet';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { hasSwitchedAccountsState, transactionAccountIndexState } from '@store/accounts';
|
||||
import { networkTransactionVersionState } from '@store/networks';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const TIMEOUT = 350;
|
||||
|
||||
export const useSwitchAccount = (callback?: () => void) => {
|
||||
const { wallet, currentAccountIndex, doSwitchAccount } = useWallet();
|
||||
const txIndex = useRecoilValue(transactionAccountIndexState);
|
||||
const transactionVersion = useRecoilValue(networkTransactionVersionState);
|
||||
const [hasSwitched, setHasSwitched] = useRecoilState(hasSwitchedAccountsState);
|
||||
|
||||
const handleSwitchAccount = useCallback(
|
||||
async index => {
|
||||
if (typeof txIndex === 'number') setHasSwitched(true);
|
||||
await doSwitchAccount(index);
|
||||
if (callback) {
|
||||
window.setTimeout(() => {
|
||||
callback();
|
||||
}, TIMEOUT);
|
||||
}
|
||||
},
|
||||
[txIndex, setHasSwitched, doSwitchAccount, callback]
|
||||
);
|
||||
|
||||
const accounts = wallet?.accounts || [];
|
||||
const getIsActive = (index: number) =>
|
||||
typeof txIndex === 'number' && !hasSwitched ? index === txIndex : index === currentAccountIndex;
|
||||
|
||||
return { accounts, handleSwitchAccount, getIsActive, transactionVersion };
|
||||
};
|
||||
59
src/common/hooks/use-transaction-error.ts
Normal file
59
src/common/hooks/use-transaction-error.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
useTransactionContractInterface,
|
||||
useTransactionRequest,
|
||||
} from '@common/hooks/use-transaction';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isUnauthorizedTransactionState, transactionBroadcastErrorState } from '@store/transaction';
|
||||
import { useWallet } from '@common/hooks/use-wallet';
|
||||
import { useFetchBalances } from '@common/hooks/use-account-info';
|
||||
import { useMemo } from 'react';
|
||||
import { TransactionErrorReason } from '@pages/transaction/transaction-error';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { TransactionTypes } from '@stacks/connect';
|
||||
import { useTransactionFee } from '@common/hooks/use-transaction-fee';
|
||||
|
||||
export function useTransactionError() {
|
||||
const transactionRequest = useTransactionRequest();
|
||||
const fee = useTransactionFee();
|
||||
const contractInterface = useTransactionContractInterface();
|
||||
const broadcastError = useRecoilValue(transactionBroadcastErrorState);
|
||||
const isUnauthorizedTransaction = useRecoilValue(isUnauthorizedTransactionState);
|
||||
const { currentAccount } = useWallet();
|
||||
const balances = useFetchBalances();
|
||||
return useMemo<TransactionErrorReason | void>(() => {
|
||||
if (isUnauthorizedTransaction) return TransactionErrorReason.Unauthorized;
|
||||
|
||||
if (!transactionRequest || balances.errorMaybe() || !currentAccount) {
|
||||
return TransactionErrorReason.Generic;
|
||||
}
|
||||
if (
|
||||
transactionRequest.txType === TransactionTypes.ContractCall &&
|
||||
!contractInterface.isLoading &&
|
||||
!contractInterface.contents
|
||||
)
|
||||
return TransactionErrorReason.NoContract;
|
||||
if (broadcastError) return TransactionErrorReason.BroadcastError;
|
||||
|
||||
if (balances.value) {
|
||||
const stxBalance = new BigNumber(balances.value.stx.balance);
|
||||
if (transactionRequest.txType === TransactionTypes.STXTransfer) {
|
||||
const transferAmount = new BigNumber(transactionRequest.amount);
|
||||
if (transferAmount.gte(stxBalance))
|
||||
return TransactionErrorReason.StxTransferInsufficientFunds;
|
||||
}
|
||||
if (fee && !fee.isSponsored && fee.amount) {
|
||||
const feeAmount = new BigNumber(fee.amount);
|
||||
if (feeAmount.gte(stxBalance)) return TransactionErrorReason.FeeInsufficientFunds;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}, [
|
||||
fee,
|
||||
broadcastError,
|
||||
contractInterface,
|
||||
balances,
|
||||
currentAccount,
|
||||
transactionRequest,
|
||||
isUnauthorizedTransaction,
|
||||
]);
|
||||
}
|
||||
13
src/common/hooks/use-transaction-fee.ts
Normal file
13
src/common/hooks/use-transaction-fee.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useSignedTransaction } from '@common/hooks/use-transaction';
|
||||
import { AuthType } from '@stacks/transactions';
|
||||
|
||||
export function useTransactionFee() {
|
||||
const signedTransaction = useSignedTransaction();
|
||||
if (!signedTransaction.value) return;
|
||||
const isSponsored = signedTransaction.value.auth.authType === AuthType.Sponsored;
|
||||
const amount = signedTransaction.value.auth.spendingCondition?.fee?.toNumber();
|
||||
return {
|
||||
amount,
|
||||
isSponsored,
|
||||
};
|
||||
}
|
||||
16
src/common/hooks/use-transaction-page-title.ts
Normal file
16
src/common/hooks/use-transaction-page-title.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useTransactionRequest } from '@common/hooks/use-transaction';
|
||||
import { useMemo } from 'react';
|
||||
import { TransactionTypes } from '@stacks/connect';
|
||||
|
||||
export function useTransactionPageTitle() {
|
||||
const transactionRequest = useTransactionRequest();
|
||||
const txType = transactionRequest?.txType;
|
||||
return useMemo(() => {
|
||||
if (!transactionRequest) return;
|
||||
if (txType === TransactionTypes.STXTransfer) return 'Confirm transfer';
|
||||
if (txType === TransactionTypes.ContractDeploy) return 'Deploy contract';
|
||||
if (txType === TransactionTypes.ContractCall && 'functionName' in transactionRequest)
|
||||
return transactionRequest.functionName || 'Sign transaction';
|
||||
return 'Sign transaction';
|
||||
}, [transactionRequest, txType]);
|
||||
}
|
||||
35
src/common/hooks/use-transaction.ts
Normal file
35
src/common/hooks/use-transaction.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useLoadable } from '@common/hooks/use-loadable';
|
||||
import { requestTokenPayloadState } from '@store/transactions/requests';
|
||||
import { postConditionsState, signedTransactionState } from '@store/transactions';
|
||||
import {
|
||||
transactionContractInterfaceState,
|
||||
transactionContractSourceState,
|
||||
transactionFunctionsState,
|
||||
} from '@store/transactions/contract-call';
|
||||
|
||||
export function useTransactionRequest() {
|
||||
const payload = useLoadable(requestTokenPayloadState);
|
||||
return payload?.value;
|
||||
}
|
||||
|
||||
export function useTransactionContractInterface() {
|
||||
return useLoadable(transactionContractInterfaceState);
|
||||
}
|
||||
|
||||
export function useTransactionContractSource() {
|
||||
return useLoadable(transactionContractSourceState);
|
||||
}
|
||||
|
||||
export function useTransactionFunction() {
|
||||
const payload = useLoadable(transactionFunctionsState);
|
||||
return payload?.value;
|
||||
}
|
||||
|
||||
export function useTransactionPostConditions() {
|
||||
const payload = useLoadable(postConditionsState);
|
||||
return payload?.value;
|
||||
}
|
||||
|
||||
export function useSignedTransaction() {
|
||||
return useLoadable(signedTransactionState);
|
||||
}
|
||||
@@ -1,56 +1,63 @@
|
||||
import { useWallet } from './use-wallet';
|
||||
import {
|
||||
contractSourceStore,
|
||||
contractInterfaceStore,
|
||||
pendingTransactionStore,
|
||||
signedTransactionStore,
|
||||
pendingTransactionFunctionSelector,
|
||||
transactionBroadcastErrorStore,
|
||||
requestTokenStore,
|
||||
isUnauthorizedTransactionStore,
|
||||
} from '@store/transaction';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import { currentNetworkStore } from '@store/networks';
|
||||
import { transactionBroadcastErrorState, isUnauthorizedTransactionState } from '@store/transaction';
|
||||
import { useRecoilCallback, useRecoilValue, waitForAll } from 'recoil';
|
||||
import { currentNetworkState } from '@store/networks';
|
||||
import { finishTransaction } from '@common/transaction-utils';
|
||||
import { useLoadable } from '@common/hooks/use-loadable';
|
||||
import { finalizeTxSignature } from '@common/utils';
|
||||
import {
|
||||
useSignedTransaction,
|
||||
useTransactionContractInterface,
|
||||
useTransactionContractSource,
|
||||
useTransactionFunction,
|
||||
useTransactionRequest,
|
||||
} from '@common/hooks/use-transaction';
|
||||
import { signedTransactionState } from '@store/transactions';
|
||||
import { requestTokenPayloadState, requestTokenState } from '@store/transactions/requests';
|
||||
|
||||
export const useTxState = () => {
|
||||
export function useHandleSubmitPendingTransaction() {
|
||||
const { doSetLatestNonce } = useWallet();
|
||||
const broadcastError = useRecoilValue(transactionBroadcastErrorStore);
|
||||
const pendingTransaction = useRecoilValue(pendingTransactionStore);
|
||||
const contractSource = useLoadable(contractSourceStore);
|
||||
const contractInterface = useLoadable(contractInterfaceStore);
|
||||
const pendingTransactionFunction = useLoadable(pendingTransactionFunctionSelector);
|
||||
const signedTransaction = useLoadable(signedTransactionStore);
|
||||
const isUnauthorizedTransaction = useRecoilValue(isUnauthorizedTransactionStore);
|
||||
|
||||
const doSubmitPendingTransaction = useRecoilCallback(
|
||||
return useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async () => {
|
||||
const pendingTransaction = await snapshot.getPromise(pendingTransactionStore);
|
||||
const requestPayload = await snapshot.getPromise(requestTokenStore);
|
||||
if (!pendingTransaction || !requestPayload) {
|
||||
set(transactionBroadcastErrorStore, 'No pending transaction found.');
|
||||
const { tx, pendingTransaction, requestToken, network } = await snapshot.getPromise(
|
||||
waitForAll({
|
||||
tx: signedTransactionState,
|
||||
pendingTransaction: requestTokenPayloadState,
|
||||
requestToken: requestTokenState,
|
||||
network: currentNetworkState,
|
||||
})
|
||||
);
|
||||
|
||||
if (!pendingTransaction || !requestToken || !tx) {
|
||||
set(transactionBroadcastErrorState, 'No pending transaction found.');
|
||||
return;
|
||||
}
|
||||
const tx = await snapshot.getPromise(signedTransactionStore);
|
||||
const currentNetwork = await snapshot.getPromise(currentNetworkStore);
|
||||
if (!tx) return;
|
||||
try {
|
||||
const result = await finishTransaction({
|
||||
tx,
|
||||
pendingTransaction,
|
||||
nodeUrl: currentNetwork.url,
|
||||
nodeUrl: network.url,
|
||||
});
|
||||
await doSetLatestNonce(tx);
|
||||
finalizeTxSignature(requestPayload, result);
|
||||
finalizeTxSignature(requestToken, result);
|
||||
} catch (error) {
|
||||
set(transactionBroadcastErrorStore, error.message);
|
||||
set(transactionBroadcastErrorState, error.message);
|
||||
}
|
||||
},
|
||||
[doSetLatestNonce]
|
||||
);
|
||||
}
|
||||
|
||||
export const useTxState = () => {
|
||||
const pendingTransaction = useTransactionRequest();
|
||||
const contractInterface = useTransactionContractInterface();
|
||||
const pendingTransactionFunction = useTransactionFunction();
|
||||
const signedTransaction = useSignedTransaction();
|
||||
const contractSource = useTransactionContractSource();
|
||||
const handleSubmitPendingTransaction = useHandleSubmitPendingTransaction();
|
||||
|
||||
const broadcastError = useRecoilValue(transactionBroadcastErrorState);
|
||||
const isUnauthorizedTransaction = useRecoilValue(isUnauthorizedTransactionState);
|
||||
|
||||
return {
|
||||
pendingTransaction,
|
||||
@@ -58,7 +65,7 @@ export const useTxState = () => {
|
||||
contractSource,
|
||||
contractInterface,
|
||||
pendingTransactionFunction,
|
||||
doSubmitPendingTransaction,
|
||||
handleSubmitPendingTransaction,
|
||||
broadcastError,
|
||||
isUnauthorizedTransaction,
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { InMemoryVault } from '@background/vault';
|
||||
import { InternalMethods } from '@content-scripts/message-types';
|
||||
import { currentAccountIndexStore } from '@store/accounts';
|
||||
import { textToBytes } from '@store/common/utils';
|
||||
|
||||
type Set = <T>(store: RecoilState<T>, value: T) => void;
|
||||
|
||||
@@ -27,7 +28,7 @@ const innerMessageWrapper = async (message: VaultActions, set: Set) => {
|
||||
set(hasRehydratedVaultStore, true);
|
||||
set(hasSetPasswordState, vault.hasSetPassword);
|
||||
set(walletState, vault.wallet);
|
||||
set(secretKeyState, vault.secretKey);
|
||||
set(secretKeyState, vault.secretKey ? textToBytes(vault.secretKey) : undefined);
|
||||
set(currentAccountIndexStore, vault.currentAccountIndex);
|
||||
set(encryptedSecretKeyStore, vault.encryptedSecretKey);
|
||||
resolve(vault);
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
import { useRecoilValue, useRecoilState, useRecoilCallback } from 'recoil';
|
||||
import { gaiaUrl } from '@common/constants';
|
||||
import {
|
||||
currentNetworkKeyStore,
|
||||
currentNetworkStore,
|
||||
networksStore,
|
||||
currentTransactionVersion,
|
||||
latestBlockHeightStore,
|
||||
currentNetworkKeyState,
|
||||
currentNetworkState,
|
||||
networksState,
|
||||
networkTransactionVersionState,
|
||||
latestBlockHeightState,
|
||||
} from '@store/networks';
|
||||
import {
|
||||
walletState,
|
||||
@@ -28,14 +28,15 @@ import { useVaultMessenger } from '@common/hooks/use-vault-messenger';
|
||||
|
||||
import { useOnboardingState } from './use-onboarding-state';
|
||||
import { finalizeAuthResponse } from '@common/utils';
|
||||
import { apiRevalidation } from '@store/common/api';
|
||||
import { apiRevalidation } from '@store/common/api-helpers';
|
||||
import { useLoadable } from '@common/hooks/use-loadable';
|
||||
import {
|
||||
currentAccountIndexStore,
|
||||
currentAccountStore,
|
||||
currentAccountStxAddressStore,
|
||||
currentAccountState,
|
||||
currentAccountStxAddressState,
|
||||
} from '@store/accounts';
|
||||
import { latestNoncesState } from '@store/accounts/nonce';
|
||||
import { bytesToText } from '@store/common/utils';
|
||||
|
||||
export const useWallet = () => {
|
||||
const hasRehydratedVault = useRecoilValue(hasRehydratedVaultStore);
|
||||
@@ -44,12 +45,12 @@ export const useWallet = () => {
|
||||
const encryptedSecretKey = useRecoilValue(encryptedSecretKeyStore);
|
||||
const currentAccountIndex = useRecoilValue(currentAccountIndexStore);
|
||||
const hasSetPassword = useRecoilValue(hasSetPasswordState);
|
||||
const currentAccount = useRecoilValue(currentAccountStore);
|
||||
const currentAccountStxAddress = useRecoilValue(currentAccountStxAddressStore);
|
||||
const transactionVersion = useRecoilValue(currentTransactionVersion);
|
||||
const networks = useRecoilValue(networksStore);
|
||||
const currentNetwork = useRecoilValue(currentNetworkStore);
|
||||
const currentNetworkKey = useRecoilValue(currentNetworkKeyStore);
|
||||
const currentAccount = useRecoilValue(currentAccountState);
|
||||
const currentAccountStxAddress = useRecoilValue(currentAccountStxAddressState);
|
||||
const transactionVersion = useRecoilValue(networkTransactionVersionState);
|
||||
const networks = useRecoilValue(networksState);
|
||||
const currentNetwork = useRecoilValue(currentNetworkState);
|
||||
const currentNetworkKey = useRecoilValue(currentNetworkKeyState);
|
||||
const walletConfig = useLoadable(walletConfigStore);
|
||||
const vaultMessenger = useVaultMessenger();
|
||||
|
||||
@@ -67,9 +68,9 @@ export const useWallet = () => {
|
||||
const newNonce = tx.auth.spendingCondition?.nonce.toNumber();
|
||||
if (newNonce !== undefined) {
|
||||
set(apiRevalidation, current => (current as number) + 1);
|
||||
const blockHeight = await snapshot.getPromise(latestBlockHeightStore);
|
||||
const network = await snapshot.getPromise(currentNetworkStore);
|
||||
const address = await snapshot.getPromise(currentAccountStxAddressStore);
|
||||
const blockHeight = await snapshot.getPromise(latestBlockHeightState);
|
||||
const network = await snapshot.getPromise(currentNetworkState);
|
||||
const address = await snapshot.getPromise(currentAccountStxAddressState);
|
||||
set(latestNoncesState([network.url, address || '']), () => ({
|
||||
blockHeight,
|
||||
nonce: newNonce,
|
||||
@@ -132,7 +133,7 @@ export const useWallet = () => {
|
||||
return {
|
||||
hasRehydratedVault,
|
||||
wallet,
|
||||
secretKey,
|
||||
secretKey: secretKey ? bytesToText(secretKey) : undefined,
|
||||
isSignedIn,
|
||||
currentAccount,
|
||||
currentAccountIndex,
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { c32addressDecode } from 'c32check';
|
||||
import { getAssetStringParts } from '@stacks/ui-utils';
|
||||
import { Network } from '@store/networks';
|
||||
import { Network } from './constants';
|
||||
import { STX_DECIMALS } from './constants';
|
||||
import { abbreviateNumber } from '@common/utils';
|
||||
|
||||
@@ -47,7 +47,7 @@ export const encodeContractCallArgument = ({ type, value }: ContractCallArgument
|
||||
|
||||
export const stacksValue = ({
|
||||
value,
|
||||
fixedDecimals = false,
|
||||
fixedDecimals = true,
|
||||
withTicker = true,
|
||||
abbreviate = false,
|
||||
}: {
|
||||
|
||||
@@ -126,12 +126,12 @@ export const generateTransaction = async ({
|
||||
txData: TransactionPayload;
|
||||
}) => {
|
||||
let tx: StacksTransaction | null = null;
|
||||
console.log('generate', txData, nonce);
|
||||
if (!txData.network?.getTransferFeeEstimateApiUrl) {
|
||||
const network =
|
||||
txData.network =
|
||||
txData.network?.version === TransactionVersion.Mainnet
|
||||
? new StacksMainnet()
|
||||
: new StacksTestnet();
|
||||
txData.network = network;
|
||||
}
|
||||
switch (txData.txType) {
|
||||
case TransactionTypes.ContractCall:
|
||||
@@ -152,24 +152,25 @@ export const generateTransaction = async ({
|
||||
return tx;
|
||||
};
|
||||
|
||||
interface FinishTransactionOptions {
|
||||
tx: StacksTransaction;
|
||||
pendingTransaction: TransactionPayloadWithAttachment;
|
||||
nodeUrl: string;
|
||||
}
|
||||
|
||||
export const finishTransaction = async ({
|
||||
tx,
|
||||
pendingTransaction,
|
||||
nodeUrl,
|
||||
}: {
|
||||
tx: StacksTransaction;
|
||||
pendingTransaction: TransactionPayloadWithAttachment;
|
||||
nodeUrl: string;
|
||||
}): Promise<TxResult> => {
|
||||
}: FinishTransactionOptions): Promise<TxResult> => {
|
||||
const serialized = tx.serialize();
|
||||
const txRaw = `0x${serialized.toString('hex')}`;
|
||||
|
||||
// if sponsored, return raw tx
|
||||
if (tx.auth.authType === AuthType.Sponsored) {
|
||||
if (tx.auth.authType === AuthType.Sponsored)
|
||||
return {
|
||||
txRaw,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await broadcastRawTransaction(
|
||||
serialized,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { wordlists } from 'bip39';
|
||||
import { isValidUrl } from './validate-url';
|
||||
import { getTab, deleteTabForRequest, StorageKey } from '../storage';
|
||||
import { BufferReader, deserializePostCondition, PostCondition } from '@stacks/transactions';
|
||||
import { KEBAB_REGEX } from '@common/constants';
|
||||
import {
|
||||
AuthenticationResponseMessage,
|
||||
ExternalMethods,
|
||||
@@ -13,6 +12,9 @@ import {
|
||||
TxResult,
|
||||
} from '@content-scripts/message-types';
|
||||
|
||||
import { KEBAB_REGEX, Network } from '@common/constants';
|
||||
import { StacksNetwork } from '@stacks/network';
|
||||
|
||||
function kebabCase(str: string) {
|
||||
return str.replace(KEBAB_REGEX, match => '-' + match.toLowerCase());
|
||||
}
|
||||
@@ -265,3 +267,17 @@ export function hexToHumanReadable(hex: string) {
|
||||
if (isUtf8(buff)) return buff.toString('utf8');
|
||||
return `0x${hex}`;
|
||||
}
|
||||
|
||||
export function findMatchingNetworkKey(
|
||||
txNetwork: StacksNetwork,
|
||||
networks: Record<string, Network>
|
||||
) {
|
||||
if (!networks) return;
|
||||
const newNetworkKey = Object.keys(networks).find((key: string) => {
|
||||
const network = networks[key] as Network;
|
||||
return network.url === txNetwork?.coreApiUrl || network.chainId === txNetwork?.chainId;
|
||||
});
|
||||
|
||||
if (newNetworkKey) return newNetworkKey;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -5,5 +5,9 @@ const hiroHeaders: HeadersInit = {
|
||||
|
||||
export function fetcher(input: RequestInfo, init: RequestInit = {}) {
|
||||
const initHeaders = init.headers || {};
|
||||
return fetch(input, { ...init, headers: { ...initHeaders, ...hiroHeaders } });
|
||||
return fetch(input, {
|
||||
credentials: 'omit',
|
||||
...init,
|
||||
headers: { ...initHeaders, ...hiroHeaders },
|
||||
});
|
||||
}
|
||||
|
||||
24
src/components/account-item.tsx
Normal file
24
src/components/account-item.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Stack } from '@stacks/ui';
|
||||
import { AccountAvatar } from '@components/account-avatar';
|
||||
import { Caption, Title } from '@components/typography';
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
import { getAccountDisplayName } from '@stacks/wallet-sdk';
|
||||
import { useAccountNames } from '@common/hooks/use-account-names';
|
||||
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);
|
||||
return (
|
||||
<Stack isInline alignItems="center" spacing="base" {...rest}>
|
||||
<AccountAvatar name={name} account={account} />
|
||||
<Stack spacing="base-tight">
|
||||
<Title fontSize={2} lineHeight="1rem" fontWeight="400">
|
||||
{name}
|
||||
</Title>
|
||||
<Caption>{truncateMiddle(account.address, 9)}</Caption>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
@@ -1,19 +1,20 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Flex, FlexProps, Spinner, color, Stack } from '@stacks/ui';
|
||||
import { Caption, Text, Title } from '@components/typography';
|
||||
import { color, Flex, FlexProps, Spinner, Stack } from '@stacks/ui';
|
||||
import { Caption, Title, Text } from '@components/typography';
|
||||
|
||||
import { ScreenPaths } from '@store/common/types';
|
||||
|
||||
import { PlusInCircle } from '@components/icons/plus-in-circle';
|
||||
import { ListItem } from './list-item';
|
||||
import { useDoChangeScreen } from '@common/hooks/use-do-change-screen';
|
||||
import { useWallet } from '@common/hooks/use-wallet';
|
||||
import { getStxAddress, Account, getAccountDisplayName } from '@stacks/wallet-sdk';
|
||||
import { useOnboardingState } from '@common/hooks/use-onboarding-state';
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
import { useAccountDisplayName, useAccountNames } from '@common/hooks/use-account-names';
|
||||
import { accountsWithAddressState } from '@store/accounts';
|
||||
import { useLoadable } from '@common/hooks/use-loadable';
|
||||
import { Account, getAccountDisplayName } from '@stacks/wallet-sdk';
|
||||
import { AccountAvatar } from '@components/account-avatar';
|
||||
import { SpaceBetween } from '@components/space-between';
|
||||
import { useAccountDisplayName, useAccountNames } from '@common/hooks/use-account-names';
|
||||
import { ScreenPaths } from '@store/common/types';
|
||||
import { PlusInCircle } from '@components/icons/plus-in-circle';
|
||||
|
||||
const loadingProps = { color: '#A1A7B3' };
|
||||
const getLoadingProps = (loading: boolean) => (loading ? loadingProps : {});
|
||||
@@ -28,7 +29,7 @@ interface AccountItemProps extends FlexProps {
|
||||
account: Account;
|
||||
}
|
||||
|
||||
const AccountItem: React.FC<AccountItemProps> = ({
|
||||
export const AccountItem: React.FC<AccountItemProps> = ({
|
||||
address,
|
||||
selectedAddress,
|
||||
account,
|
||||
@@ -36,7 +37,6 @@ const AccountItem: React.FC<AccountItemProps> = ({
|
||||
}) => {
|
||||
const { decodedAuthRequest } = useOnboardingState();
|
||||
const name = useAccountDisplayName(account);
|
||||
|
||||
const loading = address === selectedAddress;
|
||||
const showLoadingProps = !!selectedAddress || !decodedAuthRequest;
|
||||
return (
|
||||
@@ -73,7 +73,8 @@ export const Accounts: React.FC<AccountsProps> = ({
|
||||
next,
|
||||
...rest
|
||||
}) => {
|
||||
const { wallet, transactionVersion } = useWallet();
|
||||
const { wallet } = useWallet();
|
||||
const { value: accounts } = useLoadable(accountsWithAddressState);
|
||||
const [selectedAddress, setSelectedAddress] = useState<null | string>(null);
|
||||
const { decodedAuthRequest } = useOnboardingState();
|
||||
const doChangeScreen = useDoChangeScreen();
|
||||
@@ -83,15 +84,7 @@ export const Accounts: React.FC<AccountsProps> = ({
|
||||
}
|
||||
}, [accountIndex, setSelectedAddress, selectedAddress]);
|
||||
const names = useAccountNames();
|
||||
if (!wallet) return null;
|
||||
|
||||
const accounts = wallet.accounts.map(account => ({
|
||||
...account,
|
||||
stxAddress: getStxAddress({ account, transactionVersion }),
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
number: account.index + 1,
|
||||
}));
|
||||
|
||||
if (!wallet || !accounts) return null;
|
||||
const disableSelect = !decodedAuthRequest || !!selectedAddress;
|
||||
|
||||
return (
|
||||
@@ -101,21 +94,21 @@ export const Accounts: React.FC<AccountsProps> = ({
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={account.stxAddress}
|
||||
key={account.address}
|
||||
isFirst={index === 0}
|
||||
cursor={disableSelect ? 'not-allowed' : 'pointer'}
|
||||
iconComponent={() => <AccountAvatar account={account} name={name} />}
|
||||
iconComponent={() => <AccountAvatar account={account} name={name} mr={3} />}
|
||||
hasAction={!!next && selectedAddress === null}
|
||||
data-test={`account-${(account?.username || account?.stxAddress).split('.')[0]}`}
|
||||
data-test={`account-${(account.username || account.address).split('.')[0]}`}
|
||||
onClick={() => {
|
||||
if (!next) return;
|
||||
if (selectedAddress) return;
|
||||
setSelectedAddress(account?.stxAddress);
|
||||
setSelectedAddress(account.address);
|
||||
next(index);
|
||||
}}
|
||||
>
|
||||
<AccountItem
|
||||
address={account.stxAddress}
|
||||
address={account.address}
|
||||
selectedAddress={selectedAddress}
|
||||
data-test={`account-index-${index}`}
|
||||
account={account}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { VaultLoader } from '@components/vault-loader';
|
||||
import { AccountsDrawer } from './drawer/accounts';
|
||||
import { NetworksDrawer } from './drawer/networks-drawer';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { DebugObserver } from '@components/debug-observer';
|
||||
|
||||
export const App: React.FC = () => {
|
||||
useEffect(() => {
|
||||
@@ -19,6 +20,7 @@ export const App: React.FC = () => {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<RecoilRoot>
|
||||
<DebugObserver />
|
||||
<ColorModeProvider defaultMode="light">
|
||||
<>
|
||||
<GlobalStyles />
|
||||
|
||||
17
src/components/debug-observer.tsx
Normal file
17
src/components/debug-observer.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,42 +1,20 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { Box, Fade, Button, Stack, color } from '@stacks/ui';
|
||||
import { Title, Caption } from '@components/typography';
|
||||
import { useWallet } from '@common/hooks/use-wallet';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { accountDrawerStep, AccountStep } from '@store/ui';
|
||||
import { getAccountDisplayName, getStxAddress } from '@stacks/wallet-sdk';
|
||||
import { currentTransactionVersion } from '@store/networks';
|
||||
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/use-account-names';
|
||||
import { useSwitchAccount } from '@common/hooks/use-switch-account';
|
||||
|
||||
interface SwitchAccountProps {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
const TIMEOUT = 350;
|
||||
|
||||
const useSwitchAccount = (handleClose: () => void) => {
|
||||
const { wallet, currentAccountIndex, doSwitchAccount } = useWallet();
|
||||
const transactionVersion = useRecoilValue(currentTransactionVersion);
|
||||
|
||||
const handleSwitchAccount = useCallback(
|
||||
async index => {
|
||||
await doSwitchAccount(index);
|
||||
window.setTimeout(() => {
|
||||
handleClose();
|
||||
}, TIMEOUT);
|
||||
},
|
||||
[doSwitchAccount, handleClose]
|
||||
);
|
||||
|
||||
const accounts = wallet?.accounts || [];
|
||||
const getIsActive = (index: number) => index === currentAccountIndex;
|
||||
return { accounts, handleSwitchAccount, getIsActive, transactionVersion };
|
||||
};
|
||||
|
||||
// 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 }) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useWallet } from '@common/hooks/use-wallet';
|
||||
import { CheckmarkIcon } from '@components/icons/checkmark-icon';
|
||||
import { useDoChangeScreen } from '@common/hooks/use-do-change-screen';
|
||||
import { ScreenPaths } from '@store/common/types';
|
||||
import { currentNetworkKeyStore } from '@store/networks';
|
||||
import { currentNetworkKeyState } from '@store/networks';
|
||||
import { showNetworksStore } from '@store/ui';
|
||||
import { useDrawers } from '@common/hooks/use-drawers';
|
||||
import { Caption, Title } from '@components/typography';
|
||||
@@ -14,7 +14,7 @@ import { Caption, Title } from '@components/typography';
|
||||
const NetworkListItem: React.FC<{ item: string } & BoxProps> = memo(({ item, ...props }) => {
|
||||
const { setShowNetworks } = useDrawers();
|
||||
const { networks, currentNetworkKey } = useWallet();
|
||||
const setCurrentNetworkKey = useSetRecoilState(currentNetworkKeyStore);
|
||||
const setCurrentNetworkKey = useSetRecoilState(currentNetworkKeyState);
|
||||
|
||||
const network = networks[item];
|
||||
const delayToShowCheckmarkMotion = 350;
|
||||
|
||||
@@ -2,17 +2,12 @@ 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 {
|
||||
contractSourceStore,
|
||||
contractInterfaceStore,
|
||||
pendingTransactionFunctionSelector,
|
||||
signedTransactionStore,
|
||||
pendingTransactionStore,
|
||||
} from '@store/transaction';
|
||||
import { accountDataStore } from '@store/accounts';
|
||||
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';
|
||||
import { signedTransactionState } from '@store/transactions';
|
||||
|
||||
const openGithubIssue = (loadable: ReturnType<typeof useLoadable>) => {
|
||||
const issueParams = new URLSearchParams();
|
||||
@@ -58,16 +53,11 @@ type Loadables = ReturnType<typeof useLoadable>[];
|
||||
*
|
||||
*/
|
||||
export const ErrorBoundary: React.FC = ({ children }) => {
|
||||
const pendingTransaction = useRecoilValue(pendingTransactionStore);
|
||||
const pendingTransaction = useTransactionRequest();
|
||||
const wallet = useRecoilValue(walletState);
|
||||
let loadables: Loadables = [];
|
||||
const walletLoadables: Loadables = [useLoadable(accountDataStore)];
|
||||
const txLoadables: Loadables = [
|
||||
useLoadable(contractSourceStore),
|
||||
useLoadable(contractInterfaceStore),
|
||||
useLoadable(pendingTransactionFunctionSelector),
|
||||
useLoadable(signedTransactionStore),
|
||||
];
|
||||
const walletLoadables: Loadables = [useLoadable(accountDataState)];
|
||||
const txLoadables: Loadables = [useLoadable(signedTransactionState)];
|
||||
|
||||
if (wallet) {
|
||||
loadables = loadables.concat(walletLoadables);
|
||||
@@ -81,6 +71,7 @@ export const ErrorBoundary: React.FC = ({ children }) => {
|
||||
|
||||
if (errorLoadables.length > 0) {
|
||||
const [loadable] = errorLoadables;
|
||||
console.log(errorLoadables);
|
||||
const error = errorLoadables[0].errorOrThrow();
|
||||
return (
|
||||
<PopupContainer header={<Header title="Uh oh! Something went wrong." />}>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Box, color, Flex, FlexProps, Text } from '@stacks/ui';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { currentNetworkStore } from '@store/networks';
|
||||
import { currentNetworkState } from '@store/networks';
|
||||
import { ChainID } from '@stacks/transactions';
|
||||
import { IconFlask } from '@tabler/icons';
|
||||
import { useDrawers } from '@common/hooks/use-drawers';
|
||||
|
||||
export const NetworkModeBadge: React.FC<FlexProps> = memo(props => {
|
||||
const { chainId } = useRecoilValue(currentNetworkStore);
|
||||
const { chainId } = useRecoilValue(currentNetworkState);
|
||||
const isTestnet = useMemo(() => chainId === ChainID.Testnet, [chainId]);
|
||||
const { setShowNetworks } = useDrawers();
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { ScreenPaths } from '@store/common/types';
|
||||
import { useDoChangeScreen } from '@common/hooks/use-do-change-screen';
|
||||
import { useWallet } from '@common/hooks/use-wallet';
|
||||
import { useOnboardingState } from '@common/hooks/use-onboarding-state';
|
||||
import { useSaveAuthRequest } from '@common/hooks/callbacks/use-save-auth-request-callback';
|
||||
import { useSaveAuthRequest } from '@common/hooks/use-save-auth-request-callback';
|
||||
import { Route as RouterRoute, Routes as RoutesDom, useLocation } from 'react-router-dom';
|
||||
import { Navigate } from '@components/navigate';
|
||||
import { AccountGate } from '@components/account-gate';
|
||||
|
||||
85
src/components/transactions/actions.tsx
Normal file
85
src/components/transactions/actions.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { Box, Button, color, Stack, StackProps } from '@stacks/ui';
|
||||
import { LOADING_KEYS, useLoading } from '@common/hooks/use-loading';
|
||||
import { useDrawers } from '@common/hooks/use-drawers';
|
||||
import { SpaceBetween } from '@components/space-between';
|
||||
import { Caption } from '@components/typography';
|
||||
import { NetworkRowItem } from '@components/network-row-item';
|
||||
import { useTransactionError } from '@common/hooks/use-transaction-error';
|
||||
import { FeeComponent } from '@components/transactions/fee';
|
||||
import { useSignedTransaction } from '@common/hooks/use-transaction';
|
||||
import { useHandleSubmitPendingTransaction } from '@common/hooks/use-tx-state';
|
||||
import { TransactionErrorReason } from '@pages/transaction/transaction-error';
|
||||
import { FiAlertTriangle } from 'react-icons/fi';
|
||||
|
||||
const MinimalErrorMessage = memo((props: StackProps) => {
|
||||
const error = useTransactionError();
|
||||
if (!error) return null;
|
||||
|
||||
const getTitle = () => {
|
||||
if (error) {
|
||||
switch (error) {
|
||||
case TransactionErrorReason.Unauthorized:
|
||||
return 'Unauthorized request';
|
||||
case TransactionErrorReason.NoContract:
|
||||
return 'Contract not found';
|
||||
case TransactionErrorReason.StxTransferInsufficientFunds:
|
||||
case TransactionErrorReason.FeeInsufficientFunds:
|
||||
return 'Insufficient balance';
|
||||
case TransactionErrorReason.BroadcastError:
|
||||
return 'Broadcast error';
|
||||
case TransactionErrorReason.Generic:
|
||||
return 'Something went wrong';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return (
|
||||
<Stack alignItems="center" bg="#FCEEED" p="base" borderRadius="12px" isInline {...props}>
|
||||
<Box color={color('feedback-error')} strokeWidth={2} as={FiAlertTriangle} />
|
||||
<Caption color={color('feedback-error')}>{getTitle()}</Caption>
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
||||
export const TransactionsActions = memo((props: StackProps) => {
|
||||
const handleSubmitPendingTransaction = useHandleSubmitPendingTransaction();
|
||||
const signedTransaction = useSignedTransaction();
|
||||
const { setShowNetworks } = useDrawers();
|
||||
const error = useTransactionError();
|
||||
const { setIsLoading, setIsIdle, isLoading } = useLoading(LOADING_KEYS.SUBMIT_TRANSACTION);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
setIsLoading();
|
||||
await handleSubmitPendingTransaction();
|
||||
setIsIdle();
|
||||
}, [setIsLoading, setIsIdle, handleSubmitPendingTransaction]);
|
||||
|
||||
return (
|
||||
<Stack mt="auto" pt="loose" spacing="loose" bg={color('bg')} {...props}>
|
||||
<Stack spacing="base-loose">
|
||||
<SpaceBetween>
|
||||
<Caption>Fees</Caption>
|
||||
<Caption>
|
||||
<FeeComponent />
|
||||
</Caption>
|
||||
</SpaceBetween>
|
||||
<SpaceBetween>
|
||||
<Caption>Network</Caption>
|
||||
<NetworkRowItem onClick={() => setShowNetworks(true)} />
|
||||
</SpaceBetween>
|
||||
</Stack>
|
||||
<MinimalErrorMessage />
|
||||
<Button
|
||||
borderRadius="12px"
|
||||
py="base"
|
||||
width="100%"
|
||||
onClick={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
isDisabled={!!error || !signedTransaction.value}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useTxState } from '@common/hooks/use-tx-state';
|
||||
import React from 'react';
|
||||
import { RowItem } from '@components/transactions/row-item';
|
||||
import { hexToHumanReadable } from '@common/utils';
|
||||
import { useTransactionRequest } from '@common/hooks/use-transaction';
|
||||
|
||||
export const AttachmentRow: React.FC = () => {
|
||||
const { pendingTransaction } = useTxState();
|
||||
const pendingTransaction = useTransactionRequest();
|
||||
return pendingTransaction?.attachment ? (
|
||||
<RowItem name="Attachment" value={hexToHumanReadable(pendingTransaction.attachment)} />
|
||||
) : null;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { useTxState } from '@common/hooks/use-tx-state';
|
||||
import React, { memo } from 'react';
|
||||
import { Stack, color, StackProps } from '@stacks/ui';
|
||||
import { Divider } from '@components/divider';
|
||||
import { deserializeCV, cvToString, getCVTypeString } from '@stacks/transactions';
|
||||
@@ -8,35 +7,35 @@ import { useExplorerLink } from '@common/hooks/use-explorer-link';
|
||||
import { Caption, Title } from '@components/typography';
|
||||
import { ContractPreview } from '@components/transactions/contract-preview';
|
||||
import { RowItem } from '@components/transactions/row-item';
|
||||
import { useTransactionRequest } from '@common/hooks/use-transaction';
|
||||
import { useLoadable } from '@common/hooks/use-loadable';
|
||||
import { transactionFunctionsState } from '@store/transactions/contract-call';
|
||||
|
||||
interface ArgumentProps {
|
||||
arg: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const FunctionArgumentRow: React.FC<ArgumentProps> = ({ arg, index, ...rest }) => {
|
||||
const { pendingTransactionFunction } = useTxState();
|
||||
const FunctionArgumentRow: React.FC<ArgumentProps> = memo(({ arg, index, ...rest }) => {
|
||||
const payload = useLoadable(transactionFunctionsState);
|
||||
const argCV = deserializeCV(Buffer.from(arg, 'hex'));
|
||||
const strValue = cvToString(argCV);
|
||||
const name =
|
||||
pendingTransactionFunction.state === 'hasValue'
|
||||
? pendingTransactionFunction.contents?.args[index].name
|
||||
: null;
|
||||
const name = payload.value?.args[index].name || null;
|
||||
|
||||
return <RowItem name={name} type={getCVTypeString(argCV)} value={strValue} {...rest} />;
|
||||
};
|
||||
});
|
||||
|
||||
const FunctionArgumentsList = (props: StackProps) => {
|
||||
const { pendingTransaction } = useTxState();
|
||||
const FunctionArgumentsList = memo((props: StackProps) => {
|
||||
const transactionRequest = useTransactionRequest();
|
||||
|
||||
if (!pendingTransaction || pendingTransaction.txType !== 'contract_call') {
|
||||
if (!transactionRequest || transactionRequest.txType !== 'contract_call') {
|
||||
return null;
|
||||
}
|
||||
const hasArgs = pendingTransaction.functionArgs.length > 0;
|
||||
const hasArgs = transactionRequest.functionArgs.length > 0;
|
||||
return (
|
||||
<Stack divider={<Divider />} spacing="base" {...props}>
|
||||
{hasArgs ? (
|
||||
pendingTransaction.functionArgs.map((arg, index) => {
|
||||
transactionRequest.functionArgs.map((arg, index) => {
|
||||
return <FunctionArgumentRow key={`${arg}-${index}`} arg={arg} index={index} />;
|
||||
})
|
||||
) : (
|
||||
@@ -44,14 +43,13 @@ const FunctionArgumentsList = (props: StackProps) => {
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export const ContractCallDetails: React.FC = () => {
|
||||
const { pendingTransaction } = useTxState();
|
||||
export const ContractCallDetails = memo(() => {
|
||||
const transactionRequest = useTransactionRequest();
|
||||
const { handleOpenTxLink } = useExplorerLink();
|
||||
if (!pendingTransaction || pendingTransaction.txType !== 'contract_call') {
|
||||
return null;
|
||||
}
|
||||
if (!transactionRequest || transactionRequest.txType !== 'contract_call') return null;
|
||||
const { contractAddress, contractName, functionName, attachment } = transactionRequest;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
@@ -67,19 +65,15 @@ export const ContractCallDetails: React.FC = () => {
|
||||
</Title>
|
||||
|
||||
<ContractPreview
|
||||
onClick={() =>
|
||||
handleOpenTxLink(
|
||||
`${pendingTransaction.contractAddress}.${pendingTransaction.contractName}`
|
||||
)
|
||||
}
|
||||
contractAddress={pendingTransaction.contractAddress}
|
||||
contractName={pendingTransaction.contractName}
|
||||
functionName={pendingTransaction.functionName}
|
||||
onClick={() => handleOpenTxLink(`${contractAddress}.${contractName}`)}
|
||||
contractAddress={contractAddress}
|
||||
contractName={contractName}
|
||||
functionName={functionName}
|
||||
/>
|
||||
<Stack divider={<Divider />} spacing="base">
|
||||
<FunctionArgumentsList />
|
||||
{pendingTransaction?.attachment ? <AttachmentRow /> : null}
|
||||
{attachment && <AttachmentRow />}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Caption, Title } from '@components/typography';
|
||||
import { Divider } from '@components/divider';
|
||||
import { ContractPreview } from '@components/transactions/contract-preview';
|
||||
import { RowItem } from '@components/transactions/row-item';
|
||||
import { useTransactionRequest } from '@common/hooks/use-transaction';
|
||||
|
||||
function ContractCodeSection() {
|
||||
const { pendingTransaction } = useTxState();
|
||||
@@ -50,7 +51,7 @@ function TabButton({ isActive, ...props }: { isActive?: boolean } & BoxProps) {
|
||||
}
|
||||
|
||||
export const ContractDeployDetails: React.FC = () => {
|
||||
const { pendingTransaction } = useTxState();
|
||||
const pendingTransaction = useTransactionRequest();
|
||||
const { currentAccount, currentAccountStxAddress } = useWallet();
|
||||
const [tab, setTab] = useState<'details' | 'code'>('details');
|
||||
if (
|
||||
|
||||
77
src/components/transactions/error.tsx
Normal file
77
src/components/transactions/error.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Box, BoxProps, color, Stack, StackProps } from '@stacks/ui';
|
||||
import { Caption, Text } from '@components/typography';
|
||||
import React, { memo } from 'react';
|
||||
import { FiAlertTriangle } from 'react-icons/fi';
|
||||
|
||||
function ErrorButton({ variant, ...props }: { variant?: 'secondary' } & BoxProps) {
|
||||
return (
|
||||
<Caption
|
||||
as="button"
|
||||
border={0}
|
||||
borderRadius="12px"
|
||||
px="base"
|
||||
py="base"
|
||||
color={color('text-title')}
|
||||
bg={variant === 'secondary' ? 'transparent' : color('bg-4')}
|
||||
fontWeight={variant === 'secondary' ? 400 : 500}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ErrorMessageProps extends StackProps {
|
||||
title: string;
|
||||
body: string | JSX.Element;
|
||||
actions?: {
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
variant?: 'secondary';
|
||||
}[];
|
||||
}
|
||||
|
||||
export const ErrorMessage = memo(({ title, body, actions, ...rest }: ErrorMessageProps) => {
|
||||
return (
|
||||
<Stack
|
||||
mt="loose"
|
||||
p="loose"
|
||||
borderRadius="12px"
|
||||
border="4px solid #FCEEED"
|
||||
spacing="extra-loose"
|
||||
color={color('feedback-error')}
|
||||
{...rest}
|
||||
>
|
||||
<Stack spacing="base-loose">
|
||||
<Stack alignItems="center" isInline>
|
||||
<Box strokeWidth={2} as={FiAlertTriangle} />
|
||||
<Text color="currentColor">{title}</Text>
|
||||
</Stack>
|
||||
<Caption color={color('text-body')}>{body}</Caption>
|
||||
</Stack>
|
||||
<Stack spacing="loose">
|
||||
{actions && (
|
||||
<Stack isInline flexWrap="wrap">
|
||||
{actions.map(action => (
|
||||
<ErrorButton
|
||||
flexGrow={1}
|
||||
borderRadius="12px"
|
||||
onClick={action.onClick}
|
||||
variant={action.variant}
|
||||
>
|
||||
{action.label}
|
||||
</ErrorButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
<ErrorButton
|
||||
p={0}
|
||||
flexGrow={1}
|
||||
borderRadius="12px"
|
||||
onClick={() => window.close()}
|
||||
variant="secondary"
|
||||
>
|
||||
Close window
|
||||
</ErrorButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
23
src/components/transactions/fee.tsx
Normal file
23
src/components/transactions/fee.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React, { memo } from 'react';
|
||||
import { useSignedTransaction } from '@common/hooks/use-transaction';
|
||||
import { LoadingRectangle } from '@components/loading-rectangle';
|
||||
import { AuthType } from '@stacks/transactions';
|
||||
import { stacksValue } from '@common/stacks-utils';
|
||||
|
||||
export const FeeComponent = memo(() => {
|
||||
const signedTransaction = useSignedTransaction();
|
||||
if (signedTransaction.isLoading) return <LoadingRectangle width="100px" height="14px" />;
|
||||
if (!signedTransaction.value) return null;
|
||||
const sponsored = signedTransaction.value.auth.authType === AuthType.Sponsored;
|
||||
const value = signedTransaction.value.auth.spendingCondition?.fee?.toNumber() || 0;
|
||||
return (
|
||||
<>
|
||||
{sponsored
|
||||
? '🎉 sponsored'
|
||||
: stacksValue({
|
||||
value,
|
||||
fixedDecimals: true,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
27
src/components/transactions/page-top.tsx
Normal file
27
src/components/transactions/page-top.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { memo } from 'react';
|
||||
import { useTransactionRequest } from '@common/hooks/use-transaction';
|
||||
import { useOrigin } from '@common/hooks/use-origin';
|
||||
import { useTransactionPageTitle } from '@common/hooks/use-transaction-page-title';
|
||||
import { Stack } from '@stacks/ui';
|
||||
import { Caption, Title } from '@components/typography';
|
||||
|
||||
export const TransactionPageTop = memo(() => {
|
||||
const transactionRequest = useTransactionRequest();
|
||||
const origin = useOrigin();
|
||||
const pageTitle = useTransactionPageTitle();
|
||||
if (!transactionRequest) return null;
|
||||
|
||||
const appName = transactionRequest?.appDetails?.name;
|
||||
return (
|
||||
<Stack pt="extra-loose" spacing="base">
|
||||
<Title fontWeight="bold" as="h1">
|
||||
{pageTitle}
|
||||
</Title>
|
||||
{appName ? (
|
||||
<Caption>
|
||||
Requested by {appName} {origin ? `(${origin?.split('//')[1]})` : null}
|
||||
</Caption>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
@@ -1,12 +1,6 @@
|
||||
import React from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Box, Circle, color, Flex, Stack } from '@stacks/ui';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
currentPostConditionIndexStore,
|
||||
pendingTransactionStore,
|
||||
postConditionsStore,
|
||||
showTxDetails,
|
||||
} from '@store/transaction';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { PostConditionComponent } from './single';
|
||||
import { TransactionTypes } from '@stacks/connect';
|
||||
import { stacksValue } from '@common/stacks-utils';
|
||||
@@ -14,28 +8,12 @@ import { IconLock } from '@tabler/icons';
|
||||
import { Body } from '@components/typography';
|
||||
import { TransactionEventCard } from '@components/transactions/event-card';
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
import { useSelectedAsset } from '@common/hooks/use-selected-asset';
|
||||
|
||||
function usePostconditionsState() {
|
||||
const showDetails = useRecoilValue(showTxDetails);
|
||||
const pendingTransaction = useRecoilValue(pendingTransactionStore);
|
||||
const setShowDetails = useSetRecoilState(showTxDetails);
|
||||
const postConditions = useRecoilValue(postConditionsStore);
|
||||
const setCurrentPostConditionIndex = useSetRecoilState(currentPostConditionIndexStore);
|
||||
const { handleUpdateSelectedAsset } = useSelectedAsset();
|
||||
|
||||
return {
|
||||
showDetails,
|
||||
pendingTransaction,
|
||||
setShowDetails,
|
||||
postConditions,
|
||||
setCurrentPostConditionIndex,
|
||||
handleUpdateSelectedAsset,
|
||||
};
|
||||
}
|
||||
import { useLoadable } from '@common/hooks/use-loadable';
|
||||
import { postConditionsState } from '@store/transactions';
|
||||
import { useTransactionRequest } from '@common/hooks/use-transaction';
|
||||
|
||||
function StxPostcondition() {
|
||||
const { pendingTransaction } = usePostconditionsState();
|
||||
const pendingTransaction = useTransactionRequest();
|
||||
if (!pendingTransaction || pendingTransaction.txType !== TransactionTypes.STXTransfer)
|
||||
return null;
|
||||
return (
|
||||
@@ -70,11 +48,12 @@ function NoPostconditions() {
|
||||
);
|
||||
}
|
||||
|
||||
function PostConditionsList() {
|
||||
const { postConditions } = usePostconditionsState();
|
||||
const PostConditionsList = memo(() => {
|
||||
const postConditions = useRecoilValue(postConditionsState);
|
||||
|
||||
return (
|
||||
<>
|
||||
{postConditions.map((pc, index) => (
|
||||
{postConditions?.map((pc, index) => (
|
||||
<PostConditionComponent
|
||||
pc={pc}
|
||||
key={`${pc.type}-${pc.conditionCode}`}
|
||||
@@ -83,16 +62,20 @@ function PostConditionsList() {
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const PostConditions: React.FC = () => {
|
||||
const { pendingTransaction, postConditions } = usePostconditionsState();
|
||||
|
||||
const hasPostConditions = postConditions.length > 0;
|
||||
});
|
||||
|
||||
export const PostConditions: React.FC = memo(() => {
|
||||
const { value: postConditions } = useLoadable(postConditionsState);
|
||||
const pendingTransaction = useTransactionRequest();
|
||||
const hasPostConditions = useMemo(
|
||||
() => postConditions && postConditions?.length > 0,
|
||||
[postConditions]
|
||||
);
|
||||
const isStxTransfer =
|
||||
pendingTransaction?.txType === TransactionTypes.STXTransfer && !hasPostConditions;
|
||||
|
||||
if (!postConditions || !pendingTransaction) return <></>;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
border="4px solid"
|
||||
@@ -111,4 +94,4 @@ export const PostConditions: React.FC = () => {
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import { addressToString, PostCondition } from '@stacks/transactions';
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
import { ftDecimals, getPostConditionTitle } from '@common/stacks-utils';
|
||||
|
||||
import { usePendingTransaction } from '@common/hooks/use-pending-transaction';
|
||||
import { TransactionEventCard } from '@components/transactions/event-card';
|
||||
import { useCurrentAccount } from '@common/hooks/use-current-account';
|
||||
import {
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
getSymbolFromPostCondition,
|
||||
useAssetInfoFromPostCondition,
|
||||
} from '@common/postcondition-utils';
|
||||
import { useTransactionRequest } from '@common/hooks/use-transaction';
|
||||
|
||||
interface PostConditionProps {
|
||||
pc: PostCondition;
|
||||
@@ -24,7 +24,7 @@ interface PostConditionProps {
|
||||
export const PostConditionComponent: React.FC<PostConditionProps> = ({ pc, isLast }) => {
|
||||
const { stxAddress } = useCurrentAccount();
|
||||
const asset = useAssetInfoFromPostCondition(pc);
|
||||
const pendingTransaction = usePendingTransaction();
|
||||
const pendingTransaction = useTransactionRequest();
|
||||
const title = getPostConditionTitle(pc);
|
||||
const iconString = getIconStringFromPostCondition(pc);
|
||||
const _ticker = getSymbolFromPostCondition(pc);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useTxState } from '@common/hooks/use-tx-state';
|
||||
import { color, Stack } from '@stacks/ui';
|
||||
import { AttachmentRow } from './attachment-row';
|
||||
import { RowItem } from '@components/transactions/row-item';
|
||||
import { Title } from '@components/typography';
|
||||
import { Divider } from '@components/divider';
|
||||
import { useTransactionRequest } from '@common/hooks/use-transaction';
|
||||
|
||||
export const StxTransferDetails: React.FC = () => {
|
||||
const { pendingTransaction } = useTxState();
|
||||
const pendingTransaction = useTransactionRequest();
|
||||
if (!pendingTransaction || pendingTransaction.txType !== 'token_transfer') {
|
||||
return null;
|
||||
}
|
||||
|
||||
120
src/components/transactions/transaction-errors.tsx
Normal file
120
src/components/transactions/transaction-errors.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { memo } from 'react';
|
||||
import { useCurrentAccount } from '@common/hooks/use-current-account';
|
||||
import { color, Stack, useClipboard } from '@stacks/ui';
|
||||
import { useTransactionRequest } from '@common/hooks/use-transaction';
|
||||
import { useFetchBalances } from '@common/hooks/use-account-info';
|
||||
import { Caption } from '@components/typography';
|
||||
import { SpaceBetween } from '@components/space-between';
|
||||
import { stacksValue } from '@common/stacks-utils';
|
||||
import { STXTransferPayload, TransactionTypes } from '@stacks/connect';
|
||||
import { useCurrentNetwork } from '@common/hooks/use-current-network';
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
import { useTxState } from '@common/hooks/use-tx-state';
|
||||
import { ErrorMessage } from '@components/transactions/error';
|
||||
import { useDrawers } from '@common/hooks/use-drawers';
|
||||
|
||||
export const FeeInsufficientFundsErrorMessage = memo(props => {
|
||||
const currentAccount = useCurrentAccount();
|
||||
const { setShowAccounts } = useDrawers();
|
||||
const { onCopy, hasCopied } = useClipboard(currentAccount.address || '');
|
||||
return (
|
||||
<ErrorMessage
|
||||
title="Insufficient balance"
|
||||
body={`You do not have enough STX to cover the network fees for this transaction.`}
|
||||
actions={[
|
||||
{ onClick: () => setShowAccounts(true), label: 'Switch account' },
|
||||
{ onClick: () => onCopy(), label: hasCopied ? 'Copied!' : 'Copy address' },
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const StxTransferInsufficientFundsErrorMessage = memo(props => {
|
||||
const pendingTransaction = useTransactionRequest();
|
||||
const balances = useFetchBalances();
|
||||
const currentAccount = useCurrentAccount();
|
||||
const { setShowAccounts } = useDrawers();
|
||||
const { onCopy, hasCopied } = useClipboard(currentAccount.address || '');
|
||||
return (
|
||||
<ErrorMessage
|
||||
title="Insufficient balance"
|
||||
body={
|
||||
<Stack spacing="loose">
|
||||
<Caption color={color('text-body')}>
|
||||
You don't have enough STX to make this transfer. Send some STX to this address, or
|
||||
switch to another account.
|
||||
</Caption>
|
||||
|
||||
<Stack spacing="base" justifyContent="flex-end" textAlign="right">
|
||||
<SpaceBetween>
|
||||
<Caption>Current balance</Caption>
|
||||
<Caption>
|
||||
{balances?.value?.stx?.balance
|
||||
? stacksValue({
|
||||
value: balances?.value?.stx?.balance,
|
||||
withTicker: true,
|
||||
})
|
||||
: '--'}
|
||||
</Caption>
|
||||
</SpaceBetween>
|
||||
<SpaceBetween>
|
||||
<Caption>Transfer amount</Caption>
|
||||
<Caption>
|
||||
{stacksValue({
|
||||
value: (pendingTransaction as STXTransferPayload).amount,
|
||||
withTicker: true,
|
||||
})}
|
||||
</Caption>
|
||||
</SpaceBetween>
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
actions={[
|
||||
{ onClick: () => setShowAccounts(true), label: 'Switch account' },
|
||||
{ onClick: () => onCopy(), label: hasCopied ? 'Copied!' : 'Copy address' },
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const NoContractErrorMessage = memo(props => {
|
||||
const network = useCurrentNetwork();
|
||||
const pendingTransaction = useTransactionRequest();
|
||||
|
||||
if (!pendingTransaction || pendingTransaction.txType !== TransactionTypes.ContractCall)
|
||||
return null;
|
||||
return (
|
||||
<ErrorMessage
|
||||
title="Contract not found"
|
||||
body={`The contract (${truncateMiddle(pendingTransaction.contractAddress)}.${
|
||||
pendingTransaction.contractName
|
||||
}) that you are trying to call cannot be found on ${network.mode}.`}
|
||||
actions={[{ onClick: () => window.close(), label: 'Switch network' }]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const UnauthorizedErrorMessage = memo(props => {
|
||||
return (
|
||||
<ErrorMessage
|
||||
title="Unauthorized request"
|
||||
body="The transaction request was not properly authorized by any of your accounts. If you've logged in to this app before, then you might need to re-authenticate into this application before attempting to sign a transaction with the Stacks Wallet."
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const BroadcastErrorMessage = memo(props => {
|
||||
const { broadcastError } = useTxState();
|
||||
if (!broadcastError) return null;
|
||||
return (
|
||||
<ErrorMessage
|
||||
title="There was an error when broadcasting this transaction:"
|
||||
body={broadcastError}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -124,6 +124,7 @@ export const Text = forwardRefWithAs<BoxProps, 'span'>((props, ref) => (
|
||||
letterSpacing="-0.01em"
|
||||
color={color('text-body')}
|
||||
display="block"
|
||||
lineHeight="1.5"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,6 @@ export const Unlock: React.FC = () => {
|
||||
try {
|
||||
await doUnlockWallet(password);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError('The password you entered is invalid.');
|
||||
}
|
||||
setLoading(false);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
import { Box, Text, Input, InputGroup, Button } from '@stacks/ui';
|
||||
import { Formik } from 'formik';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { currentNetworkKeyStore, networksStore } from '@store/networks';
|
||||
import { currentNetworkKeyState, networksState } from '@store/networks';
|
||||
import { PopupContainer } from '@components/popup/container';
|
||||
import { useDoChangeScreen } from '@common/hooks/use-do-change-screen';
|
||||
import { ScreenPaths } from '@store/common/types';
|
||||
@@ -15,8 +15,8 @@ export const AddNetwork: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const doChangeScreen = useDoChangeScreen();
|
||||
const setNetworks = useSetRecoilState(networksStore);
|
||||
const setNetworkKey = useSetRecoilState(currentNetworkKeyStore);
|
||||
const setNetworks = useSetRecoilState(networksState);
|
||||
const setNetworkKey = useSetRecoilState(currentNetworkKeyState);
|
||||
|
||||
return (
|
||||
<PopupContainer
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<<<<<<< HEAD
|
||||
// @ts-nocheck TODO: actually use this page
|
||||
=======
|
||||
// @ts-nocheck
|
||||
>>>>>>> 3805c0e8 (refactor: continue refactor)
|
||||
import React, { Suspense, useEffect } from 'react';
|
||||
import { Box, Button, Flex, Text, Input } from '@stacks/ui';
|
||||
import { TxLoading } from '@pages/transaction';
|
||||
|
||||
@@ -1,179 +1,27 @@
|
||||
import React, { useMemo, useCallback, useState } from 'react';
|
||||
import { Box, Button, color, Flex, Stack, StackProps } from '@stacks/ui';
|
||||
import React, { memo } from 'react';
|
||||
import { PopupHeader } from '@components/transactions/popup-header';
|
||||
import { PopupContainer } from '@components/popup/container';
|
||||
import { LoadingRectangle } from '@components/loading-rectangle';
|
||||
import { useTxState } from '@common/hooks/use-tx-state';
|
||||
import { useFetchBalances } from '@common/hooks/use-account-info';
|
||||
import { useSetupTx } from '@common/hooks/use-setup-tx';
|
||||
import { stacksValue } from '@common/stacks-utils';
|
||||
import { TransactionsActions } from '@components/transactions/actions';
|
||||
import { TransactionError } from './transaction-error';
|
||||
import { TransactionPageTop } from '@components/transactions/page-top';
|
||||
import { ContractCallDetails } from '@components/transactions/contract-call-details';
|
||||
import { StxTransferDetails } from '@components/transactions/stx-transfer-details';
|
||||
import { ContractDeployDetails } from '@components/transactions/contract-deploy-details';
|
||||
import { PostConditions } from '@components/transactions/post-conditions/list';
|
||||
import { showTxDetails } from '@store/transaction';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { TransactionTypes } from '@stacks/connect';
|
||||
import { useWallet } from '@common/hooks/use-wallet';
|
||||
import { TransactionError, TransactionErrorReason } from './transaction-error';
|
||||
import { AuthType } from '@stacks/transactions';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { Caption, Title } from '@components/typography';
|
||||
import { useOrigin } from '@common/hooks/use-origin';
|
||||
import { SpaceBetween } from '@components/space-between';
|
||||
import { useDrawers } from '@common/hooks/use-drawers';
|
||||
import { PopupHeader } from '@components/transactions/popup-header';
|
||||
import { NetworkRowItem } from '@components/network-row-item';
|
||||
import { StxTransferDetails } from '@components/transactions/stx-transfer-details';
|
||||
import { useTransactionRequest } from '@common/hooks/use-transaction';
|
||||
|
||||
export const TxLoading: React.FC = () => {
|
||||
export const TransactionPage = memo(() => {
|
||||
const transactionRequest = useTransactionRequest();
|
||||
if (!transactionRequest) return null;
|
||||
return (
|
||||
<Flex flexDirection="column" mt="extra-loose">
|
||||
<Box width="100%">
|
||||
<LoadingRectangle width="60%" height="24px" />
|
||||
</Box>
|
||||
<Box width="100%" mt="base">
|
||||
<LoadingRectangle width="40%" height="16px" />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeeValue = () => {
|
||||
const { signedTransaction: tx } = useTxState();
|
||||
if (tx.state === 'loading' && !tx.value) return <LoadingRectangle width="100px" height="14px" />;
|
||||
|
||||
if (!tx.value) return null;
|
||||
|
||||
const sponsored = tx.value.auth.authType === AuthType.Sponsored;
|
||||
|
||||
return (
|
||||
<>
|
||||
{sponsored
|
||||
? '🎉 sponsored'
|
||||
: stacksValue({
|
||||
value: tx.value.auth.spendingCondition?.fee?.toNumber() || 0,
|
||||
fixedDecimals: true,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function useTransactionError() {
|
||||
const { pendingTransaction, broadcastError, isUnauthorizedTransaction } = useTxState();
|
||||
const { currentAccount } = useWallet();
|
||||
const balances = useFetchBalances();
|
||||
|
||||
return useMemo<TransactionErrorReason | void>(() => {
|
||||
if (isUnauthorizedTransaction) return TransactionErrorReason.Unauthorized;
|
||||
|
||||
if (!pendingTransaction || balances.errorMaybe() || !currentAccount) {
|
||||
return TransactionErrorReason.Generic;
|
||||
}
|
||||
if (broadcastError) return TransactionErrorReason.BroadcastError;
|
||||
if (balances.value) {
|
||||
const stxBalance = new BigNumber(balances.value.stx.balance);
|
||||
if (pendingTransaction.txType === TransactionTypes.STXTransfer) {
|
||||
const transferAmount = new BigNumber(pendingTransaction.amount);
|
||||
if (transferAmount.gte(stxBalance)) {
|
||||
return TransactionErrorReason.StxTransferInsufficientFunds;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}, [balances, currentAccount, pendingTransaction, broadcastError, isUnauthorizedTransaction]);
|
||||
}
|
||||
|
||||
function useTransactionPageTitle() {
|
||||
const { pendingTransaction } = useTxState();
|
||||
const txType = pendingTransaction?.txType;
|
||||
return useMemo(() => {
|
||||
if (!pendingTransaction) return;
|
||||
if (txType === TransactionTypes.STXTransfer) return 'Confirm transfer';
|
||||
if (txType === TransactionTypes.ContractDeploy) return 'Deploy contract';
|
||||
if (txType === TransactionTypes.ContractCall && 'functionName' in pendingTransaction)
|
||||
return pendingTransaction.functionName || 'Sign transaction';
|
||||
return 'Sign transaction';
|
||||
}, [pendingTransaction, txType]);
|
||||
}
|
||||
|
||||
export const TransactionPage: React.FC = () => {
|
||||
const isSetup = useSetupTx();
|
||||
|
||||
const { pendingTransaction } = useTxState();
|
||||
const { currentAccount } = useWallet();
|
||||
const origin = useOrigin();
|
||||
const pageTitle = useTransactionPageTitle();
|
||||
const error = useTransactionError();
|
||||
const showDetails = useRecoilValue(showTxDetails);
|
||||
const appName = pendingTransaction?.appDetails?.name;
|
||||
|
||||
if (!isSetup) {
|
||||
// loading state
|
||||
return <></>;
|
||||
}
|
||||
if (error !== undefined) {
|
||||
return <TransactionError reason={error} />;
|
||||
}
|
||||
if (!currentAccount || !pendingTransaction) throw new Error('Invalid code path.');
|
||||
|
||||
return (
|
||||
<PopupContainer header={<PopupHeader />} requestType="transaction">
|
||||
<Stack pt="extra-loose" spacing="base">
|
||||
<Title fontWeight="bold" as="h1">
|
||||
{pageTitle}
|
||||
</Title>
|
||||
{appName ? (
|
||||
<Caption>
|
||||
Requested by {appName} {origin ? `(${origin?.split('//')[1]})` : null}
|
||||
</Caption>
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
<PopupContainer header={<PopupHeader />}>
|
||||
<TransactionPageTop />
|
||||
<TransactionError />
|
||||
<PostConditions />
|
||||
{showDetails && (
|
||||
<>
|
||||
<ContractCallDetails />
|
||||
<StxTransferDetails />
|
||||
<ContractDeployDetails />
|
||||
</>
|
||||
)}
|
||||
<ActionBar />
|
||||
<ContractCallDetails />
|
||||
<StxTransferDetails />
|
||||
<ContractDeployDetails />
|
||||
<TransactionsActions />
|
||||
</PopupContainer>
|
||||
);
|
||||
};
|
||||
|
||||
function ActionBar(props: StackProps) {
|
||||
const { signedTransaction, doSubmitPendingTransaction } = useTxState();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const handleSubmit = useCallback(async () => {
|
||||
setIsSubmitting(true);
|
||||
await doSubmitPendingTransaction();
|
||||
setIsSubmitting(false);
|
||||
}, [setIsSubmitting, doSubmitPendingTransaction]);
|
||||
const { setShowNetworks } = useDrawers();
|
||||
return (
|
||||
<Stack mt="auto" pt="loose" spacing="loose" bg={color('bg')} {...props}>
|
||||
<Stack spacing="base-loose">
|
||||
<SpaceBetween>
|
||||
<Caption>Fees</Caption>
|
||||
<Caption>
|
||||
<FeeValue />
|
||||
</Caption>
|
||||
</SpaceBetween>
|
||||
<SpaceBetween>
|
||||
<Caption>Network</Caption>
|
||||
<NetworkRowItem onClick={() => setShowNetworks(true)} />
|
||||
</SpaceBetween>
|
||||
</Stack>
|
||||
<Button
|
||||
borderRadius="12px"
|
||||
py="base"
|
||||
width="100%"
|
||||
onClick={handleSubmit}
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={!signedTransaction.value}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,120 +1,37 @@
|
||||
import React from 'react';
|
||||
import { PopupContainer } from '@components/popup/container';
|
||||
import { Box, Text, Button } from '@stacks/ui';
|
||||
import { useTxState } from '@common/hooks/use-tx-state';
|
||||
import { STXTransferPayload } from '@stacks/connect';
|
||||
import { stacksValue } from '@common/stacks-utils';
|
||||
import { useFetchBalances } from '@common/hooks/use-account-info';
|
||||
import { LoadingRectangle } from '@components/loading-rectangle';
|
||||
import { useWallet } from '@common/hooks/use-wallet';
|
||||
import { Body } from '@components/typography';
|
||||
import { Header } from '@components/header';
|
||||
import React, { memo } from 'react';
|
||||
import { useTransactionError } from '@common/hooks/use-transaction-error';
|
||||
import {
|
||||
BroadcastErrorMessage,
|
||||
FeeInsufficientFundsErrorMessage,
|
||||
NoContractErrorMessage,
|
||||
StxTransferInsufficientFundsErrorMessage,
|
||||
UnauthorizedErrorMessage,
|
||||
} from '@components/transactions/transaction-errors';
|
||||
|
||||
export enum TransactionErrorReason {
|
||||
StxTransferInsufficientFunds = 0,
|
||||
FeeInsufficientFunds = 1,
|
||||
Generic = 2,
|
||||
BroadcastError = 3,
|
||||
Unauthorized = 4,
|
||||
StxTransferInsufficientFunds = 1,
|
||||
FeeInsufficientFunds = 2,
|
||||
Generic = 3,
|
||||
BroadcastError = 4,
|
||||
Unauthorized = 5,
|
||||
NoContract = 6,
|
||||
}
|
||||
|
||||
interface TransactionErrorProps {
|
||||
reason: TransactionErrorReason;
|
||||
}
|
||||
|
||||
export const TransactionError: React.FC<TransactionErrorProps> = ({ reason }) => {
|
||||
const { pendingTransaction, broadcastError } = useTxState();
|
||||
const { currentAccountDisplayName, currentAccountStxAddress, currentNetwork } = useWallet();
|
||||
const balances = useFetchBalances();
|
||||
|
||||
return (
|
||||
<PopupContainer header={<Header />}>
|
||||
<Box mt="loose" />
|
||||
{reason === TransactionErrorReason.StxTransferInsufficientFunds ? (
|
||||
<Box>
|
||||
<Text display="block" fontSize={2}>
|
||||
You don't have enough STX to make this transfer. Try switching accounts, switching
|
||||
networks, or adding STX to this account.
|
||||
</Text>
|
||||
<Box my="base">
|
||||
<Text fontSize={2} fontWeight="500">
|
||||
Transfer amount:
|
||||
</Text>
|
||||
<Text fontSize={2} ml="tight" fontFamily="mono">
|
||||
{stacksValue({
|
||||
value: (pendingTransaction as STXTransferPayload).amount,
|
||||
withTicker: true,
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box my="base">
|
||||
<Text fontSize={2} fontWeight="500">
|
||||
Current balance:
|
||||
</Text>
|
||||
{balances.state === 'hasValue' && balances.contents ? (
|
||||
<Text fontSize={2} ml="tight" fontFamily="mono">
|
||||
{stacksValue({ value: balances.contents.stx.balance, withTicker: true })}
|
||||
</Text>
|
||||
) : (
|
||||
<LoadingRectangle height="16px" width="50px" />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
{reason === TransactionErrorReason.BroadcastError ? (
|
||||
<Box>
|
||||
<Text display="block" fontSize={2}>
|
||||
There was an error when broadcasting this transaction:
|
||||
</Text>
|
||||
<Box my="base">
|
||||
<Text fontSize={2} fontFamily="mono">
|
||||
{broadcastError}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
{reason === TransactionErrorReason.Unauthorized ? (
|
||||
<Box mb="base">
|
||||
<Body display="block" fontSize={2} my="loose">
|
||||
The request to sign a transaction was not properly authorized by any of your accounts.
|
||||
</Body>
|
||||
<Body display="block" fontSize={2} my="loose">
|
||||
If you've logged in to this app before, then you might need to re-authenticate into this
|
||||
application before attempting to sign a transaction with the Stacks Wallet.
|
||||
</Body>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box mb="base">
|
||||
<Text fontSize={2} fontWeight="500">
|
||||
Current Network:
|
||||
</Text>
|
||||
<Text fontSize={2} ml="tight" fontFamily="mono">
|
||||
{currentNetwork.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width="100%">
|
||||
<Text
|
||||
fontSize={2}
|
||||
fontWeight="600"
|
||||
fontFamily="heading"
|
||||
color="ink.1000"
|
||||
display="block"
|
||||
>
|
||||
{currentAccountDisplayName}
|
||||
</Text>
|
||||
<Text textStyle="body.small" color="ink.600">
|
||||
{currentAccountStxAddress}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<Box flexGrow={1} />
|
||||
<Box mt="extra-loose" mb="base">
|
||||
<Button width="100%" onClick={() => window.close()}>
|
||||
Close
|
||||
</Button>
|
||||
</Box>
|
||||
</PopupContainer>
|
||||
);
|
||||
};
|
||||
export const TransactionError = memo(() => {
|
||||
const reason = useTransactionError();
|
||||
if (!reason) return null;
|
||||
switch (reason) {
|
||||
case TransactionErrorReason.NoContract:
|
||||
return <NoContractErrorMessage />;
|
||||
case TransactionErrorReason.StxTransferInsufficientFunds:
|
||||
return <StxTransferInsufficientFundsErrorMessage />;
|
||||
case TransactionErrorReason.BroadcastError:
|
||||
return <BroadcastErrorMessage />;
|
||||
case TransactionErrorReason.FeeInsufficientFunds:
|
||||
return <FeeInsufficientFundsErrorMessage />;
|
||||
case TransactionErrorReason.Unauthorized:
|
||||
return <UnauthorizedErrorMessage />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,42 +1,40 @@
|
||||
import { atom, selector } from 'recoil';
|
||||
import { currentNetworkStore, currentTransactionVersion } from '@store/networks';
|
||||
import { MempoolTransaction, Transaction } from '@blockstack/stacks-blockchain-api-types';
|
||||
import { Account, getStxAddress } from '@stacks/wallet-sdk';
|
||||
import { walletState } from '@store/wallet';
|
||||
import { apiRevalidation, intervalStore } from '@store/common/api';
|
||||
import { fetchAllAccountData } from '@common/api/accounts';
|
||||
import { atom, selector, waitForAll } from 'recoil';
|
||||
import BN from 'bn.js';
|
||||
|
||||
import type { AllAccountData } from '@common/api/accounts';
|
||||
import { fetchAllAccountData } from '@common/api/accounts';
|
||||
import { apiRevalidation, intervalStore } from '@store/common/api-helpers';
|
||||
import { fetcher } from '@common/wrapped-fetch';
|
||||
|
||||
import { transactionRequestStxAddressState } from '@store/transactions/requests';
|
||||
import { currentNetworkState, networkTransactionVersionState } from '@store/networks';
|
||||
import { DEFAULT_POLLING_INTERVAL } from '@store/common/constants';
|
||||
import { walletState } from '@store/wallet';
|
||||
|
||||
export const currentAccountIndexStore = atom<number | undefined>({
|
||||
key: 'wallet.current-account-index',
|
||||
default: undefined,
|
||||
});
|
||||
|
||||
export const currentAccountStore = selector({
|
||||
key: 'wallet.current-account',
|
||||
get: ({ get }) => {
|
||||
const accountIndex = get(currentAccountIndexStore);
|
||||
const wallet = get(walletState);
|
||||
if (accountIndex === undefined || !wallet) {
|
||||
return undefined;
|
||||
}
|
||||
return wallet.accounts[accountIndex];
|
||||
},
|
||||
});
|
||||
|
||||
export const currentAccountStxAddressStore = selector({
|
||||
key: 'wallet.current-stx-address',
|
||||
get: ({ get }) => {
|
||||
const account = get(currentAccountStore);
|
||||
if (!account) return undefined;
|
||||
const transactionVersion = get(currentTransactionVersion);
|
||||
return getStxAddress({ account, transactionVersion });
|
||||
},
|
||||
});
|
||||
/**
|
||||
* --------------------------------------
|
||||
* Overview
|
||||
* --------------------------------------
|
||||
*
|
||||
* accountsState -- the array of accounts from the wallet state, uses `walletState`
|
||||
* accountsWithAddressState - a mapped array of the above state with the current network mode stx address included
|
||||
* currentAccountIndexStore - the last selected index by the user (or default 0)
|
||||
* hasSwitchedAccountsState - a toggle for when a user switches accounts during a pending transaction request
|
||||
* transactionAccountIndexState - if `stxAccount` is passed with a transaction request, this is the index of that address
|
||||
* currentAccountState - the current active account, either the account associated with a pending transaction request, or the one selected by the user
|
||||
* currentAccountStxAddressState - a selector for the address of the current account
|
||||
* accountDataState -- all external API data for the selected STX address
|
||||
* accountBalancesState - a selector for the balances from `accountDataState`
|
||||
* accountInfoStore - external API data from the `v2/accounts` endpoint, should be the most up-to-date
|
||||
*/
|
||||
|
||||
//--------------------------------------
|
||||
// All accounts
|
||||
//--------------------------------------
|
||||
export const accountsState = selector<Account[] | undefined>({
|
||||
key: 'wallet.accounts',
|
||||
key: 'accounts.base',
|
||||
get: ({ get }) => {
|
||||
const wallet = get(walletState);
|
||||
if (!wallet) return undefined;
|
||||
@@ -44,50 +42,134 @@ export const accountsState = selector<Account[] | undefined>({
|
||||
},
|
||||
});
|
||||
|
||||
export const accountBalancesStore = selector({
|
||||
key: 'api.account-balances',
|
||||
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<AccountWithAddress[] | undefined>({
|
||||
key: 'accounts.with-address',
|
||||
get: ({ get }) => {
|
||||
const accountData = get(accountDataStore);
|
||||
return accountData?.balances;
|
||||
const accounts = get(accountsState);
|
||||
const transactionVersion = get(networkTransactionVersionState);
|
||||
if (!accounts) return undefined;
|
||||
return accounts.map(account => {
|
||||
const address = getStxAddress({ account, transactionVersion });
|
||||
return {
|
||||
...account,
|
||||
address,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const accountDataStore = selector({
|
||||
key: 'api.account-data',
|
||||
get: async ({ get }) => {
|
||||
get(apiRevalidation);
|
||||
get(intervalStore(DEFAULT_POLLING_INTERVAL));
|
||||
const { url } = get(currentNetworkStore);
|
||||
const address = get(currentAccountStxAddressStore);
|
||||
if (!address) {
|
||||
console.error('Cannot get account info when logged out.');
|
||||
return;
|
||||
//--------------------------------------
|
||||
// Current account
|
||||
//--------------------------------------
|
||||
|
||||
// The index of the current account
|
||||
// persists through sessions (viewings)
|
||||
export const currentAccountIndexStore = atom<number | undefined>({
|
||||
key: 'wallet.current-account-index',
|
||||
default: undefined,
|
||||
});
|
||||
|
||||
// This is only used when there is a pending transaction request and
|
||||
// the user switches accounts during the signing process
|
||||
export const hasSwitchedAccountsState = atom<boolean>({
|
||||
key: 'account.has-switched',
|
||||
default: 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<number | undefined>({
|
||||
key: 'account.transaction-account-index',
|
||||
get: ({ get }) => {
|
||||
const { accounts, txAddress } = get(
|
||||
waitForAll({
|
||||
accounts: accountsWithAddressState,
|
||||
txAddress: transactionRequestStxAddressState,
|
||||
})
|
||||
);
|
||||
if (txAddress && accounts) {
|
||||
const selectedAccount = accounts.findIndex(account => account.address === txAddress);
|
||||
if (selectedAccount) return selectedAccount;
|
||||
}
|
||||
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<AccountWithAddress | undefined>({
|
||||
key: 'account.current',
|
||||
get: ({ get }) => {
|
||||
const { accountIndex, txIndex, hasSwitched, accounts } = get(
|
||||
waitForAll({
|
||||
accountIndex: currentAccountIndexStore,
|
||||
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,
|
||||
});
|
||||
|
||||
// gets the address of the current account (in the current network mode)
|
||||
export const currentAccountStxAddressState = selector<string | undefined>({
|
||||
key: 'account.current-address',
|
||||
get: ({ get }) => get(currentAccountState)?.address,
|
||||
});
|
||||
|
||||
// external API data associated with the current account's address
|
||||
export const accountDataState = selector<AllAccountData | undefined>({
|
||||
key: 'account.data',
|
||||
get: async ({ get }) => {
|
||||
const { network, address } = get(
|
||||
waitForAll({
|
||||
apiRevalidation,
|
||||
interval: intervalStore(DEFAULT_POLLING_INTERVAL),
|
||||
network: currentNetworkState,
|
||||
address: currentAccountStxAddressState,
|
||||
})
|
||||
);
|
||||
if (!address) return;
|
||||
try {
|
||||
return fetchAllAccountData(url)(address);
|
||||
return fetchAllAccountData(network.url)(address);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error(`Unable to fetch account data from ${url}`);
|
||||
console.error(`Unable to fetch account data from ${network.url}`);
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const accountInfoStore = selector<{ balance: BN; nonce: number }>({
|
||||
key: 'wallet.account-info',
|
||||
// the balances of the current account's address
|
||||
export const accountBalancesState = selector<AllAccountData['balances'] | undefined>({
|
||||
key: 'account.balances',
|
||||
get: ({ get }) => get(accountDataState)?.balances,
|
||||
});
|
||||
|
||||
// the raw account info from the `v2/accounts` endpoint, should be most up-to-date info (compared to the extended API)
|
||||
export const accountInfoStore = selector<undefined | { balance: BN; nonce: number }>({
|
||||
key: 'account.info',
|
||||
get: async ({ get }) => {
|
||||
get(apiRevalidation);
|
||||
get(intervalStore(DEFAULT_POLLING_INTERVAL));
|
||||
const address = get(currentAccountStxAddressStore);
|
||||
if (!address) {
|
||||
throw new Error('Cannot get account info when logged out.');
|
||||
}
|
||||
const network = get(currentNetworkStore);
|
||||
const { address, network } = get(
|
||||
waitForAll({
|
||||
revalidation: apiRevalidation,
|
||||
interval: intervalStore(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, {
|
||||
credentials: 'omit',
|
||||
});
|
||||
const response = await fetcher(url);
|
||||
if (!response.ok) throw error;
|
||||
const data = await response.json();
|
||||
return {
|
||||
@@ -97,10 +179,11 @@ export const accountInfoStore = selector<{ balance: BN; nonce: number }>({
|
||||
},
|
||||
});
|
||||
|
||||
export const accountTransactionsState = selector({
|
||||
key: 'activity.transactions',
|
||||
// combo of pending and confirmed transactions for the current address
|
||||
export const accountTransactionsState = selector<(MempoolTransaction | Transaction)[]>({
|
||||
key: 'account.all-transactions',
|
||||
get: async ({ get }) => {
|
||||
const data = get(accountDataStore);
|
||||
const data = get(accountDataState);
|
||||
const transactions = data?.transactions?.results || [];
|
||||
const pending = data?.pendingTransactions || [];
|
||||
return [...pending, ...transactions];
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getStxAddress } from '@stacks/wallet-sdk';
|
||||
import { fetcher } from '@common/wrapped-fetch';
|
||||
|
||||
import { accountsState } from './index';
|
||||
import { currentNetworkStore, currentTransactionVersion } from '@store/networks';
|
||||
import { currentNetworkState, networkTransactionVersionState } from '@store/networks';
|
||||
|
||||
async function fetchNamesByAddress(networkUrl: string, address: string): Promise<string[]> {
|
||||
const res = await fetcher(networkUrl + `/v1/addresses/stacks/${address}`);
|
||||
@@ -42,8 +42,8 @@ export const accountNameState = selector<AccountNameState>({
|
||||
key: 'names',
|
||||
get: async ({ get }) => {
|
||||
const accounts = get(accountsState);
|
||||
const network = get(currentNetworkStore);
|
||||
const transactionVersion = get(currentTransactionVersion);
|
||||
const network = get(currentNetworkState);
|
||||
const transactionVersion = get(networkTransactionVersionState);
|
||||
|
||||
if (!network || !accounts) return null;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { atomFamily, selector, waitForAll } from 'recoil';
|
||||
import { localStorageEffect } from '@store/common/utils';
|
||||
import { currentNetworkStore } from '@store/networks';
|
||||
import { accountDataStore, currentAccountStxAddressStore } from '@store/accounts/index';
|
||||
import { apiRevalidation } from '@store/common/api';
|
||||
import { currentNetworkState } from '@store/networks';
|
||||
import { accountDataState, currentAccountStxAddressState } from '@store/accounts/index';
|
||||
import { apiRevalidation } from '@store/common/api-helpers';
|
||||
import { accountInfoStore } from '@store/accounts/index';
|
||||
|
||||
export const latestNoncesState = atomFamily<
|
||||
@@ -20,8 +20,8 @@ export const latestNoncesState = atomFamily<
|
||||
export const latestNonceState = selector({
|
||||
key: 'wallet.latest-nonce',
|
||||
get: ({ get }) => {
|
||||
const network = get(currentNetworkStore);
|
||||
const address = get(currentAccountStxAddressStore);
|
||||
const network = get(currentNetworkState);
|
||||
const address = get(currentAccountStxAddressState);
|
||||
return get(latestNoncesState([network.url, address || '']));
|
||||
},
|
||||
});
|
||||
@@ -35,8 +35,8 @@ export const correctNonceState = selector({
|
||||
waitForAll({
|
||||
account: accountInfoStore,
|
||||
lastConfirmedTx: latestNonceState,
|
||||
accountData: accountDataStore,
|
||||
address: currentAccountStxAddressStore,
|
||||
accountData: accountDataState,
|
||||
address: currentAccountStxAddressState,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { selector, selectorFamily } from 'recoil';
|
||||
import { currentNetworkStore } from '@store/networks';
|
||||
import { accountBalancesStore } from '@store/accounts';
|
||||
import { currentNetworkState } from '@store/networks';
|
||||
import { accountBalancesState } from '@store/accounts';
|
||||
import {
|
||||
fetchFungibleTokenMetaData,
|
||||
fetchSip10Status,
|
||||
@@ -24,7 +24,7 @@ export const assetSip10ImplementationState = selectorFamily<
|
||||
get:
|
||||
({ contractName, contractAddress }) =>
|
||||
async ({ get }) => {
|
||||
const network = get(currentNetworkStore);
|
||||
const network = get(currentNetworkState);
|
||||
const chain = getNetworkChain(network);
|
||||
try {
|
||||
return fetchSip10Status({
|
||||
@@ -55,7 +55,7 @@ export const assetMetaDataState = selectorFamily<
|
||||
})
|
||||
);
|
||||
if (isImplemented || isImplemented === null) {
|
||||
const network = get(currentNetworkStore);
|
||||
const network = get(currentNetworkState);
|
||||
const localData = getLocalData(network.url, contractAddress, contractName);
|
||||
if (localData) {
|
||||
return {
|
||||
@@ -83,7 +83,7 @@ export const assetMetaDataState = selectorFamily<
|
||||
export const assetsState = selector<AssetWithMeta[] | undefined>({
|
||||
key: 'assets',
|
||||
get: async ({ get }) => {
|
||||
const balance = get(accountBalancesStore);
|
||||
const balance = get(accountBalancesState);
|
||||
if (!balance) return;
|
||||
const assets = transformAssets(balance);
|
||||
const _assets: AssetWithMeta[] = (await Promise.all(
|
||||
@@ -128,7 +128,7 @@ export const nonFungibleTokensState = selector({
|
||||
export const stxTokenState = selector({
|
||||
key: 'assets.stx',
|
||||
get: ({ get }) => {
|
||||
const balances = get(accountBalancesStore);
|
||||
const balances = get(accountBalancesState);
|
||||
if (!balances || balances.stx.balance === '0') return;
|
||||
return {
|
||||
type: 'stx',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Configuration, SmartContractsApi } from '@stacks/blockchain-api-client';
|
||||
import { ChainID, cvToString, hexToCV } from '@stacks/transactions';
|
||||
import { SIP_010 } from '@common/constants';
|
||||
import { Asset, FungibleTokenOptions } from '@store/assets/types';
|
||||
import { Network } from '@store/networks';
|
||||
import { AddressBalanceResponse } from '@blockstack/stacks-blockchain-api-types';
|
||||
import type { Asset, FungibleTokenOptions } from '@store/assets/types';
|
||||
import type { Network } from '@common/constants';
|
||||
import type { AddressBalanceResponse } from '@blockstack/stacks-blockchain-api-types';
|
||||
import { getAssetStringParts, truncateMiddle } from '@stacks/ui-utils';
|
||||
|
||||
export async function callReadOnlyFunction({
|
||||
|
||||
50
src/store/common/api-clients.ts
Normal file
50
src/store/common/api-clients.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { selector } from 'recoil';
|
||||
import { currentNetworkState } from '@store/networks';
|
||||
import {
|
||||
Configuration,
|
||||
AccountsApi,
|
||||
SmartContractsApi,
|
||||
InfoApi,
|
||||
BlocksApi,
|
||||
} from '@stacks/blockchain-api-client';
|
||||
import { fetcher } from '@common/wrapped-fetch';
|
||||
|
||||
export const apiClientConfiguration = selector({
|
||||
key: 'clients.config',
|
||||
get: ({ get }) => {
|
||||
const network = get(currentNetworkState);
|
||||
return new Configuration({ basePath: network.url, fetchApi: fetcher });
|
||||
},
|
||||
});
|
||||
|
||||
export const smartContractClientState = selector({
|
||||
key: 'clients.smart-contract',
|
||||
get: ({ get }) => {
|
||||
const config = get(apiClientConfiguration);
|
||||
return new SmartContractsApi(config);
|
||||
},
|
||||
});
|
||||
|
||||
export const accountsApiClientState = selector({
|
||||
key: 'clients.accounts',
|
||||
get: ({ get }) => {
|
||||
const config = get(apiClientConfiguration);
|
||||
return new AccountsApi(config);
|
||||
},
|
||||
});
|
||||
|
||||
export const infoApiClientState = selector({
|
||||
key: 'clients.info',
|
||||
get: ({ get }) => {
|
||||
const config = get(apiClientConfiguration);
|
||||
return new InfoApi(config);
|
||||
},
|
||||
});
|
||||
|
||||
export const blocksApiClientState = selector({
|
||||
key: 'clients.blocks',
|
||||
get: ({ get }) => {
|
||||
const config = get(apiClientConfiguration);
|
||||
return new BlocksApi(config);
|
||||
},
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
export const DEFAULT_POLLING_INTERVAL = 5000;
|
||||
export const DEFAULT_POLLING_INTERVAL = 30000;
|
||||
|
||||
@@ -62,3 +62,10 @@ export const localStorageEffect =
|
||||
});
|
||||
}
|
||||
};
|
||||
export function textToBytes(content: string) {
|
||||
return new TextEncoder().encode(content);
|
||||
}
|
||||
|
||||
export function bytesToText(buffer: Uint8Array) {
|
||||
return new TextDecoder().decode(buffer);
|
||||
}
|
||||
|
||||
@@ -1,89 +1,64 @@
|
||||
import { atom, selector } from 'recoil';
|
||||
import { atom, selector, waitForAll } from 'recoil';
|
||||
import { localStorageEffect } from './common/utils';
|
||||
import RPCClient from '@stacks/rpc-client';
|
||||
import { ChainID, TransactionVersion } from '@stacks/transactions';
|
||||
import { StacksNetwork, StacksTestnet, StacksMainnet } from '@stacks/network';
|
||||
import { BlockListResponse, CoreNodeInfoResponse } from '@blockstack/stacks-blockchain-api-types';
|
||||
import { fetchFromSidecar } from '@common/api/fetch';
|
||||
import { fetcher } from '@common/wrapped-fetch';
|
||||
import { apiRevalidation } from '@store/common/api';
|
||||
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';
|
||||
|
||||
export interface Network {
|
||||
url: string;
|
||||
name: string;
|
||||
chainId: ChainID;
|
||||
}
|
||||
|
||||
interface Networks {
|
||||
[key: string]: Network;
|
||||
}
|
||||
|
||||
export const defaultNetworks: Networks = {
|
||||
mainnet: {
|
||||
url: 'https://stacks-node-api.mainnet.stacks.co',
|
||||
name: 'Mainnet',
|
||||
chainId: ChainID.Mainnet,
|
||||
},
|
||||
testnet: {
|
||||
url: 'https://stacks-node-api.testnet.stacks.co',
|
||||
name: 'Testnet',
|
||||
chainId: ChainID.Testnet,
|
||||
},
|
||||
regtest: {
|
||||
url: 'https://stacks-node-api.regtest.stacks.co',
|
||||
name: 'Regtest',
|
||||
chainId: ChainID.Testnet,
|
||||
},
|
||||
localnet: {
|
||||
url: 'http://localhost:3999',
|
||||
name: 'Localnet',
|
||||
chainId: ChainID.Testnet,
|
||||
},
|
||||
};
|
||||
|
||||
export const currentNetworkKeyStore = atom({
|
||||
key: 'networks.current-key-v2',
|
||||
default: 'mainnet',
|
||||
effects_UNSTABLE: [localStorageEffect()],
|
||||
});
|
||||
|
||||
export const networksStore = atom<Networks>({
|
||||
key: 'networks.networks-v3',
|
||||
export const networksState = atom<Networks>({
|
||||
key: 'networks',
|
||||
default: defaultNetworks,
|
||||
effects_UNSTABLE: [localStorageEffect()],
|
||||
});
|
||||
|
||||
export const currentNetworkStore = selector({
|
||||
export const currentNetworkKeyState = atom({
|
||||
key: 'networks.current-key',
|
||||
default: selector({
|
||||
key: 'networks.current-key.default',
|
||||
get: ({ get }) => {
|
||||
const { networks, txNetwork } = get(
|
||||
waitForAll({
|
||||
networks: networksState,
|
||||
txNetwork: transactionRequestNetwork,
|
||||
})
|
||||
);
|
||||
if (txNetwork && networks && Object.keys(networks).length > 0) {
|
||||
const newKey = findMatchingNetworkKey(txNetwork, networks);
|
||||
if (newKey) return newKey;
|
||||
}
|
||||
return 'mainnet';
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const currentNetworkState = selector({
|
||||
key: 'networks.current-network',
|
||||
get: ({ get }) => {
|
||||
const networks = get(networksStore);
|
||||
const key = get(currentNetworkKeyStore);
|
||||
const { networks, key } = get(
|
||||
waitForAll({
|
||||
networks: networksState,
|
||||
key: currentNetworkKeyState,
|
||||
})
|
||||
);
|
||||
return networks[key];
|
||||
},
|
||||
});
|
||||
|
||||
export const currentTransactionVersion = selector({
|
||||
export const networkTransactionVersionState = selector({
|
||||
key: 'networks.transaction-version',
|
||||
get: ({ get }) => {
|
||||
const network = get(currentNetworkStore);
|
||||
return network.chainId === ChainID.Mainnet
|
||||
get: ({ get }) =>
|
||||
get(currentNetworkState).chainId === ChainID.Mainnet
|
||||
? TransactionVersion.Mainnet
|
||||
: TransactionVersion.Testnet;
|
||||
},
|
||||
});
|
||||
|
||||
export const rpcClientStore = selector({
|
||||
key: 'networks.rpc-client',
|
||||
get: ({ get }) => {
|
||||
const network = get(currentNetworkStore);
|
||||
return new RPCClient(network.url);
|
||||
},
|
||||
: TransactionVersion.Testnet,
|
||||
});
|
||||
|
||||
export const stacksNetworkStore = selector<StacksNetwork>({
|
||||
key: 'networks.stacks-network',
|
||||
get: ({ get }) => {
|
||||
const network = get(currentNetworkStore);
|
||||
const network = get(currentNetworkState);
|
||||
const stacksNetwork =
|
||||
network.chainId === ChainID.Testnet ? new StacksTestnet() : new StacksMainnet();
|
||||
stacksNetwork.coreApiUrl = network.url;
|
||||
@@ -91,29 +66,20 @@ export const stacksNetworkStore = selector<StacksNetwork>({
|
||||
},
|
||||
});
|
||||
|
||||
export const latestBlockHeightStore = selector({
|
||||
export const latestBlockHeightState = selector({
|
||||
key: 'api.latest-block-height',
|
||||
get: async ({ get }) => {
|
||||
const { url } = get(currentNetworkStore);
|
||||
const blocksResponse: BlockListResponse = await fetchFromSidecar(url)('/block');
|
||||
const [block] = blocksResponse.results;
|
||||
return block.height;
|
||||
const client = get(blocksApiClientState);
|
||||
const data = await client.getBlockList({});
|
||||
return data?.results?.[0]?.height;
|
||||
},
|
||||
});
|
||||
|
||||
export const chainInfoStore = selector({
|
||||
key: 'api.chain-info',
|
||||
export const networkInfoState = selector({
|
||||
key: 'api.network-info',
|
||||
get: async ({ get }) => {
|
||||
get(apiRevalidation);
|
||||
const { url } = get(currentNetworkStore);
|
||||
const infoUrl = `${url}/v2/info`;
|
||||
try {
|
||||
const res = await fetcher(infoUrl);
|
||||
if (!res.ok) throw `Unable to fetch chain data from ${infoUrl}`;
|
||||
const info: CoreNodeInfoResponse = await res.json();
|
||||
return info;
|
||||
} catch (error) {
|
||||
throw `Unable to fetch chain data from ${infoUrl}`;
|
||||
}
|
||||
const client = get(infoApiClientState);
|
||||
return client.getCoreApiInfo();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,217 +9,29 @@ import {
|
||||
standardPrincipalCVFromAddress,
|
||||
uintCV,
|
||||
} from '@stacks/transactions';
|
||||
import { TransactionPayload } from '@stacks/connect';
|
||||
import { atom, selectorFamily } from 'recoil';
|
||||
import {
|
||||
ContractCallPayload,
|
||||
ContractDeployPayload,
|
||||
STXTransferPayload,
|
||||
TransactionPayload,
|
||||
TransactionTypes,
|
||||
} from '@stacks/connect';
|
||||
import { decodeToken } from 'jsontokens';
|
||||
import { atom, selector, selectorFamily } from 'recoil';
|
||||
import { generateTransaction } from '@common/transaction-utils';
|
||||
import {
|
||||
accountBalancesStore,
|
||||
currentAccountStore,
|
||||
currentAccountStxAddressStore,
|
||||
accountBalancesState,
|
||||
currentAccountState,
|
||||
currentAccountStxAddressState,
|
||||
} from '@store/accounts';
|
||||
import { currentNetworkStore, rpcClientStore, stacksNetworkStore } from '@store/networks';
|
||||
import { stacksNetworkStore } from '@store/networks';
|
||||
import { selectedAssetStore } from './assets/asset-search';
|
||||
import BN from 'bn.js';
|
||||
import { stxToMicroStx } from '@common/stacks-utils';
|
||||
import { getAssetStringParts } from '@stacks/ui-utils';
|
||||
import { correctNonceState } from '@store/accounts/nonce';
|
||||
|
||||
export type TransactionPayloadWithAttachment = TransactionPayload & {
|
||||
attachment?: string;
|
||||
};
|
||||
|
||||
/** Transaction signing popup store */
|
||||
|
||||
export const showTxDetails = atom<boolean>({
|
||||
key: 'transaction.show-details',
|
||||
default: true,
|
||||
});
|
||||
|
||||
export const requestTokenStore = atom<string | null>({
|
||||
key: 'transaction.request-token',
|
||||
default: null,
|
||||
});
|
||||
|
||||
function getPayload(requestToken: string) {
|
||||
if (!requestToken) return undefined;
|
||||
const token = decodeToken(requestToken);
|
||||
const payload = token.payload as unknown as TransactionPayloadWithAttachment;
|
||||
|
||||
if (payload.txType === TransactionTypes.ContractCall)
|
||||
return payload as ContractCallPayload & {
|
||||
attachment?: string;
|
||||
};
|
||||
if (payload.txType === TransactionTypes.ContractDeploy)
|
||||
return payload as ContractDeployPayload & {
|
||||
attachment?: string;
|
||||
};
|
||||
if (payload.txType === TransactionTypes.STXTransfer)
|
||||
return payload as STXTransferPayload & {
|
||||
attachment?: string;
|
||||
};
|
||||
return payload;
|
||||
}
|
||||
|
||||
export const transactionPayloadStore = selector({
|
||||
key: 'transaction.payload',
|
||||
get: ({ get }) => {
|
||||
const requestToken = get(requestTokenStore);
|
||||
if (requestToken === null) return;
|
||||
return getPayload(requestToken);
|
||||
},
|
||||
});
|
||||
|
||||
export const pendingTransactionStore = selector({
|
||||
key: 'transaction.pending-transaction',
|
||||
get: ({ get }) => {
|
||||
const requestToken = get(requestTokenStore);
|
||||
if (!requestToken) return undefined;
|
||||
const tx = getPayload(requestToken);
|
||||
if (!tx) return undefined;
|
||||
const network = get(stacksNetworkStore);
|
||||
const postConditions = get(postConditionsStore);
|
||||
tx.postConditions = postConditions;
|
||||
tx.network = network;
|
||||
return tx;
|
||||
},
|
||||
});
|
||||
|
||||
export const contractSourceStore = selector({
|
||||
key: 'transaction.contract-source',
|
||||
get: async ({ get }) => {
|
||||
const tx = get(pendingTransactionStore);
|
||||
const rpcClient = get(rpcClientStore);
|
||||
|
||||
if (!tx) return '';
|
||||
|
||||
if (tx.txType === TransactionTypes.ContractCall)
|
||||
return rpcClient.fetchContractSource({
|
||||
contractName: tx.contractName,
|
||||
contractAddress: tx.contractAddress,
|
||||
});
|
||||
|
||||
if (tx.txType === TransactionTypes.ContractDeploy) return tx.codeBody;
|
||||
|
||||
return '';
|
||||
},
|
||||
});
|
||||
|
||||
export const contractInterfaceStore = selector({
|
||||
key: 'transaction.contract-interface',
|
||||
get: async ({ get }) => {
|
||||
const payload = get(transactionPayloadStore);
|
||||
const tx = get(pendingTransactionStore);
|
||||
const network = get(currentNetworkStore);
|
||||
const rpcClient = get(rpcClientStore);
|
||||
if (!tx) {
|
||||
return undefined;
|
||||
}
|
||||
if (payload?.network && payload.network.chainId !== network.chainId) return undefined;
|
||||
if (tx.txType === TransactionTypes.ContractCall) {
|
||||
try {
|
||||
// TODO: replace with smartContract client from api client
|
||||
return rpcClient.fetchContractInterface({
|
||||
contractName: tx.contractName,
|
||||
contractAddress: tx.contractAddress,
|
||||
});
|
||||
} catch (error) {
|
||||
// TODO: fix race condition
|
||||
// we don't need to throw here
|
||||
// the network switch can happen before or after this,
|
||||
// and the interface can sometimes fail before landing on the correct network
|
||||
// @see https://github.com/blockstack/stacks-wallet-web/issues/1174
|
||||
console.log(
|
||||
`Unable to fetch interface for contract ${tx.contractAddress}.${tx.contractName}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
export const pendingTransactionFunctionSelector = selector({
|
||||
key: 'transactions.pending-transaction-function',
|
||||
get: ({ get }) => {
|
||||
const pendingTransaction = get(pendingTransactionStore);
|
||||
const contractInterface = get(contractInterfaceStore);
|
||||
if (
|
||||
!pendingTransaction ||
|
||||
pendingTransaction.txType !== 'contract_call' ||
|
||||
!contractInterface
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const selectedFunction = contractInterface.functions.find(func => {
|
||||
return func.name === pendingTransaction.functionName;
|
||||
});
|
||||
if (!selectedFunction) {
|
||||
throw new Error(
|
||||
`Attempting to call a function (\`${pendingTransaction.functionName}\`) that ` +
|
||||
`does not exist on contract ${pendingTransaction.contractAddress}.${pendingTransaction.contractName}`
|
||||
);
|
||||
}
|
||||
return selectedFunction;
|
||||
},
|
||||
});
|
||||
|
||||
export const signedTransactionStore = selector({
|
||||
key: 'transaction.signedTransaction',
|
||||
get: async ({ get }) => {
|
||||
const account = get(currentAccountStore);
|
||||
if (!account) {
|
||||
console.error('Unable to sign transaction when logged out.');
|
||||
return undefined;
|
||||
}
|
||||
const pendingTransaction = get(pendingTransactionStore);
|
||||
if (!pendingTransaction) {
|
||||
console.error('Unable to get signed transaction - no pending transaction found.');
|
||||
return undefined;
|
||||
}
|
||||
const nonce = get(correctNonceState);
|
||||
const tx = await generateTransaction({
|
||||
senderKey: account.stxPrivateKey,
|
||||
nonce,
|
||||
txData: pendingTransaction,
|
||||
});
|
||||
return tx;
|
||||
},
|
||||
dangerouslyAllowMutability: true,
|
||||
});
|
||||
|
||||
export const postConditionsStore = atom<PostCondition[]>({
|
||||
key: 'transaction.postConditions',
|
||||
default: [],
|
||||
});
|
||||
|
||||
export const postConditionsHasSetStore = atom<boolean>({
|
||||
key: 'transaction.postConditions.hasSet',
|
||||
export const isUnauthorizedTransactionState = atom<boolean>({
|
||||
key: 'transaction.is-unauthorized-tx',
|
||||
default: false,
|
||||
});
|
||||
export const currentPostConditionIndexStore = atom<number | undefined>({
|
||||
key: 'transaction.currentPostConditionIndex',
|
||||
default: undefined,
|
||||
});
|
||||
|
||||
export const currentPostConditionStore = selector<PostCondition | undefined>({
|
||||
key: 'transaction.currentPostCondition',
|
||||
get: ({ get }) => {
|
||||
const index = get(currentPostConditionIndexStore);
|
||||
if (index === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const postConditions = get(postConditionsStore);
|
||||
return postConditions[index];
|
||||
},
|
||||
});
|
||||
|
||||
export const transactionBroadcastErrorStore = atom<string | null>({
|
||||
export const transactionBroadcastErrorState = atom<string | null>({
|
||||
key: 'transaction.broadcast-error',
|
||||
default: null,
|
||||
});
|
||||
@@ -232,11 +44,11 @@ export const internalTransactionStore = selectorFamily({
|
||||
async ({ get }) => {
|
||||
try {
|
||||
const asset = get(selectedAssetStore);
|
||||
const currentAccount = get(currentAccountStore);
|
||||
const currentAccountStxAddress = get(currentAccountStxAddressStore);
|
||||
const currentAccount = get(currentAccountState);
|
||||
const currentAccountStxAddress = get(currentAccountStxAddressState);
|
||||
if (!asset || !currentAccount || !currentAccountStxAddress) return null;
|
||||
const network = get(stacksNetworkStore);
|
||||
const balances = get(accountBalancesStore);
|
||||
const balances = get(accountBalancesState);
|
||||
if (asset.type === 'stx') {
|
||||
const mStx = stxToMicroStx(amount);
|
||||
try {
|
||||
@@ -294,8 +106,3 @@ export const internalTransactionStore = selectorFamily({
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const isUnauthorizedTransactionStore = atom<boolean>({
|
||||
key: 'transaction.is-unauthorized-tx',
|
||||
default: false,
|
||||
});
|
||||
|
||||
82
src/store/transactions/contract-call.ts
Normal file
82
src/store/transactions/contract-call.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { selector, waitForAll } from 'recoil';
|
||||
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';
|
||||
|
||||
type ContractInterfaceResponseWithFunctions = Omit<ContractInterfaceResponse, 'functions'> & {
|
||||
functions: ContractInterfaceFunction[];
|
||||
};
|
||||
export const transactionContractInterfaceState = selector<
|
||||
undefined | ContractInterfaceResponseWithFunctions
|
||||
>({
|
||||
key: 'transactions.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({
|
||||
contractName: payload.contractName,
|
||||
contractAddress: payload.contractAddress,
|
||||
});
|
||||
if (!data) return undefined;
|
||||
return data as ContractInterfaceResponseWithFunctions;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const transactionContractSourceState = selector({
|
||||
key: 'transactions.contract-source',
|
||||
get: async ({ get }) => {
|
||||
const { payload, client } = get(
|
||||
waitForAll({
|
||||
payload: requestTokenPayloadState,
|
||||
client: smartContractClientState,
|
||||
})
|
||||
);
|
||||
|
||||
if (payload?.txType !== TransactionTypes.ContractCall) return;
|
||||
|
||||
try {
|
||||
return client.getContractSource({
|
||||
contractName: payload.contractName,
|
||||
contractAddress: payload.contractAddress,
|
||||
});
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
});
|
||||
export const transactionFunctionsState = selector({
|
||||
key: 'transactions.pending-transaction-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;
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import { selector } from 'recoil';
|
||||
import { selector, waitForAll } from 'recoil';
|
||||
import { selectedAssetStore } from '@store/assets/asset-search';
|
||||
import {
|
||||
accountBalancesStore,
|
||||
currentAccountStore,
|
||||
currentAccountStxAddressStore,
|
||||
accountBalancesState,
|
||||
currentAccountState,
|
||||
currentAccountStxAddressState,
|
||||
} from '@store/accounts';
|
||||
import { stacksNetworkStore } from '@store/networks';
|
||||
import { correctNonceState } from '@store/accounts/nonce';
|
||||
@@ -11,12 +11,17 @@ import { correctNonceState } from '@store/accounts/nonce';
|
||||
export const makeFungibleTokenTransferState = selector({
|
||||
key: 'transaction.internal-transaction-asset',
|
||||
get: ({ get }) => {
|
||||
const asset = get(selectedAssetStore);
|
||||
const currentAccount = get(currentAccountStore);
|
||||
const network = get(stacksNetworkStore);
|
||||
const balances = get(accountBalancesStore);
|
||||
const stxAddress = get(currentAccountStxAddressStore);
|
||||
const nonce = get(correctNonceState);
|
||||
const { asset, currentAccount, network, balances, stxAddress, nonce } = get(
|
||||
waitForAll({
|
||||
asset: selectedAssetStore,
|
||||
currentAccount: currentAccountState,
|
||||
network: stacksNetworkStore,
|
||||
balances: accountBalancesState,
|
||||
stxAddress: currentAccountStxAddressState,
|
||||
nonce: correctNonceState,
|
||||
})
|
||||
);
|
||||
|
||||
if (asset && currentAccount && stxAddress) {
|
||||
const { contractName, contractAddress, name: assetName } = asset;
|
||||
return {
|
||||
@@ -30,10 +35,7 @@ export const makeFungibleTokenTransferState = selector({
|
||||
contractAddress,
|
||||
contractName,
|
||||
};
|
||||
} else {
|
||||
console.error('[makeFungibleTokenTransferState]: malformed state');
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
70
src/store/transactions/index.ts
Normal file
70
src/store/transactions/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { selector, waitForAll } from 'recoil';
|
||||
import { ChainID } from '@stacks/transactions';
|
||||
import { StacksMainnet, StacksTestnet } from '@stacks/network';
|
||||
|
||||
import { currentNetworkState } from '@store/networks';
|
||||
import { correctNonceState } from '@store/accounts/nonce';
|
||||
import { currentAccountState, currentAccountStxAddressState } from '@store/accounts';
|
||||
import { requestTokenPayloadState } from '@store/transactions/requests';
|
||||
|
||||
import { getPostCondition, handlePostConditions } from '@common/post-condition-utils';
|
||||
import { generateTransaction } from '@common/transaction-utils';
|
||||
|
||||
export const postConditionsState = selector({
|
||||
key: 'transactions.post-conditions',
|
||||
get: ({ get }) => {
|
||||
const { payload, address } = get(
|
||||
waitForAll({
|
||||
payload: requestTokenPayloadState,
|
||||
address: currentAccountStxAddressState,
|
||||
})
|
||||
);
|
||||
|
||||
if (!payload || !address) return;
|
||||
|
||||
if (payload.postConditions) {
|
||||
if (payload.stxAddress)
|
||||
return handlePostConditions(payload.postConditions, payload.stxAddress, address);
|
||||
|
||||
return payload.postConditions.map(getPostCondition);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
export const pendingTransactionState = selector({
|
||||
key: 'transactions.pending',
|
||||
get: ({ get }) => {
|
||||
const { payload, postConditions, _network } = get(
|
||||
waitForAll({
|
||||
payload: requestTokenPayloadState,
|
||||
postConditions: postConditionsState,
|
||||
_network: currentNetworkState,
|
||||
})
|
||||
);
|
||||
const network =
|
||||
_network.chainId === ChainID.Mainnet ? new StacksMainnet() : new StacksTestnet();
|
||||
network.coreApiUrl = _network.url;
|
||||
if (!payload) return;
|
||||
return { ...payload, postConditions, network };
|
||||
},
|
||||
});
|
||||
|
||||
export const signedTransactionState = selector({
|
||||
key: 'transactions.signed',
|
||||
get: async ({ get }) => {
|
||||
const { account, pendingTransaction, nonce } = get(
|
||||
waitForAll({
|
||||
account: currentAccountState,
|
||||
pendingTransaction: pendingTransactionState,
|
||||
nonce: correctNonceState,
|
||||
})
|
||||
);
|
||||
if (!account || !pendingTransaction) return;
|
||||
return generateTransaction({
|
||||
senderKey: account.stxPrivateKey,
|
||||
nonce,
|
||||
txData: pendingTransaction,
|
||||
});
|
||||
},
|
||||
});
|
||||
39
src/store/transactions/requests.ts
Normal file
39
src/store/transactions/requests.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { atom, selector } from 'recoil';
|
||||
import { getPayloadFromToken } from '@store/transactions/utils';
|
||||
|
||||
export const requestTokenState = atom<string | null>({
|
||||
key: 'transactions.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 = selector({
|
||||
key: 'transactions.request-token-payload',
|
||||
get: ({ get }) => {
|
||||
const token = get(requestTokenState);
|
||||
return token ? getPayloadFromToken(token) : null;
|
||||
},
|
||||
});
|
||||
|
||||
export const transactionRequestStxAddressState = selector({
|
||||
key: 'transactions.request.address',
|
||||
get: ({ get }) => get(requestTokenPayloadState)?.stxAddress,
|
||||
});
|
||||
|
||||
export const transactionRequestNetwork = selector({
|
||||
key: 'transactions.request.network',
|
||||
get: ({ get }) => get(requestTokenPayloadState)?.network,
|
||||
});
|
||||
28
src/store/transactions/utils.ts
Normal file
28
src/store/transactions/utils.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { decodeToken } from 'jsontokens';
|
||||
import {
|
||||
ContractCallPayload,
|
||||
ContractDeployPayload,
|
||||
STXTransferPayload,
|
||||
TransactionTypes,
|
||||
} from '@stacks/connect';
|
||||
import { TransactionPayloadWithAttachment } from '@store/transaction';
|
||||
|
||||
export function getPayloadFromToken(requestToken: string) {
|
||||
if (!requestToken) return undefined;
|
||||
const token = decodeToken(requestToken);
|
||||
const payload = token.payload as unknown as TransactionPayloadWithAttachment;
|
||||
|
||||
if (payload.txType === TransactionTypes.ContractCall)
|
||||
return payload as ContractCallPayload & {
|
||||
attachment?: string;
|
||||
};
|
||||
if (payload.txType === TransactionTypes.ContractDeploy)
|
||||
return payload as ContractDeployPayload & {
|
||||
attachment?: string;
|
||||
};
|
||||
if (payload.txType === TransactionTypes.STXTransfer)
|
||||
return payload as STXTransferPayload & {
|
||||
attachment?: string;
|
||||
};
|
||||
return payload;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@stacks/wallet-sdk';
|
||||
import { gaiaUrl } from '@common/constants';
|
||||
|
||||
export const secretKeyState = atom<string | undefined>({
|
||||
export const secretKeyState = atom<Uint8Array | undefined>({
|
||||
key: 'wallet.secret-key',
|
||||
default: undefined,
|
||||
});
|
||||
@@ -39,9 +39,7 @@ export const walletConfigStore = selector<WalletConfig | null>({
|
||||
key: 'wallet.wallet-config',
|
||||
get: async ({ get }) => {
|
||||
const wallet = get(walletState);
|
||||
if (!wallet) {
|
||||
return null;
|
||||
}
|
||||
if (!wallet) return null;
|
||||
const gaiaHubConfig = await createWalletGaiaConfig({ wallet, gaiaHubUrl: gaiaUrl });
|
||||
return fetchWalletConfig({ wallet, gaiaHubConfig });
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user