From b90b53c59dbf3050bb49a2ae2e5796410883a3f3 Mon Sep 17 00:00:00 2001 From: Burnt Val Date: Fri, 8 Mar 2024 11:21:39 -0500 Subject: [PATCH 1/8] fix account balance formatting --- .../components/Overview.tsx | 17 ++++++++++--- apps/abstraxion-dashboard/utils/index.ts | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/apps/abstraxion-dashboard/components/Overview.tsx b/apps/abstraxion-dashboard/components/Overview.tsx index 77bb7157..9f6cbc5e 100644 --- a/apps/abstraxion-dashboard/components/Overview.tsx +++ b/apps/abstraxion-dashboard/components/Overview.tsx @@ -1,3 +1,5 @@ +import { formatBalance, getCommaSeperatedNumber } from "@/utils"; + interface OverviewProps { balanceInfo: BalanceInfo | null; } @@ -6,7 +8,7 @@ const XION_TO_USDC_CONVERSION = 50; export const Overview = ({ balanceInfo }: OverviewProps) => { const xionBalance = balanceInfo?.balances.find( - (balance) => balance.denom === "xion" + (balance) => balance.denom === "xion", ); return ( @@ -28,7 +30,10 @@ export const Overview = ({ balanceInfo }: OverviewProps) => {
{balanceInfo && (

- ${balanceInfo?.total} + ${/* TODO: Change once we support multiple currencies */} + {formatBalance( + Number(xionBalance?.amount) * XION_TO_USDC_CONVERSION, + )}

)} {/* Hidden until functionality is in place. */} @@ -64,10 +69,14 @@ export const Overview = ({ balanceInfo }: OverviewProps) => {

- {xionBalance.amount} XION + {getCommaSeperatedNumber(Number(xionBalance.amount))} XION

- ${Number(xionBalance.amount) * XION_TO_USDC_CONVERSION} USDC + $ + {formatBalance( + Number(xionBalance.amount) * XION_TO_USDC_CONVERSION, + )}{" "} + USD

diff --git a/apps/abstraxion-dashboard/utils/index.ts b/apps/abstraxion-dashboard/utils/index.ts index bd818611..98ffda1d 100644 --- a/apps/abstraxion-dashboard/utils/index.ts +++ b/apps/abstraxion-dashboard/utils/index.ts @@ -38,3 +38,28 @@ export function getEnvStringOrThrow(key: string, value?: string): string { return value; } + +export function removeTrailingDigits(number: number) { + return Math.floor(number / 1000000); +} + +export function getCommaSeperatedNumber(number: number) { + const millionthPart = removeTrailingDigits(number); + return new Intl.NumberFormat("en-US").format(millionthPart); +} + +export function formatBalance( + number: number, + locale: string = "en-US", + currency: string = "USD", +) { + const millionthPart = removeTrailingDigits(number); + return new Intl.NumberFormat(locale, { + style: "currency", + currency, + currencyDisplay: "code", + }) + .format(millionthPart) + .replace(currency, "") + .trim(); +} From 2b460cbb16b13a5bbdcca69531c0927e6f29e18e Mon Sep 17 00:00:00 2001 From: Burnt Val Date: Wed, 13 Mar 2024 11:27:57 -0400 Subject: [PATCH 2/8] add send functionality --- apps/abstraxion-dashboard/app/page.tsx | 12 +- .../components/ErrorDisplay/index.tsx | 22 +- .../components/Overview.tsx | 53 ++-- .../components/WalletSend/WalletSend.tsx | 272 ++++++++++++++++++ .../hooks/useAccountBalance.ts | 97 ++++--- apps/abstraxion-dashboard/utils/index.ts | 29 +- packages/ui/src/styles.css | 173 ++++++----- 7 files changed, 493 insertions(+), 165 deletions(-) create mode 100644 apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx diff --git a/apps/abstraxion-dashboard/app/page.tsx b/apps/abstraxion-dashboard/app/page.tsx index 9d5cc1ba..6b6ea53a 100644 --- a/apps/abstraxion-dashboard/app/page.tsx +++ b/apps/abstraxion-dashboard/app/page.tsx @@ -5,20 +5,12 @@ import { AccountInfo } from "@/components/AccountInfo"; import { AbstraxionContext } from "@/components/AbstraxionContext"; import { Overview } from "@/components/Overview"; import { Sidebar } from "@/components/Sidebar"; -import { useAccountBalance } from "@/hooks/useAccountBalance"; import { Abstraxion } from "@/components/Abstraxion"; -import { - AbstraxionAccount, - useAbstraxionAccount, - useAbstraxionSigningClient, -} from "../hooks"; - +import { AbstraxionAccount, useAbstraxionAccount } from "../hooks"; export default function Home() { const searchParams = useSearchParams(); const { data: account } = useAbstraxionAccount(); - const { client } = useAbstraxionSigningClient(); - const accountBalance = useAccountBalance(account, client); const contracts = searchParams.get("contracts"); const stake = Boolean(searchParams.get("stake")); @@ -45,7 +37,7 @@ export default function Home() {

Overview

- +

Account Info

diff --git a/apps/abstraxion-dashboard/components/ErrorDisplay/index.tsx b/apps/abstraxion-dashboard/components/ErrorDisplay/index.tsx index 6fac152c..b15316f0 100644 --- a/apps/abstraxion-dashboard/components/ErrorDisplay/index.tsx +++ b/apps/abstraxion-dashboard/components/ErrorDisplay/index.tsx @@ -6,9 +6,11 @@ import { import { Button } from "@burnt-labs/ui"; export const ErrorDisplay = ({ - message, + title = "OOPS! Something went wrong...", + message = "Please try again later.", onClose, }: { + title?: string; message?: string; onClose: VoidFunction; }) => { @@ -17,25 +19,21 @@ export const ErrorDisplay = ({ ) as AbstraxionContextProps; return ( -
-

- Uh oh. +
+

+ {title}

-

Something went wrong.

- {message && ( -

- {message} -

- )} +

+ {message} +

); diff --git a/apps/abstraxion-dashboard/components/Overview.tsx b/apps/abstraxion-dashboard/components/Overview.tsx index 9f6cbc5e..8633a7f5 100644 --- a/apps/abstraxion-dashboard/components/Overview.tsx +++ b/apps/abstraxion-dashboard/components/Overview.tsx @@ -1,14 +1,15 @@ import { formatBalance, getCommaSeperatedNumber } from "@/utils"; +import { useAccountBalance } from "@/hooks/useAccountBalance"; +import { RightArrowIcon } from "./Icons"; +import { WalletSend } from "./WalletSend/WalletSend"; -interface OverviewProps { - balanceInfo: BalanceInfo | null; -} +export const XION_TO_USDC_CONVERSION = 50; -const XION_TO_USDC_CONVERSION = 50; +export const Overview = () => { + const { balanceInfo: accountBalance, sendTokens } = useAccountBalance(); -export const Overview = ({ balanceInfo }: OverviewProps) => { - const xionBalance = balanceInfo?.balances.find( - (balance) => balance.denom === "xion", + const xionBalance = accountBalance?.balances.find( + (balance) => balance.denom === "uxion", ); return ( @@ -28,7 +29,7 @@ export const Overview = ({ balanceInfo }: OverviewProps) => { Current Balance

- {balanceInfo && ( + {accountBalance && (

${/* TODO: Change once we support multiple currencies */} {formatBalance( @@ -37,31 +38,23 @@ export const Overview = ({ balanceInfo }: OverviewProps) => {

)} {/* Hidden until functionality is in place. */} - {/*
-
+
+ {/*
-
-
- -
-
*/} +
*/} + + +
+ } + /> +
{/* Divider */} -
- {/* Wait for USDC */} - {/*
-

- USDC -

-
-

- 190 USDC -

-

- $190 USDC -

-
-
*/} +
{xionBalance && (

diff --git a/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx b/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx new file mode 100644 index 00000000..9ec6d130 --- /dev/null +++ b/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx @@ -0,0 +1,272 @@ +import { ChangeEvent, ReactElement, useEffect, useState } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogTrigger, + Input, +} from "@burnt-labs/ui"; +import { useAbstraxionAccount } from "@/hooks"; +import { formatBalance, isValidWalletAddress, truncateAddress } from "@/utils"; +import { XION_TO_USDC_CONVERSION } from "../Overview"; +import { ErrorDisplay } from "../ErrorDisplay"; +import { DeliverTxResponse } from "@cosmjs/stargate"; + +export function WalletSend({ + trigger, + sendTokens, + balanceInfo, +}: { + trigger: ReactElement; + sendTokens: ( + senderAddress: string, + sendAmount: number, + memo: string, + ) => Promise; + balanceInfo: BalanceInfo; +}) { + const { data: account } = useAbstraxionAccount(); + + const [isOpen, setIsOpen] = useState(false); + const [sendAmount, setSendAmount] = useState("0"); + const [amountError, setAmountError] = useState(""); + const [recipientAddress, setRecipientAddress] = useState(""); + const [recipientAddressError, setRecipientAddressError] = useState(""); + const [userMemo, setUserMemo] = useState(""); + + const [isOnReviewStep, setIsOnReviewStep] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [sendTokensError, setSendTokensError] = useState(false); + + // TODO: Make dialog only exit onClick of close button + + function handleAmountChange(event: ChangeEvent) { + setAmountError(""); + if (sendAmount === "0" && event.target.value === "00") return; + if (!event.target.value) { + setSendAmount("0"); + return; + } + setSendAmount(event.target.value.replace(/^0+/, "")); + } + + function handleStart() { + if (!sendAmount || sendAmount === "0") { + setAmountError("No amount entered"); + return; + } + + if (balanceInfo.total < Number(sendAmount) * 1000000) { + setAmountError("Input is greater than your current balance"); + return; + } + + if (!isValidWalletAddress(recipientAddress)) { + setRecipientAddressError("Invalid wallet address"); + return; + } + + setIsOnReviewStep(true); + } + + async function triggerSend() { + try { + setIsLoading(true); + + const res = await sendTokens( + recipientAddress, + Number(sendAmount), + userMemo, + ); + console.log(res); + setIsSuccess(true); + } catch (error) { + console.log(error); + setSendTokensError(true); + } finally { + setIsLoading(false); + } + } + + // Reset state on close + useEffect(() => { + if (!isOpen) { + setAmountError(""); + setSendAmount("0"); + setRecipientAddress(""); + setUserMemo(""); + setIsOnReviewStep(false); + setIsLoading(false); + setIsSuccess(false); + setSendTokensError(false); + } + }, [isOpen]); + + return ( +

+ {trigger} + + {sendTokensError ? ( + setIsOpen(false)} + /> + ) : isSuccess ? ( + <> +
+

+ SUCCESS! +

+

+ You have initiated the transaction below. +

+
+

+ Transfer Amount +

+

+ {sendAmount} XION +

+

+ $ + {formatBalance( + Number(sendAmount) * 1000000 * XION_TO_USDC_CONVERSION, + )}{" "} + USD +

+

+ {userMemo} +

+
+
+

+ From +

+

+ {account.id} +

+
+
+

+ To +

+

+ {recipientAddress} +

+
+ +
+ + ) : isOnReviewStep ? ( + <> +
+

+ REVIEW +

+

+ You are about to make the transaction below. +

+
+

+ Transfer Amount +

+

+ {sendAmount} XION +

+

+ $ + {formatBalance( + Number(sendAmount) * 1000000 * XION_TO_USDC_CONVERSION, + )}{" "} + USD +

+

+ {userMemo} +

+
+
+

+ From +

+

+ {account.id} +

+
+
+

+ To +

+

+ {recipientAddress} +

+
+ + +
+ + ) : ( + <> +

+ SEND +

+
+
+

Amount

+
+ +

+ XION +

+
+ {amountError ? ( +

{amountError}

+ ) : null} +
+
+ +

+ {truncateAddress(account.id)} +

+
+ { + setRecipientAddressError(""); + setRecipientAddress(e.target.value); + }} + placeholder="Recipient Address" + value={recipientAddress} + /> + setUserMemo(e.target.value)} + placeholder="Memo (Optional)" + value={userMemo} + /> + +
+ + )} + +
+ ); +} diff --git a/apps/abstraxion-dashboard/hooks/useAccountBalance.ts b/apps/abstraxion-dashboard/hooks/useAccountBalance.ts index 8cecc813..d433f126 100644 --- a/apps/abstraxion-dashboard/hooks/useAccountBalance.ts +++ b/apps/abstraxion-dashboard/hooks/useAccountBalance.ts @@ -1,49 +1,80 @@ -import { AbstraxionAccount } from "@/hooks"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { useAbstraxionAccount, useAbstraxionSigningClient } from "@/hooks"; -const XION_FRACTIONAL = 1000000; - -export function useAccountBalance(account?: AbstraxionAccount, client?: any) { +export function useAccountBalance() { + const { data: account } = useAbstraxionAccount(); + const { client } = useAbstraxionSigningClient(); const [balanceInfo, setBalanceInfo] = useState({ total: 0, balances: [], }); + async function fetchBalances() { + try { + if (!account) { + throw new Error("No account"); + } + + if (!client) { + throw new Error("No signing client"); + } + const uxionBalance = await client.getBalance(account.id, "uxion"); + + setBalanceInfo({ + total: Number(uxionBalance.amount), + balances: [uxionBalance], + }); + } catch (error) { + console.error("Error fetching balances:", error); + } + } + useEffect(() => { - if (!account?.id) return; - - const fetchBalances = async () => { - let newBalances: Coin[] = []; - const uxionBalance = await client?.getBalance(account.id, "uxion"); - - if (uxionBalance?.amount) { - const xionBalance: Coin = { - denom: "xion", - amount: String(Number(uxionBalance.amount) * XION_FRACTIONAL), - }; - newBalances.push(xionBalance); + if (account && client) { + fetchBalances(); + } + }, [account, client]); + + async function sendTokens( + senderAddress: string, + sendAmount: number, + memo: string, + ) { + try { + if (!account) { + throw new Error("No account"); } - const newTotal = newBalances.reduce( - (acc, curr) => acc + Number(curr.amount), - 0, - ); + if (!client) { + throw new Error("No signing client"); + } - const newBalanceInfo = { - total: newTotal, - balances: newBalances, - }; + // Convert user input to uxion + const amountToUxion = String(sendAmount * 1000000); + + const res = await client.sendTokens( + account.id, + senderAddress, + [{ denom: "uxion", amount: amountToUxion }], + { + amount: [{ denom: "uxion", amount: "0" }], + gas: "200000", // TODO: Dynamic? + }, + memo, + ); - // Check if the new balance info is different from the old one - if (JSON.stringify(newBalanceInfo) === JSON.stringify(balanceInfo)) { - return; + if (res.rawLog?.includes("failed")) { + throw new Error(res.rawLog); } - setBalanceInfo(newBalanceInfo); - }; + fetchBalances(); // Update balances after successful token send + return res; + } catch (error) { + throw error; + } + } - fetchBalances(); - }, [account, client, balanceInfo]); + const memoizedBalanceInfo = useMemo(() => balanceInfo, [balanceInfo]); - return balanceInfo; + return { balanceInfo: memoizedBalanceInfo, sendTokens }; } diff --git a/apps/abstraxion-dashboard/utils/index.ts b/apps/abstraxion-dashboard/utils/index.ts index 98ffda1d..e8c0df2f 100644 --- a/apps/abstraxion-dashboard/utils/index.ts +++ b/apps/abstraxion-dashboard/utils/index.ts @@ -40,12 +40,20 @@ export function getEnvStringOrThrow(key: string, value?: string): string { } export function removeTrailingDigits(number: number) { - return Math.floor(number / 1000000); + return number / 1000000; } export function getCommaSeperatedNumber(number: number) { const millionthPart = removeTrailingDigits(number); - return new Intl.NumberFormat("en-US").format(millionthPart); + return millionthPart.toLocaleString("en-US", { + minimumFractionDigits: Math.max( + 0, + Math.ceil( + Math.abs(millionthPart) < 1 ? Math.log10(Math.abs(millionthPart)) : 0, + ), + ), + maximumFractionDigits: 6, + }); } export function formatBalance( @@ -63,3 +71,20 @@ export function formatBalance( .replace(currency, "") .trim(); } + +export function isValidWalletAddress(address: string) { + if (address.length !== 40 && address.length !== 63) { + return false; + } + + const validCharacters = /^[0-9a-zA-Z]+$/; + if (!validCharacters.test(address)) { + return false; + } + + if (!address.startsWith("xion")) { + return false; + } + + return true; +} diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css index 6fe3fd91..9bbf9ed5 100644 --- a/packages/ui/src/styles.css +++ b/packages/ui/src/styles.css @@ -3,82 +3,99 @@ @tailwind utilities; @font-face { - font-family: 'Akkurat'; - src: url('./assets/fonts/AkkuratLLWeb-Thin.woff2') format('woff2'), - url('./assets/fonts/AkkuratLLWeb-Thin.woff') format('woff'); - font-weight: 100; - font-style: normal; - } - - @font-face { - font-family: 'Akkurat'; - src: url('./assets/fonts/AkkuratLLWeb-ThinItalic.woff2') format('woff2'), - url('./assets/fonts/AkkuratLLWeb-ThinItalic.woff') format('woff'); - font-weight: 100; - font-style: italic; - } - - @font-face { - font-family: 'Akkurat'; - src: url('./assets/fonts/AkkuratLLWeb-Light.woff2') format('woff2'), - url('./assets/fonts/AkkuratLLWeb-Light.woff') format('woff'); - font-weight: 300; - font-style: normal; - } - - @font-face { - font-family: 'Akkurat'; - src: url('./assets/fonts/AkkuratLLWeb-LightItalic.woff2') format('woff2'), - url('./assets/fonts/AkkuratLLWeb-LightItalic.woff') format('woff'); - font-weight: 300; - font-style: italic; - } - - @font-face { - font-family: 'Akkurat'; - src: url('./assets/fonts/AkkuratLLWeb-Regular.woff2') format('woff2'), - url('./assets/fonts/AkkuratLLWeb-Regular.woff') format('woff'); - font-weight: 400; - font-style: normal; - } - - @font-face { - font-family: 'Akkurat'; - src: url('./assets/fonts/AkkuratLLWeb-Italic.woff2') format('woff2'), - url('./assets/fonts/AkkuratLLWeb-Italic.woff') format('woff'); - font-weight: 400; - font-style: italic; - } - - @font-face { - font-family: 'Akkurat'; - src: url('./assets/fonts/AkkuratLLWeb-Bold.woff2') format('woff2'), - url('./assets/fonts/AkkuratLLWeb-Bold.woff') format('woff'); - font-weight: 700; - font-style: normal; - } - - @font-face { - font-family: 'Akkurat'; - src: url('./assets/fonts/AkkuratLLWeb-BoldItalic.woff2') format('woff2'), - url('./assets/fonts/AkkuratLLWeb-BoldItalic.woff') format('woff'); - font-weight: 700; - font-style: italic; - } - - @font-face { - font-family: 'Akkurat'; - src: url('./assets/fonts/AkkuratLLWeb-Black.woff2') format('woff2'), - url('./assets/fonts/AkkuratLLWeb-Black.woff') format('woff'); - font-weight: 900; - font-style: normal; - } - - @font-face { - font-family: 'Akkurat'; - src: url('./assets/fonts/AkkuratLLWeb-BlackItalic.woff2') format('woff2'), - url('./assets/fonts/AkkuratLLWeb-BlackItalic.woff') format('woff'); - font-weight: 900; - font-style: italic; + font-family: "Akkurat"; + src: + url("./assets/fonts/AkkuratLLWeb-Thin.woff2") format("woff2"), + url("./assets/fonts/AkkuratLLWeb-Thin.woff") format("woff"); + font-weight: 100; + font-style: normal; +} + +@font-face { + font-family: "Akkurat"; + src: + url("./assets/fonts/AkkuratLLWeb-ThinItalic.woff2") format("woff2"), + url("./assets/fonts/AkkuratLLWeb-ThinItalic.woff") format("woff"); + font-weight: 100; + font-style: italic; +} + +@font-face { + font-family: "Akkurat"; + src: + url("./assets/fonts/AkkuratLLWeb-Light.woff2") format("woff2"), + url("./assets/fonts/AkkuratLLWeb-Light.woff") format("woff"); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: "Akkurat"; + src: + url("./assets/fonts/AkkuratLLWeb-LightItalic.woff2") format("woff2"), + url("./assets/fonts/AkkuratLLWeb-LightItalic.woff") format("woff"); + font-weight: 300; + font-style: italic; +} + +@font-face { + font-family: "Akkurat"; + src: + url("./assets/fonts/AkkuratLLWeb-Regular.woff2") format("woff2"), + url("./assets/fonts/AkkuratLLWeb-Regular.woff") format("woff"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: "Akkurat"; + src: + url("./assets/fonts/AkkuratLLWeb-Italic.woff2") format("woff2"), + url("./assets/fonts/AkkuratLLWeb-Italic.woff") format("woff"); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: "Akkurat"; + src: + url("./assets/fonts/AkkuratLLWeb-Bold.woff2") format("woff2"), + url("./assets/fonts/AkkuratLLWeb-Bold.woff") format("woff"); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: "Akkurat"; + src: + url("./assets/fonts/AkkuratLLWeb-BoldItalic.woff2") format("woff2"), + url("./assets/fonts/AkkuratLLWeb-BoldItalic.woff") format("woff"); + font-weight: 700; + font-style: italic; +} + +@font-face { + font-family: "Akkurat"; + src: + url("./assets/fonts/AkkuratLLWeb-Black.woff2") format("woff2"), + url("./assets/fonts/AkkuratLLWeb-Black.woff") format("woff"); + font-weight: 900; + font-style: normal; +} + +@font-face { + font-family: "Akkurat"; + src: + url("./assets/fonts/AkkuratLLWeb-BlackItalic.woff2") format("woff2"), + url("./assets/fonts/AkkuratLLWeb-BlackItalic.woff") format("woff"); + font-weight: 900; + font-style: italic; +} + +@layer base { + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; } - +} From 9a8021aff9f08bdafc4b9b2cb8c2d52eb5613ef6 Mon Sep 17 00:00:00 2001 From: Burnt Val Date: Wed, 13 Mar 2024 11:50:46 -0400 Subject: [PATCH 3/8] remove ability to click outside of send dialog to close --- .../components/WalletSend/WalletSend.tsx | 10 +++++++++- packages/ui/src/icons/close.tsx | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx b/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx index 9ec6d130..6ec90ab2 100644 --- a/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx +++ b/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx @@ -11,6 +11,8 @@ import { formatBalance, isValidWalletAddress, truncateAddress } from "@/utils"; import { XION_TO_USDC_CONVERSION } from "../Overview"; import { ErrorDisplay } from "../ErrorDisplay"; import { DeliverTxResponse } from "@cosmjs/stargate"; +import { DialogClose } from "@burnt-labs/ui"; +import { CloseIcon } from "@burnt-labs/ui"; export function WalletSend({ trigger, @@ -106,7 +108,13 @@ export function WalletSend({ return ( {trigger} - + e.preventDefault()} + > + + + {sendTokensError ? ( { height="24" viewBox="0 0 24 24" fill="none" - stroke="black" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" From 181dd792ab1c383fe4f1b0ac40204d539922eb83 Mon Sep 17 00:00:00 2001 From: Burnt Val Date: Wed, 13 Mar 2024 12:11:44 -0400 Subject: [PATCH 4/8] remove prohibiting conditional on signingClient hook --- apps/abstraxion-dashboard/hooks/useAbstraxionSigningClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/abstraxion-dashboard/hooks/useAbstraxionSigningClient.ts b/apps/abstraxion-dashboard/hooks/useAbstraxionSigningClient.ts index 848cb14d..ed1b6cbc 100644 --- a/apps/abstraxion-dashboard/hooks/useAbstraxionSigningClient.ts +++ b/apps/abstraxion-dashboard/hooks/useAbstraxionSigningClient.ts @@ -118,10 +118,10 @@ export const useAbstraxionSigningClient = () => { }, [sessionToken, abstractAccount, connectionType, data, keplr]); useEffect(() => { - if (abstractAccount && !abstractClient) { + if (abstractAccount) { getSigner(); } - }, [abstractAccount, getSigner]); + }, [abstractAccount]); const memoizedClient = useMemo( () => ({ client: abstractClient }), From 783e769bae7d7ec046508f69aef47bc82f6c4645 Mon Sep 17 00:00:00 2001 From: Burnt Val Date: Wed, 13 Mar 2024 13:34:34 -0400 Subject: [PATCH 5/8] add changeset --- .changeset/curly-deers-beg.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/curly-deers-beg.md diff --git a/.changeset/curly-deers-beg.md b/.changeset/curly-deers-beg.md new file mode 100644 index 00000000..6afd0c35 --- /dev/null +++ b/.changeset/curly-deers-beg.md @@ -0,0 +1,6 @@ +--- +"abstraxion-dashboard": minor +"@burnt-labs/ui": minor +--- + +Implement wallet send functionality From b760898df3a735873bec8fd8ce48a106a151bd68 Mon Sep 17 00:00:00 2001 From: Burnt Val Date: Wed, 13 Mar 2024 13:48:12 -0400 Subject: [PATCH 6/8] styling fix --- .../components/WalletSend/WalletSend.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx b/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx index 6ec90ab2..8d1367d9 100644 --- a/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx +++ b/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx @@ -7,7 +7,7 @@ import { Input, } from "@burnt-labs/ui"; import { useAbstraxionAccount } from "@/hooks"; -import { formatBalance, isValidWalletAddress, truncateAddress } from "@/utils"; +import { formatBalance, isValidWalletAddress } from "@/utils"; import { XION_TO_USDC_CONVERSION } from "../Overview"; import { ErrorDisplay } from "../ErrorDisplay"; import { DeliverTxResponse } from "@cosmjs/stargate"; @@ -224,10 +224,10 @@ export function WalletSend({ ) : ( <> -

- SEND -

-
+
+

+ SEND +

Amount

-

- {truncateAddress(account.id)} +

+ {account.id}

Date: Wed, 13 Mar 2024 14:08:19 -0400 Subject: [PATCH 7/8] add missing balance component to send modal; fix address validity check --- .../components/WalletSend/WalletSend.tsx | 29 +++++++++++++++++-- apps/abstraxion-dashboard/utils/index.ts | 2 +- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx b/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx index 8d1367d9..6161e646 100644 --- a/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx +++ b/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx @@ -229,11 +229,34 @@ export function WalletSend({ SEND
-

Amount

+
+

XION

+

+ {/* TODO: Make configurable once we support multiple currencies */} + Balance: {formatBalance(Number(balanceInfo.total))} XION{" "} + + $ + {formatBalance( + Number(balanceInfo.total) * XION_TO_USDC_CONVERSION, + )}{" "} + USD + +

+
+
+

Amount

+

+ =$ + {formatBalance( + Number(sendAmount) * 1000000 * XION_TO_USDC_CONVERSION, + )}{" "} + USD +

+
Date: Fri, 15 Mar 2024 13:24:56 -0400 Subject: [PATCH 8/8] modularize wallet send --- .../components/WalletSend/WalletSend.tsx | 278 +----------------- .../components/WalletSend/WalletSendForm.tsx | 268 +++++++++++++++++ 2 files changed, 276 insertions(+), 270 deletions(-) create mode 100644 apps/abstraxion-dashboard/components/WalletSend/WalletSendForm.tsx diff --git a/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx b/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx index 6161e646..198ca1ec 100644 --- a/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx +++ b/apps/abstraxion-dashboard/components/WalletSend/WalletSend.tsx @@ -1,18 +1,9 @@ -import { ChangeEvent, ReactElement, useEffect, useState } from "react"; -import { - Button, - Dialog, - DialogContent, - DialogTrigger, - Input, -} from "@burnt-labs/ui"; -import { useAbstraxionAccount } from "@/hooks"; -import { formatBalance, isValidWalletAddress } from "@/utils"; -import { XION_TO_USDC_CONVERSION } from "../Overview"; -import { ErrorDisplay } from "../ErrorDisplay"; +import { ReactElement, useState } from "react"; +import { Dialog, DialogContent, DialogTrigger } from "@burnt-labs/ui"; import { DeliverTxResponse } from "@cosmjs/stargate"; import { DialogClose } from "@burnt-labs/ui"; import { CloseIcon } from "@burnt-labs/ui"; +import { WalletSendForm } from "./WalletSendForm"; export function WalletSend({ trigger, @@ -27,83 +18,7 @@ export function WalletSend({ ) => Promise; balanceInfo: BalanceInfo; }) { - const { data: account } = useAbstraxionAccount(); - const [isOpen, setIsOpen] = useState(false); - const [sendAmount, setSendAmount] = useState("0"); - const [amountError, setAmountError] = useState(""); - const [recipientAddress, setRecipientAddress] = useState(""); - const [recipientAddressError, setRecipientAddressError] = useState(""); - const [userMemo, setUserMemo] = useState(""); - - const [isOnReviewStep, setIsOnReviewStep] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isSuccess, setIsSuccess] = useState(false); - const [sendTokensError, setSendTokensError] = useState(false); - - // TODO: Make dialog only exit onClick of close button - - function handleAmountChange(event: ChangeEvent) { - setAmountError(""); - if (sendAmount === "0" && event.target.value === "00") return; - if (!event.target.value) { - setSendAmount("0"); - return; - } - setSendAmount(event.target.value.replace(/^0+/, "")); - } - - function handleStart() { - if (!sendAmount || sendAmount === "0") { - setAmountError("No amount entered"); - return; - } - - if (balanceInfo.total < Number(sendAmount) * 1000000) { - setAmountError("Input is greater than your current balance"); - return; - } - - if (!isValidWalletAddress(recipientAddress)) { - setRecipientAddressError("Invalid wallet address"); - return; - } - - setIsOnReviewStep(true); - } - - async function triggerSend() { - try { - setIsLoading(true); - - const res = await sendTokens( - recipientAddress, - Number(sendAmount), - userMemo, - ); - console.log(res); - setIsSuccess(true); - } catch (error) { - console.log(error); - setSendTokensError(true); - } finally { - setIsLoading(false); - } - } - - // Reset state on close - useEffect(() => { - if (!isOpen) { - setAmountError(""); - setSendAmount("0"); - setRecipientAddress(""); - setUserMemo(""); - setIsOnReviewStep(false); - setIsLoading(false); - setIsSuccess(false); - setSendTokensError(false); - } - }, [isOpen]); return ( @@ -115,188 +30,11 @@ export function WalletSend({ - {sendTokensError ? ( - setIsOpen(false)} - /> - ) : isSuccess ? ( - <> -
-

- SUCCESS! -

-

- You have initiated the transaction below. -

-
-

- Transfer Amount -

-

- {sendAmount} XION -

-

- $ - {formatBalance( - Number(sendAmount) * 1000000 * XION_TO_USDC_CONVERSION, - )}{" "} - USD -

-

- {userMemo} -

-
-
-

- From -

-

- {account.id} -

-
-
-

- To -

-

- {recipientAddress} -

-
- -
- - ) : isOnReviewStep ? ( - <> -
-

- REVIEW -

-

- You are about to make the transaction below. -

-
-

- Transfer Amount -

-

- {sendAmount} XION -

-

- $ - {formatBalance( - Number(sendAmount) * 1000000 * XION_TO_USDC_CONVERSION, - )}{" "} - USD -

-

- {userMemo} -

-
-
-

- From -

-

- {account.id} -

-
-
-

- To -

-

- {recipientAddress} -

-
- - -
- - ) : ( - <> -
-

- SEND -

-
-
-

XION

-

- {/* TODO: Make configurable once we support multiple currencies */} - Balance: {formatBalance(Number(balanceInfo.total))} XION{" "} - - $ - {formatBalance( - Number(balanceInfo.total) * XION_TO_USDC_CONVERSION, - )}{" "} - USD - -

-
-
-

Amount

-

- =$ - {formatBalance( - Number(sendAmount) * 1000000 * XION_TO_USDC_CONVERSION, - )}{" "} - USD -

-
-
- -

- XION -

-
- {amountError ? ( -

{amountError}

- ) : null} -
-
- -

- {account.id} -

-
- { - setRecipientAddressError(""); - setRecipientAddress(e.target.value); - }} - placeholder="Recipient Address" - value={recipientAddress} - /> - setUserMemo(e.target.value)} - placeholder="Memo (Optional)" - value={userMemo} - /> - -
- - )} +
); diff --git a/apps/abstraxion-dashboard/components/WalletSend/WalletSendForm.tsx b/apps/abstraxion-dashboard/components/WalletSend/WalletSendForm.tsx new file mode 100644 index 00000000..458d05cb --- /dev/null +++ b/apps/abstraxion-dashboard/components/WalletSend/WalletSendForm.tsx @@ -0,0 +1,268 @@ +import { ChangeEvent, useState } from "react"; +import { DeliverTxResponse } from "@cosmjs/stargate"; +import { Button, Input } from "@burnt-labs/ui"; +import { XION_TO_USDC_CONVERSION } from "@/components/Overview"; +import { ErrorDisplay } from "@/components/ErrorDisplay"; +import { useAbstraxionAccount } from "@/hooks"; +import { formatBalance, isValidWalletAddress } from "@/utils"; + +export function WalletSendForm({ + sendTokens, + balanceInfo, + setIsOpen, +}: { + sendTokens: ( + senderAddress: string, + sendAmount: number, + memo: string, + ) => Promise; + balanceInfo: BalanceInfo; + setIsOpen: any; +}) { + const { data: account } = useAbstraxionAccount(); + + const [sendAmount, setSendAmount] = useState("0"); + const [amountError, setAmountError] = useState(""); + const [recipientAddress, setRecipientAddress] = useState(""); + const [recipientAddressError, setRecipientAddressError] = useState(""); + const [userMemo, setUserMemo] = useState(""); + + const [isOnReviewStep, setIsOnReviewStep] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [sendTokensError, setSendTokensError] = useState(false); + + function handleAmountChange(event: ChangeEvent) { + setAmountError(""); + if (sendAmount === "0" && event.target.value === "00") return; + if (!event.target.value) { + setSendAmount("0"); + return; + } + setSendAmount(event.target.value.replace(/^0+/, "")); + } + + function handleStart() { + if (!sendAmount || sendAmount === "0") { + setAmountError("No amount entered"); + return; + } + + if (balanceInfo.total < Number(sendAmount) * 1000000) { + setAmountError("Input is greater than your current balance"); + return; + } + + if (!isValidWalletAddress(recipientAddress)) { + setRecipientAddressError("Invalid wallet address"); + return; + } + + setIsOnReviewStep(true); + } + + async function triggerSend() { + try { + setIsLoading(true); + + const res = await sendTokens( + recipientAddress, + Number(sendAmount), + userMemo, + ); + console.log(res); + setIsSuccess(true); + } catch (error) { + console.log(error); + setSendTokensError(true); + } finally { + setIsLoading(false); + } + } + return ( + <> + {sendTokensError ? ( + setIsOpen(false)} + /> + ) : isSuccess ? ( + <> +
+

+ SUCCESS! +

+

+ You have initiated the transaction below. +

+
+

+ Transfer Amount +

+

+ {sendAmount} XION +

+

+ $ + {formatBalance( + Number(sendAmount) * 1000000 * XION_TO_USDC_CONVERSION, + )}{" "} + USD +

+

+ {userMemo} +

+
+
+

+ From +

+

+ {account.id} +

+
+
+

+ To +

+

+ {recipientAddress} +

+
+ +
+ + ) : isOnReviewStep ? ( + <> +
+

+ REVIEW +

+

+ You are about to make the transaction below. +

+
+

+ Transfer Amount +

+

+ {sendAmount} XION +

+

+ $ + {formatBalance( + Number(sendAmount) * 1000000 * XION_TO_USDC_CONVERSION, + )}{" "} + USD +

+

+ {userMemo} +

+
+
+

+ From +

+

+ {account.id} +

+
+
+

+ To +

+

+ {recipientAddress} +

+
+ + +
+ + ) : ( + <> +
+

+ SEND +

+
+
+

XION

+

+ {/* TODO: Make configurable once we support multiple currencies */} + Balance: {formatBalance(Number(balanceInfo.total))} XION{" "} + + $ + {formatBalance( + Number(balanceInfo.total) * XION_TO_USDC_CONVERSION, + )}{" "} + USD + +

+
+
+

Amount

+

+ =$ + {formatBalance( + Number(sendAmount) * 1000000 * XION_TO_USDC_CONVERSION, + )}{" "} + USD +

+
+
+ +

+ XION +

+
+ {amountError ? ( +

{amountError}

+ ) : null} +
+
+ +

+ {account.id} +

+
+ { + setRecipientAddressError(""); + setRecipientAddress(e.target.value); + }} + placeholder="Recipient Address" + value={recipientAddress} + /> + setUserMemo(e.target.value)} + placeholder="Memo (Optional)" + value={userMemo} + /> + +
+ + )} + + ); +}