diff --git a/extension/source/Confirm/Confirm.tsx b/extension/source/Confirm/Confirm.tsx index a138c109..81e038ce 100644 --- a/extension/source/Confirm/Confirm.tsx +++ b/extension/source/Confirm/Confirm.tsx @@ -8,7 +8,7 @@ import Button from '../components/Button'; import CompactQuillHeading from '../components/CompactQuillHeading'; import { DEFAULT_CHAIN_ID_HEX } from '../env'; import { useInputDecode } from '../hooks/useInputDecode'; -import formatCompactAddress from '../Popup/helpers/formatCompactAddress'; +import formatCompactAddress from '../helpers/formatCompactAddress'; const Confirm: FunctionComponent = () => { const [id, setId] = useState(); diff --git a/extension/source/Home/Wallet/Wallets/SendDetail/AmountSelector.tsx b/extension/source/Home/Wallet/Wallets/SendDetail/AmountSelector.tsx new file mode 100644 index 00000000..12a641dd --- /dev/null +++ b/extension/source/Home/Wallet/Wallets/SendDetail/AmountSelector.tsx @@ -0,0 +1,57 @@ +import { ethers } from 'ethers'; +import { FunctionComponent, useMemo } from 'react'; +import Display from '../../../../cells/components/Display'; + +import TextBox from '../../../../cells/components/TextBox'; +import { IReadableCell } from '../../../../cells/ICell'; +import MemoryCell from '../../../../cells/MemoryCell'; +import TransformCell from '../../../../cells/TransformCell'; +import Button from '../../../../components/Button'; + +const AmountSelector: FunctionComponent<{ + selectedAsset: IReadableCell; + onSend: (amountWei: string) => void; +}> = ({ selectedAsset, onSend }) => { + const amount = useMemo(() => new MemoryCell(''), []); + + const amountValid = useMemo( + () => + new TransformCell( + amount, + ($amount) => $amount || '0', + ($amount, $newAmount) => + Number.isFinite(Number($newAmount)) ? $newAmount : $amount, + ), + [amount], + ); + + return ( +
+
Select Amount
+
+ +
+ +
+
+
+ +
+
+ ); +}; + +export default AmountSelector; diff --git a/extension/source/Home/Wallet/Wallets/SendDetail/AssetSelector.tsx b/extension/source/Home/Wallet/Wallets/SendDetail/AssetSelector.tsx new file mode 100644 index 00000000..42fa32b9 --- /dev/null +++ b/extension/source/Home/Wallet/Wallets/SendDetail/AssetSelector.tsx @@ -0,0 +1,41 @@ +import { ArrowRight } from 'phosphor-react'; +import { FunctionComponent } from 'react'; +import ICell from '../../../../cells/ICell'; + +import useCell from '../../../../cells/useCell'; +import Loading from '../../../../components/Loading'; +import onAction from '../../../../helpers/onAction'; +import { useQuill } from '../../../QuillContext'; +import Balance from '../Balance'; + +const AssetSelector: FunctionComponent<{ + selectedAsset: ICell; +}> = ({ selectedAsset }) => { + const quill = useQuill(); + const selectedAddress = useCell(quill.cells.selectedAddress); + + return ( +
+
Select Asset
+
+
selectedAsset.write('ETH'))} + > +
{/* TODO: icon */}Ether
+
+ {selectedAddress && } + {!selectedAddress && } +
+ +
+
+
+ ); +}; + +export default AssetSelector; diff --git a/extension/source/Home/Wallet/Wallets/SendDetail/BigSendButton.tsx b/extension/source/Home/Wallet/Wallets/SendDetail/BigSendButton.tsx new file mode 100644 index 00000000..57bdc47d --- /dev/null +++ b/extension/source/Home/Wallet/Wallets/SendDetail/BigSendButton.tsx @@ -0,0 +1,74 @@ +import { CaretLeft, X } from 'phosphor-react'; +import { FunctionComponent } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import useCell from '../../../../cells/useCell'; +import formatCompactAddress from '../../../../helpers/formatCompactAddress'; +import onAction from '../../../../helpers/onAction'; +import type { SendDetailCells } from './SendDetail'; + +const roundFieldClasses = [ + 'text-[8pt] bg-blue-100 bg-opacity-40 leading-normal', + 'rounded-full px-2 py-1 flex place-items-center gap-2', +].join(' '); + +const BigSendButton: FunctionComponent<{ + cells: SendDetailCells; +}> = ({ cells }) => { + const navigate = useNavigate(); + + const selectedAsset = useCell(cells.selectedAsset); + const recipient = useCell(cells.recipient); + const amountWei = useCell(cells.amountWei); + + const visibility = amountWei === undefined ? '' : 'invisible'; + + return ( +
+
+
{ + if (recipient !== undefined) { + await cells.recipient.write(undefined); + } else if (selectedAsset !== undefined) { + await cells.selectedAsset.write(undefined); + } else { + navigate('/wallets'); + } + })} + > + +
Back
+
+
+
Send
+ {selectedAsset !== undefined && ( +
+
{selectedAsset}
+
+ )} + {recipient !== undefined && ( + <> +
to
+
+
+ {formatCompactAddress(recipient)} +
+
+ + )} +
+
navigate('/wallets'))} + > +
Cancel
+ +
+
+
+ ); +}; + +export default BigSendButton; diff --git a/extension/source/Home/Wallet/Wallets/SendDetail/RecipientSelector.tsx b/extension/source/Home/Wallet/Wallets/SendDetail/RecipientSelector.tsx new file mode 100644 index 00000000..3c02aae8 --- /dev/null +++ b/extension/source/Home/Wallet/Wallets/SendDetail/RecipientSelector.tsx @@ -0,0 +1,88 @@ +import { ethers } from 'ethers'; +import { ArrowRight } from 'phosphor-react'; +import { FunctionComponent, useMemo } from 'react'; +import Blockies from 'react-blockies'; +import TextBox from '../../../../cells/components/TextBox'; + +import ICell from '../../../../cells/ICell'; +import MemoryCell from '../../../../cells/MemoryCell'; +import useCell from '../../../../cells/useCell'; +import Loading from '../../../../components/Loading'; +import onAction from '../../../../helpers/onAction'; +import { useQuill } from '../../../QuillContext'; +import Balance from '../Balance'; + +const RecipientSelector: FunctionComponent<{ + recipient: ICell; +}> = ({ recipient }) => { + const quill = useQuill(); + const selectedAddress = useCell(quill.cells.selectedAddress); + const keyring = useCell(quill.cells.keyring); + + const searchText = useMemo(() => new MemoryCell(''), []); + const searchTextValue = useCell(searchText); + + const searchTextLowercase = (searchTextValue ?? '').toLowerCase(); + + const recipients = (() => { + if (searchTextValue && ethers.utils.isAddress(searchTextValue)) { + return [{ address: searchTextValue, name: 'Custom Recipient' }]; + } + + return (keyring?.wallets ?? []) + .map((wallet, i) => ({ + address: wallet.address, + name: `Wallet ${i}`, + })) + .filter((r) => r.address !== selectedAddress) + .filter( + (r) => + r.address.toLowerCase().includes(searchTextLowercase) || + r.name.toLowerCase().includes(searchTextLowercase), + ); + })(); + + return ( +
+
Select Recipient
+
+ +
+ {recipients.length === 0 && 'No recipients found'} +
+ {recipients.map((r) => { + if (r === undefined) { + return
; + } + + return ( +
recipient.write(r.address))} + > + +
{r.name}
+
+ {selectedAddress && } + {!selectedAddress && } +
+ +
+ ); + })} +
+
+ ); +}; + +export default RecipientSelector; diff --git a/extension/source/Home/Wallet/Wallets/SendDetail/SendDetail.tsx b/extension/source/Home/Wallet/Wallets/SendDetail/SendDetail.tsx new file mode 100644 index 00000000..a9295a47 --- /dev/null +++ b/extension/source/Home/Wallet/Wallets/SendDetail/SendDetail.tsx @@ -0,0 +1,136 @@ +import { FunctionComponent, useMemo, useState } from 'react'; + +import forEach from '../../../../cells/forEach'; +import ICell from '../../../../cells/ICell'; +import MemoryCell from '../../../../cells/MemoryCell'; +import assert from '../../../../helpers/assert'; +import AsyncReturnType from '../../../../types/AsyncReturnType'; +import { RpcClient } from '../../../../types/Rpc'; +import { QuillContextValue, useQuill } from '../../../QuillContext'; + +import BigSendButton from './BigSendButton'; +import SendDetailSelectors from './SendDetailSelectors'; +import SendProgress from './SendProgress'; + +type Receipt = Exclude< + AsyncReturnType, + undefined +>; + +export type SendState = + | { step: 'sending'; sendBlock: number } + | { step: 'awaiting-confirmation'; txHash: string; sendBlock: number } + | { + step: 'confirmed'; + sendBlock: number; + receipt: Exclude< + AsyncReturnType, + undefined + >; + } + | { step: 'error'; message: string }; + +export type SendDetailCells = { + selectedAsset: ICell; + recipient: ICell; + amountWei: ICell; +}; + +const SendDetail: FunctionComponent = () => { + const quill = useQuill(); + + const cells: SendDetailCells = useMemo( + () => ({ + selectedAsset: new MemoryCell(undefined), + recipient: new MemoryCell(undefined), + amountWei: new MemoryCell(undefined), + }), + [], + ); + + const [sendState, setSendState] = useState(); + + return ( +
+ + { + cells.amountWei.write(amountWei); + + const from = await quill.cells.selectedAddress.read(); + assert(from !== undefined); + + const recipient = await cells.recipient.read(); + assert(recipient !== undefined); + + await send(quill, setSendState, { + from, + to: recipient, + value: amountWei, + }); + }} + /> + {sendState && } +
+ ); +}; + +async function send( + quill: QuillContextValue, + setSendState: (state: SendState) => void, + tx: { + from: string; + to: string; + value: string; + }, +) { + try { + const sendBlock = await quill.cells.blockNumber.read(); + setSendState({ step: 'sending', sendBlock }); + + let receipt: Receipt | undefined; + + const txHash = await quill.rpc.eth_sendTransaction({ + ...tx, + gas: undefined, + gasPrice: undefined, + data: undefined, + }); + + setSendState({ + step: 'awaiting-confirmation', + txHash, + sendBlock, + }); + + const forEachHandle = forEach( + quill.cells.blockNumber, + async ($blockNumber) => { + if ($blockNumber > sendBlock) { + receipt = await quill.rpc.eth_getTransactionReceipt(txHash); + + if (receipt === undefined) { + return; + } + + forEachHandle.stop(); + } + }, + ); + + await forEachHandle.iterationCompletionPromise; + assert(receipt !== undefined); + + setSendState({ + step: 'confirmed', + sendBlock, + receipt, + }); + } catch (error) { + assert(error instanceof Error); + setSendState({ step: 'error', message: error.message }); + } +} + +export default SendDetail; diff --git a/extension/source/Home/Wallet/Wallets/SendDetail/SendDetailSelectors.tsx b/extension/source/Home/Wallet/Wallets/SendDetail/SendDetailSelectors.tsx new file mode 100644 index 00000000..5a270b94 --- /dev/null +++ b/extension/source/Home/Wallet/Wallets/SendDetail/SendDetailSelectors.tsx @@ -0,0 +1,34 @@ +import { FunctionComponent } from 'react'; +import useCell from '../../../../cells/useCell'; +import AmountSelector from './AmountSelector'; +import AssetSelector from './AssetSelector'; +import RecipientSelector from './RecipientSelector'; + +import type { SendDetailCells } from './SendDetail'; + +const SendDetailSelectors: FunctionComponent<{ + cells: SendDetailCells; + onSend: (amountWei: string) => void; +}> = ({ cells, onSend }) => { + const selectedAsset = useCell(cells.selectedAsset); + const recipient = useCell(cells.recipient); + const amountWei = useCell(cells.amountWei); + + if (selectedAsset === undefined) { + return ; + } + + if (recipient === undefined) { + return ; + } + + if (amountWei === undefined) { + return ( + + ); + } + + return <>; +}; + +export default SendDetailSelectors; diff --git a/extension/source/Home/Wallet/Wallets/SendDetail/SendProgress.tsx b/extension/source/Home/Wallet/Wallets/SendDetail/SendProgress.tsx new file mode 100644 index 00000000..e66c97fe --- /dev/null +++ b/extension/source/Home/Wallet/Wallets/SendDetail/SendProgress.tsx @@ -0,0 +1,43 @@ +import { CaretLeft } from 'phosphor-react'; +import { FunctionComponent } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import useCell from '../../../../cells/useCell'; +import Button from '../../../../components/Button'; +import { useQuill } from '../../../QuillContext'; + +import type { SendState } from './SendDetail'; + +const SendProgress: FunctionComponent<{ state: SendState }> = ({ state }) => { + const quill = useQuill(); + const navigate = useNavigate(); + + const blockNumber = useCell(quill.cells.blockNumber); + + return ( +
+
{JSON.stringify(state, null, 2)}
+ {blockNumber && state.step !== 'confirmed' && 'sendBlock' in state && ( +
Block: Sent + {blockNumber - state.sendBlock}
+ )} + {blockNumber && state.step === 'confirmed' && ( +
Block: Confirmed + {blockNumber - state.receipt.blockNumber}
+ )} +
+ +
+
+ ); +}; + +export default SendProgress; diff --git a/extension/source/Home/Wallet/Wallets/WalletSummary.tsx b/extension/source/Home/Wallet/Wallets/WalletSummary.tsx index 8dd3ba3c..5d912361 100644 --- a/extension/source/Home/Wallet/Wallets/WalletSummary.tsx +++ b/extension/source/Home/Wallet/Wallets/WalletSummary.tsx @@ -8,10 +8,12 @@ import { // PokerChip, // Circle, } from 'phosphor-react'; +import { useNavigate } from 'react-router-dom'; import Button from '../../../components/Button'; import type { IWallet } from './WalletWrapper'; import Balance from './Balance'; import onAction from '../../../helpers/onAction'; +import formatCompactAddress from '../../../helpers/formatCompactAddress'; interface IWalletSummary { onAction: () => void; @@ -19,15 +21,13 @@ interface IWalletSummary { wallet: IWallet; } -const addressShortner = (address: string) => { - return `${address.slice(0, 6)}...${address.slice(38)}`; -}; - export const WalletSummary: React.FunctionComponent = ({ onAction: onActionParam, expanded = false, wallet, }) => { + const navigate = useNavigate(); + return (
= ({ px-2 flex place-items-center gap-2 w-28" {...onAction(() => navigator.clipboard.writeText(wallet.address))} > - {addressShortner(wallet.address)} + {formatCompactAddress(wallet.address)}{' '} +
@@ -75,7 +76,7 @@ export const WalletSummary: React.FunctionComponent = ({
diff --git a/extension/source/Popup/helpers/formatCompactAddress.ts b/extension/source/helpers/formatCompactAddress.ts similarity index 100% rename from extension/source/Popup/helpers/formatCompactAddress.ts rename to extension/source/helpers/formatCompactAddress.ts diff --git a/extension/source/styles/index.scss b/extension/source/styles/index.scss index a5bb0c7d..29c4163b 100644 --- a/extension/source/styles/index.scss +++ b/extension/source/styles/index.scss @@ -21,6 +21,16 @@ @apply btn bg-blue-500 active:bg-blue-700; } + .btn-primary-outer { + @apply text-white rounded bg-blue-500; + cursor: pointer; + user-select: none; + } + + .btn-primary-inner { + @apply py-2 px-4 active:bg-blue-700 rounded; + } + .btn-primary-alert { @apply btn bg-alert-500 active:bg-alert-600; } diff --git a/extension/source/types/Rpc.ts b/extension/source/types/Rpc.ts index 179da1f7..7eff53dd 100644 --- a/extension/source/types/Rpc.ts +++ b/extension/source/types/Rpc.ts @@ -23,10 +23,10 @@ export type ProviderState = io.TypeOf; export const SendTransactionParams = io.type({ from: io.string, to: io.string, - gas: io.union([io.undefined, io.string]), - gasPrice: io.union([io.undefined, io.string]), - value: io.union([io.undefined, io.string]), - data: io.string, + gas: optional(io.string), + gasPrice: optional(io.string), + value: optional(io.string), + data: optional(io.string), }); export type SendTransactionParams = io.TypeOf;