diff --git a/main/api/ledger/utils/ledger.ts b/main/api/ledger/utils/ledger.ts index 83895300..cd75114d 100644 --- a/main/api/ledger/utils/ledger.ts +++ b/main/api/ledger/utils/ledger.ts @@ -14,6 +14,7 @@ import { z } from "zod"; import { ledgerStore } from "../../../stores/ledgerStore"; import { handleImportAccount } from "../../accounts/handleImportAccount"; import { logger } from "../../ironfish/logger"; +import { manager } from "../../manager"; import { handleSendTransactionInput } from "../../transactions/handleSendTransaction"; import { PromiseQueue } from "../../utils/promiseQueue"; import { createUnsignedTransaction } from "../../utils/transactions"; @@ -409,13 +410,20 @@ class LedgerManager { throw new Error(signResponse.errorMessage || "No signature returned"); } - const splitSignParams = { - unsignedTransaction: unsignedTransactionBuffer, + const ironfish = await manager.getIronfish(); + const rpcClient = await ironfish.rpcClient(); + + const addSignatureResponse = await rpcClient.wallet.addSignature({ + unsignedTransaction, signature: signResponse.signature.toString("hex"), - }; + }); + + const addTransactionResponse = await rpcClient.wallet.addTransaction({ + transaction: addSignatureResponse.content.transaction, + broadcast: true, + }); - // @todo: Sign and submit the transaction once addSignature is available from the RPC client - return splitSignParams; + return addTransactionResponse.content; } catch (err) { const message = err instanceof Error ? err.message : "Failed to import account"; diff --git a/main/api/transactions/handleSendTransaction.ts b/main/api/transactions/handleSendTransaction.ts index 7352e2aa..b2b716fd 100644 --- a/main/api/transactions/handleSendTransaction.ts +++ b/main/api/transactions/handleSendTransaction.ts @@ -9,7 +9,7 @@ export const handleSendTransactionInput = z.object({ toAccount: z.string(), assetId: z.string(), amount: z.string(), - fee: z.number(), + fee: z.number().nullable(), memo: z.string().optional(), }); diff --git a/main/api/utils/transactions.ts b/main/api/utils/transactions.ts index bd76f9b9..b1b0975f 100644 --- a/main/api/utils/transactions.ts +++ b/main/api/utils/transactions.ts @@ -12,7 +12,7 @@ export const createTransactionInput = z.object({ toAccount: z.string(), assetId: z.string(), amount: z.string(), - fee: z.number(), + fee: z.number().nullable(), memo: z.string().optional(), }); @@ -67,7 +67,7 @@ export async function createRawTransaction({ assetId: assetId, }, ], - fee: CurrencyUtils.encode(BigInt(fee)), + fee: fee ? CurrencyUtils.encode(BigInt(fee)) : null, feeRate: null, expiration: undefined, confirmations: undefined, diff --git a/package-lock.json b/package-lock.json index 05b25abc..82fcf4ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MPL-2.0", "dependencies": { - "@ironfish/sdk": "2.4.1", + "@ironfish/sdk": "2.5.0", "@ledgerhq/hw-transport-node-hid": "^6.29.1", "electron-serve": "^1.1.0", "keccak": "^3.0.4" @@ -4521,26 +4521,26 @@ "dev": true }, "node_modules/@ironfish/rust-nodejs": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs/-/rust-nodejs-2.4.0.tgz", - "integrity": "sha512-dul47EmZdsP4whs9Lc+idED080yYf/n5Eq1zd2/8xRCYwr4an74GqupcTKFpkHKkD/3Gf/eQjGZxrQqrXrCiZA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs/-/rust-nodejs-2.5.0.tgz", + "integrity": "sha512-fIrX1DlOUOOvg5RO5y98h3uVm6Tl/0v5vbpJAOKcPwiMjw0/b+MCwtkQ+h6ey4I2Y5oqa9vy5I998dEzjk4Hww==", "engines": { "node": ">=18" }, "optionalDependencies": { - "@ironfish/rust-nodejs-darwin-arm64": "2.4.0", - "@ironfish/rust-nodejs-darwin-x64": "2.4.0", - "@ironfish/rust-nodejs-linux-arm64-gnu": "2.4.0", - "@ironfish/rust-nodejs-linux-arm64-musl": "2.4.0", - "@ironfish/rust-nodejs-linux-x64-gnu": "2.4.0", - "@ironfish/rust-nodejs-linux-x64-musl": "2.4.0", - "@ironfish/rust-nodejs-win32-x64-msvc": "2.4.0" + "@ironfish/rust-nodejs-darwin-arm64": "2.5.0", + "@ironfish/rust-nodejs-darwin-x64": "2.5.0", + "@ironfish/rust-nodejs-linux-arm64-gnu": "2.5.0", + "@ironfish/rust-nodejs-linux-arm64-musl": "2.5.0", + "@ironfish/rust-nodejs-linux-x64-gnu": "2.5.0", + "@ironfish/rust-nodejs-linux-x64-musl": "2.5.0", + "@ironfish/rust-nodejs-win32-x64-msvc": "2.5.0" } }, "node_modules/@ironfish/rust-nodejs-darwin-arm64": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-darwin-arm64/-/rust-nodejs-darwin-arm64-2.4.0.tgz", - "integrity": "sha512-Fj4U/fcP2j2R6qcUEgbXMz37IBC14tDGorg9f2LxU+lPeqHmsNApKrLrEXTkwW0Ec/pfxz09vkX99DaKIKTTIA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-darwin-arm64/-/rust-nodejs-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-xNnD+9wZRVTFNSDEe0W6qnFlxDW6vIwI3J6N0bziDRJ+A3I5+Zzgb79LYmd+79TEGs2Zv87Il8G3INu/ahcR7w==", "cpu": [ "arm64" ], @@ -4553,9 +4553,9 @@ } }, "node_modules/@ironfish/rust-nodejs-darwin-x64": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-darwin-x64/-/rust-nodejs-darwin-x64-2.4.0.tgz", - "integrity": "sha512-/jeUuirjCGG9g8BgYiIc9bWBhBuhjA3+F7v/qxT9If/Xe/DFF7GZwwiAGhLuG0kA483H2qB4EAa+PgUNjSazQQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-darwin-x64/-/rust-nodejs-darwin-x64-2.5.0.tgz", + "integrity": "sha512-K8MA3vYooGAecfQyYbSVSNz+QOFBowRixw8hKZAMnVp9skjGjnxQDdK/dhWbvsFT594Z3zNb0ZDLgcI55vM97w==", "cpu": [ "x64" ], @@ -4568,9 +4568,9 @@ } }, "node_modules/@ironfish/rust-nodejs-linux-arm64-gnu": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-linux-arm64-gnu/-/rust-nodejs-linux-arm64-gnu-2.4.0.tgz", - "integrity": "sha512-QO71yoOUBq2giGSwqafshHd438cX8x6COVty769RdxC/TL7adJKJ8eMrk/DQ+ou5Y8vjlQxoA0hXUwV5rvIpyg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-linux-arm64-gnu/-/rust-nodejs-linux-arm64-gnu-2.5.0.tgz", + "integrity": "sha512-gbrbB6OFGRB206Iq3chR4k5zGKIykmo+0v49N7h3/q/c/HU5BUOxISOFhQVABr4nkIOB+PuL1LGQ0ypWOhHH9g==", "cpu": [ "arm64" ], @@ -4583,9 +4583,9 @@ } }, "node_modules/@ironfish/rust-nodejs-linux-arm64-musl": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-linux-arm64-musl/-/rust-nodejs-linux-arm64-musl-2.4.0.tgz", - "integrity": "sha512-31vtiBdJYE0vku3GMLBDFq6mGubqNahPT7r1Nu69+/r3RPyds657WZbzHvaqYYRzLLnAGyoPRVGIr07G+C+HXw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-linux-arm64-musl/-/rust-nodejs-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-AJKZp2BZBTDCesEZImd7eQ6nM9ByHAyHJXrBnjUzciCj61PqcjNremg8eYX3J1l8U1NDm5thyVU9K43Rb7aLLA==", "cpu": [ "arm64" ], @@ -4598,9 +4598,9 @@ } }, "node_modules/@ironfish/rust-nodejs-linux-x64-gnu": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-linux-x64-gnu/-/rust-nodejs-linux-x64-gnu-2.4.0.tgz", - "integrity": "sha512-s1S7wnyUZBV0ZoGZLfZfo4WRUvlabi9Az+MIRYYCeFKO/RoZF0+F2in7wgsGC3oIweSGRM84Mjps/TOzzzBmAQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-linux-x64-gnu/-/rust-nodejs-linux-x64-gnu-2.5.0.tgz", + "integrity": "sha512-AL3HuehgC3Z2g3FZPmlMT4lWVcqlCSSC4J+ydREXugMc7q4o2kw4ggW9BeJyf56gXvzlLB3TwbzGQvRYWuRFtw==", "cpu": [ "x64" ], @@ -4613,9 +4613,9 @@ } }, "node_modules/@ironfish/rust-nodejs-linux-x64-musl": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-linux-x64-musl/-/rust-nodejs-linux-x64-musl-2.4.0.tgz", - "integrity": "sha512-OEx2J0m9yBR9RAjRIp1395mspMuWCTtHDvR3bvMKDMAjg3kkb6MMN/ndhesO7FGarUSPMZrhZ9aOwIvRRrWwmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-linux-x64-musl/-/rust-nodejs-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-qI0Pykpyrp8kIo08DVr6aXHEUOXPq7DnAcg2qpaK5+LJSvSVNnui8H6Z24HIm0utXm55ekaZYm5vYRdSSLqtnQ==", "cpu": [ "x64" ], @@ -4628,9 +4628,9 @@ } }, "node_modules/@ironfish/rust-nodejs-win32-x64-msvc": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-win32-x64-msvc/-/rust-nodejs-win32-x64-msvc-2.4.0.tgz", - "integrity": "sha512-nMW5rnnGzhbeeuhZOUBHYMz6QRWLtQl53dn7YBK+KMhIzZN51Yd3Oz0FdFXgJNzwVhaUSB7CK3PLsRMvfIO0jg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@ironfish/rust-nodejs-win32-x64-msvc/-/rust-nodejs-win32-x64-msvc-2.5.0.tgz", + "integrity": "sha512-uduXZ56usjwmDF0f0ubhb2drXorjlgpaULJstWEe0AyiohRxx22UAGJaAKRwuP1/ETurMyPJ4WsnMmKW2E4DoQ==", "cpu": [ "x64" ], @@ -4643,13 +4643,13 @@ } }, "node_modules/@ironfish/sdk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@ironfish/sdk/-/sdk-2.4.1.tgz", - "integrity": "sha512-FapSOLkaFRHhyMKSDZVLhyhWe00q0UjuDLOZlensh/ddzQQTKCEZyfw/Mn4Z3C8kfCy3xWGZ2kIJx1Do3JV8EA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@ironfish/sdk/-/sdk-2.5.0.tgz", + "integrity": "sha512-CebC1Ob4RYl24e05/sP9XiJ2sSsYEBI5JXuk+niEKAYp/LEytgL36XpVuILKMQmzyS9eZ4XXkoSDiSAjgaLIow==", "dependencies": { "@ethersproject/bignumber": "5.7.0", "@fast-csv/format": "4.3.5", - "@ironfish/rust-nodejs": "2.4.0", + "@ironfish/rust-nodejs": "2.5.0", "@napi-rs/blake-hash": "1.3.3", "axios": "1.7.2", "bech32": "2.0.0", @@ -4668,6 +4668,7 @@ "leveldown": "6.1.1", "levelup": "4.4.0", "lodash": "4.17.21", + "nock": "13.5.4", "node-datachannel": "0.8.0", "node-forge": "1.3.1", "parse-json": "5.2.0", @@ -12057,9 +12058,7 @@ "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "optional": true + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "node_modules/json5": { "version": "2.2.3", @@ -13869,6 +13868,19 @@ "node": ">= 10.0.0" } }, + "node_modules/nock": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", + "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-abi": { "version": "3.65.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz", @@ -14770,6 +14782,14 @@ "react-is": "^16.13.1" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "engines": { + "node": ">= 8" + } + }, "node_modules/property-expr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", diff --git a/package.json b/package.json index b4e6d1af..cc7a5512 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "license": "MPL-2.0", "dependencies": { - "@ironfish/sdk": "2.4.1", + "@ironfish/sdk": "2.5.0", "@ledgerhq/hw-transport-node-hid": "^6.29.1", "electron-serve": "^1.1.0", "keccak": "^3.0.4" diff --git a/renderer/components/AccountAssets/AccountAssets.tsx b/renderer/components/AccountAssets/AccountAssets.tsx index 447a3190..47f4ca5d 100644 --- a/renderer/components/AccountAssets/AccountAssets.tsx +++ b/renderer/components/AccountAssets/AccountAssets.tsx @@ -49,7 +49,7 @@ const messages = defineMessages({ export function AccountAssets({ accountName }: { accountName: string }) { const { formatMessage } = useIntl(); - const { data } = trpcReact.getAccount.useQuery({ + const { data: accountData } = trpcReact.getAccount.useQuery({ name: accountName, }); @@ -63,11 +63,14 @@ export function AccountAssets({ accountName }: { accountName: string }) { }, ); - if (!data) { + if (!accountData) { // @todo: Error handling return null; } + const isAccountSendEligible = + !accountData.status.viewOnly || accountData.isLedger; + return ( @@ -98,19 +101,19 @@ export function AccountAssets({ accountName }: { accountName: string }) { $IRON - {formatOre(data.balances.iron.confirmed)} + {formatOre(accountData.balances.iron.confirmed)} {formatMessage(messages.sendButton)} @@ -141,7 +146,7 @@ export function AccountAssets({ accountName }: { accountName: string }) { - {data.balances.custom.length > 0 && ( + {accountData.balances.custom.length > 0 && ( 1 ? "repeat(2, 1fr)" : "1fr" + accountData.balances.custom.length > 1 + ? "repeat(2, 1fr)" + : "1fr" } > - {data.balances.custom.map((balance) => { + {accountData.balances.custom.map((balance) => { const { confirmed, assetId, asset } = balance; const majorString = CurrencyUtils.render( confirmed, diff --git a/renderer/components/AccountRow/AccountRow.tsx b/renderer/components/AccountRow/AccountRow.tsx index 753911db..8f79088f 100644 --- a/renderer/components/AccountRow/AccountRow.tsx +++ b/renderer/components/AccountRow/AccountRow.tsx @@ -70,6 +70,8 @@ export function AccountRow({ refetchInterval: 5000, }, ); + + const isAccountSendEligible = !viewOnly || isLedger; return ( @@ -105,7 +107,7 @@ export function AccountRow({ {name} {isLedger && } - {!isLedger && viewOnly && } + {!isAccountSendEligible && } {formatOre(balance)} $IRON @@ -120,7 +122,7 @@ export function AccountRow({ > { e.preventDefault(); e.stopPropagation(); - if (viewOnly || !isSynced.synced) return; + if (!isAccountSendEligible || !isSynced.synced) return; router.push(`/send?account=${name}`); }} > diff --git a/renderer/components/SendAssetsForm/ConfirmLedgerModal/ConfirmLedgerModal.tsx b/renderer/components/SendAssetsForm/ConfirmLedgerModal/ConfirmLedgerModal.tsx index a6c67cbf..15d790f0 100644 --- a/renderer/components/SendAssetsForm/ConfirmLedgerModal/ConfirmLedgerModal.tsx +++ b/renderer/components/SendAssetsForm/ConfirmLedgerModal/ConfirmLedgerModal.tsx @@ -14,7 +14,9 @@ import { PillButton } from "@/ui/PillButton/PillButton"; import { StepConfirm } from "./Steps/StepConfirm"; import { StepConnect } from "./Steps/StepConnect"; -import { ReviewTransaction } from "../ReviewTransaction/ReviewTransaction"; +import { ReviewTransaction } from "../SharedConfirmSteps/ReviewTransaction"; +import { SubmissionError } from "../SharedConfirmSteps/SubmissionError"; +import { TransactionSubmitted } from "../SharedConfirmSteps/TransactionSubmitted"; import { TransactionData } from "../transactionSchema"; const messages = defineMessages({ @@ -65,7 +67,11 @@ export function ConfirmLedgerModal({ const [_statusError, setStatusError] = useState(""); const [step, setStep] = useState< - "IDLE" | "CONNECT_LEDGER" | "CONFIRM_TRANSACTION" | "TRANSACTION_SUBMITTED" + | "IDLE" + | "CONNECT_LEDGER" + | "CONFIRM_TRANSACTION" + | "TRANSACTION_SUBMITTED" + | "SUBMISSION_ERROR" >("IDLE"); trpcReact.ledgerStatus.useSubscription(undefined, { @@ -80,16 +86,16 @@ export function ConfirmLedgerModal({ const { mutate: submitTransaction, data: submittedTransactionData, - isIdle, isLoading, - isError: _isError, - isSuccess: _isSuccess, reset, - error: error, + error, } = trpcReact.submitLedgerTransaction.useMutation({ onSuccess: () => { setStep("TRANSACTION_SUBMITTED"); }, + onError: () => { + setStep("SUBMISSION_ERROR"); + }, }); useEffect(() => { @@ -106,11 +112,6 @@ export function ConfirmLedgerModal({ onCancel(); }, [onCancel, reset]); - // @todo: Remove this once the backend is submitting the signed transaction to the network - if (!isIdle) { - console.log({ submittedTransactionData, error }); - } - return ( @@ -179,6 +180,24 @@ export function ConfirmLedgerModal({ )} + {step === "TRANSACTION_SUBMITTED" && ( + + )} + {step === "SUBMISSION_ERROR" && ( + { + submitTransaction(transactionData); + setStep("CONFIRM_TRANSACTION"); + }} + /> + )} ); diff --git a/renderer/components/SendAssetsForm/ConfirmTransactionModal/ConfirmTransactionModal.tsx b/renderer/components/SendAssetsForm/ConfirmTransactionModal/ConfirmTransactionModal.tsx index eaa3c262..698c27e9 100644 --- a/renderer/components/SendAssetsForm/ConfirmTransactionModal/ConfirmTransactionModal.tsx +++ b/renderer/components/SendAssetsForm/ConfirmTransactionModal/ConfirmTransactionModal.tsx @@ -2,84 +2,26 @@ import { Modal, ModalOverlay, ModalContent, - ModalFooter, ModalBody, Heading, - Text, Box, Progress, } from "@chakra-ui/react"; -import { useRouter } from "next/router"; import { useCallback } from "react"; import { defineMessages, useIntl } from "react-intl"; import { AssetOptionType } from "@/components/AssetAmountInput/utils"; import { trpcReact, TRPCRouterOutputs } from "@/providers/TRPCProvider"; -import { PillButton } from "@/ui/PillButton/PillButton"; -import { ReviewTransaction } from "../ReviewTransaction/ReviewTransaction"; +import { ReviewTransaction } from "../SharedConfirmSteps/ReviewTransaction"; +import { SubmissionError } from "../SharedConfirmSteps/SubmissionError"; +import { TransactionSubmitted } from "../SharedConfirmSteps/TransactionSubmitted"; import { TransactionData } from "../transactionSchema"; const messages = defineMessages({ - confirmTransactionDetails: { - defaultMessage: "Confirm Transaction Details", - }, - from: { - defaultMessage: "From:", - }, - to: { - defaultMessage: "To:", - }, - amount: { - defaultMessage: "Amount:", - }, - fee: { - defaultMessage: "Fee:", - }, - memo: { - defaultMessage: "Memo:", - }, submittingTransaction: { defaultMessage: "Submitting Transaction", }, - transactionSubmitted: { - defaultMessage: "Transaction Submitted", - }, - transactionSubmittedText: { - defaultMessage: - "Your transaction has been submitted. It may take a few minutes until it is confirmed. This transaction will appear in your activity as pending until it is confirmed.", - }, - transactionError: { - defaultMessage: "Transaction Error", - }, - transactionErrorText: { - defaultMessage: - "Something went wrong. Please review your transaction and try again.", - }, - error: { - defaultMessage: "Error", - }, - cancelTransaction: { - defaultMessage: "Cancel Transaction", - }, - confirmAndSend: { - defaultMessage: "Confirm & Send", - }, - viewAccountActivity: { - defaultMessage: "View Account Activity", - }, - viewTransaction: { - defaultMessage: "View Transaction", - }, - tryAgain: { - defaultMessage: "Try Again", - }, - cancel: { - defaultMessage: "Cancel", - }, - unknownAsset: { - defaultMessage: "unknown asset", - }, }); type Props = { @@ -108,7 +50,6 @@ export function ConfirmTransactionModal({ error, } = trpcReact.sendTransaction.useMutation(); - const router = useRouter(); const { formatMessage } = useIntl(); const handleClose = useCallback(() => { @@ -145,79 +86,19 @@ export function ConfirmTransactionModal({ )} {isSuccess && ( - - - {formatMessage(messages.transactionSubmitted)} - - - {formatMessage(messages.transactionSubmittedText)} - - - )} - {isError && ( - - - {formatMessage(messages.transactionError)} - - - {formatMessage(messages.transactionErrorText)} - - - - {formatMessage(messages.error)} - - {error.message} - - )} - - {isSuccess && ( - - { - const account = transactionData?.fromAccount; - if (!account) { - handleClose(); - return; - } - router.push(`/accounts/${account}`); - }} - > - {formatMessage(messages.viewAccountActivity)} - - { - const account = transactionData?.fromAccount; - const transactionHash = sentTransactionData?.hash; - if (!account || !transactionHash) { - handleClose(); - return; - } - router.push( - `/accounts/${account}/transaction/${transactionHash}`, - ); - }} - > - {formatMessage(messages.viewTransaction)} - - + )} {isError && ( - - - {formatMessage(messages.cancel)} - - - {formatMessage(messages.tryAgain)} - - + )} diff --git a/renderer/components/SendAssetsForm/SendAssetsForm.tsx b/renderer/components/SendAssetsForm/SendAssetsForm.tsx index 66e2999d..20d00179 100644 --- a/renderer/components/SendAssetsForm/SendAssetsForm.tsx +++ b/renderer/components/SendAssetsForm/SendAssetsForm.tsx @@ -193,7 +193,8 @@ export function SendAssetsFormContent({ !errors.memo && !errors.amount && !errors.toAccount && - !errors.assetId, + !errors.assetId && + !selectedAccount.isLedger, }, ); @@ -253,7 +254,7 @@ export function SendAssetsFormContent({ return; } - if (!estimatedFeesData) { + if (!estimatedFeesData && !selectedAccount.isLedger) { setError("root.serverError", { message: estimatedFeesError?.message ?? @@ -262,7 +263,7 @@ export function SendAssetsFormContent({ return; } - const fee = estimatedFeesData[data.fee]; + const fee = estimatedFeesData?.[data.fee] || null; setPendingTransaction({ fromAccount: data.fromAccount, @@ -335,38 +336,40 @@ export function SendAssetsFormContent({ )} /> - + )} void; + onSubmit: () => void; + isLoading?: boolean; + selectedAsset?: AssetOptionType; +}; + +export function ReviewTransaction({ + transactionData, + selectedAccount, + selectedAsset, + isLoading, + onClose, + onSubmit, +}: Props) { + const { formatMessage } = useIntl(); + + return ( + <> + + + {formatMessage(messages.confirmTransactionDetails)} + + + + + + {formatMessage(messages.from)} + + + {selectedAccount?.name} + {selectedAccount?.isLedger && } + + + + + {formatMessage(messages.to)} + {transactionData?.toAccount ?? ""} + + + + + {formatMessage(messages.amount)} + + + {CurrencyUtils.render( + transactionData.amount.toString(), + transactionData.assetId, + selectedAsset?.asset.verification, + )}{" "} + {selectedAsset?.assetName ?? formatMessage(messages.unknownAsset)} + + + + {transactionData?.fee && ( + + + {formatMessage(messages.fee)} + + + {formatOre(transactionData?.fee ?? 0)} $IRON + + + )} + + + + {formatMessage(messages.memo)} + + {transactionData?.memo} + + + + + + + {formatMessage(messages.cancelTransaction)} + + + {formatMessage(messages.confirmAndSend)} + + + + ); +} diff --git a/renderer/components/SendAssetsForm/SharedConfirmSteps/SubmissionError.tsx b/renderer/components/SendAssetsForm/SharedConfirmSteps/SubmissionError.tsx new file mode 100644 index 00000000..aef5a804 --- /dev/null +++ b/renderer/components/SendAssetsForm/SharedConfirmSteps/SubmissionError.tsx @@ -0,0 +1,71 @@ +import { ModalFooter, ModalBody, Heading, Text } from "@chakra-ui/react"; +import { defineMessages, useIntl } from "react-intl"; + +import { PillButton } from "@/ui/PillButton/PillButton"; + +const messages = defineMessages({ + transactionError: { + defaultMessage: "Transaction Error", + }, + transactionErrorText: { + defaultMessage: + "Something went wrong. Please review your transaction and try again.", + }, + error: { + defaultMessage: "Error", + }, + tryAgain: { + defaultMessage: "Try Again", + }, + cancel: { + defaultMessage: "Cancel", + }, +}); + +type Props = { + errorMessage: string; + isLoading: boolean; + handleClose: () => void; + handleSubmit: () => void; +}; + +export function SubmissionError({ + errorMessage, + isLoading, + handleClose, + handleSubmit, +}: Props) { + const { formatMessage } = useIntl(); + + return ( + <> + + + {formatMessage(messages.transactionError)} + + + {formatMessage(messages.transactionErrorText)} + + + + {formatMessage(messages.error)} + + {errorMessage} + + + + {formatMessage(messages.cancel)} + + + {formatMessage(messages.tryAgain)} + + + + ); +} diff --git a/renderer/components/SendAssetsForm/SharedConfirmSteps/TransactionSubmitted.tsx b/renderer/components/SendAssetsForm/SharedConfirmSteps/TransactionSubmitted.tsx new file mode 100644 index 00000000..b59d41a1 --- /dev/null +++ b/renderer/components/SendAssetsForm/SharedConfirmSteps/TransactionSubmitted.tsx @@ -0,0 +1,76 @@ +import { ModalFooter, ModalBody, Heading, Text } from "@chakra-ui/react"; +import { useRouter } from "next/router"; +import { defineMessages, useIntl } from "react-intl"; + +import { PillButton } from "@/ui/PillButton/PillButton"; + +const messages = defineMessages({ + transactionSubmitted: { + defaultMessage: "Transaction Submitted", + }, + transactionSubmittedText: { + defaultMessage: + "Your transaction has been submitted. It may take a few minutes until it is confirmed. This transaction will appear in your activity as pending until it is confirmed.", + }, + + viewAccountActivity: { + defaultMessage: "View Account Activity", + }, + viewTransaction: { + defaultMessage: "View Transaction", + }, +}); + +type Props = { + fromAccount: string; + transactionHash: string; + handleClose: () => void; +}; + +export function TransactionSubmitted({ + fromAccount, + transactionHash, + handleClose, +}: Props) { + const router = useRouter(); + const { formatMessage } = useIntl(); + return ( + <> + + + {formatMessage(messages.transactionSubmitted)} + + + {formatMessage(messages.transactionSubmittedText)} + + + + { + if (!fromAccount) { + handleClose(); + return; + } + router.push(`/accounts/${fromAccount}`); + }} + > + {formatMessage(messages.viewAccountActivity)} + + { + const account = fromAccount; + if (!account || !transactionHash) { + handleClose(); + return; + } + router.push(`/accounts/${account}/transaction/${transactionHash}`); + }} + > + {formatMessage(messages.viewTransaction)} + + + + ); +} diff --git a/renderer/components/SendAssetsForm/transactionSchema.ts b/renderer/components/SendAssetsForm/transactionSchema.ts index 8f633222..31e396f7 100644 --- a/renderer/components/SendAssetsForm/transactionSchema.ts +++ b/renderer/components/SendAssetsForm/transactionSchema.ts @@ -40,5 +40,5 @@ export const transactionSchema = z.object({ export type TransactionFormData = z.infer; export type TransactionData = Omit & { - fee: number; + fee: number | null; };