Skip to content

Commit

Permalink
fix(ui): show tx simulation (#147)
Browse files Browse the repository at this point in the history
Description
---
Simulation dialog didn't show the updated balances and shows 'failure'
status every time. Error wasn't informative.
Before:

[Screencast from 21.11.2024
10:55:22.webm](https://github.com/user-attachments/assets/c4005d53-77c1-43ed-9a06-cad1b0f27de7)

After:

[Screencast from 21.11.2024
10:49:15.webm](https://github.com/user-attachments/assets/ed212139-8c64-472b-bd86-6dc6fe47066e)


Motivation and Context
---
Solves #101 

How Has This Been Tested?
---

What process can a PR reviewer use to test or verify this change?
---

<!-- Checklist -->
<!-- 1. Is the title of your PR in the form that would make nice release
notes? The title, excluding the conventional commit
tag, will be included exactly as is in the CHANGELOG, so please think
about it carefully. -->


Breaking Changes
---

- [x] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [ ] Other - Please specify

<!-- Does this include a breaking change? If so, include this line as a
footer -->
<!-- BREAKING CHANGE: Description what the user should do, e.g. delete a
database, resync the chain -->
  • Loading branch information
karczuRF authored Nov 29, 2024
1 parent b9f82e9 commit e225e0d
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 59 deletions.
5 changes: 4 additions & 1 deletion locales/en/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@
"create-account": "New account",
"open-logs-directory": "Logs",
"simulation-status": "Simulation status",
"simulation-error-msg": "Simulation error"
"simulation-error-msg": "Simulation error message",
"tx-simulation-status": "Simulated transaction status",
"tx-simulation-error-msg": "Simulated tx error message",
"no-balance-update": "No balance updates available."
}
5 changes: 4 additions & 1 deletion locales/pl/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@
"create-account": "Dodaj nowe konto",
"open-logs-directory": "Logi",
"simulation-status": "Status symulacji",
"simulation-error-msg": "Błąd symulacji"
"simulation-error-msg": "Błąd symulacji",
"tx-simulation-status": "Status symulowanej transakcji",
"tx-simulation-error-msg": "Błąd symulowanej transakcji",
"no-balance-update": "Aktualizacja balansów niedostępna"
}
50 changes: 19 additions & 31 deletions src/components/TransactionConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { simulationActions } from "../store/simulation/simulation.slice"
import { BalanceUpdateView } from "./BalanceUpdate"
import { useTranslation } from "react-i18next"
import { ErrorSource } from "../store/error/error.types"
import { CallFunction, CallMethod } from "@tari-project/tarijs/dist/builders/types/Instruction"
import { resolveBackendErrorMessage } from "./ErrorSnackBar"
import { metadataSelector } from "../store/metadata/metadata.selector"
import { getFunctionOrMethod, getTransactionStatusName } from "../helpers/transaction"

const selectSimulationById = (state: RootState, id?: number) => (id ? simulationsSelectors.selectById(state, id) : null)

Expand Down Expand Up @@ -50,31 +50,6 @@ export const TransactionConfirmationModal: React.FC = () => {
)
}

interface InstructionWithArgs {
instructionName: string
args: any[]
}
// Function to get function or method fields
function getFunctionOrMethod(instructions: object[]): InstructionWithArgs[] {
let functionNames: InstructionWithArgs[] = []
instructions.forEach((instruction) => {
// Check if the instruction is an object and not a string
if (typeof instruction === "object" && instruction !== null) {
if ("CallFunction" in instruction) {
const callFunction = instruction as CallFunction
functionNames.push({
instructionName: callFunction.CallFunction.function,
args: callFunction.CallFunction.args,
})
} else if ("CallMethod" in instruction) {
const callMethod = instruction as CallMethod
functionNames.push({ instructionName: callMethod.CallMethod.method, args: callMethod.CallMethod.args })
}
}
})
return functionNames
}

return (
<Dialog open={!!transaction} maxWidth="sm" fullWidth>
<DialogTitle textAlign="center">{t("transaction-confirmation", { ns: "components" })}:</DialogTitle>
Expand All @@ -88,7 +63,7 @@ export const TransactionConfirmationModal: React.FC = () => {
{getFunctionOrMethod(arg.instructions)
.flatMap((i) => i.instructionName + " with args: " + i.args)
.map((instruction, index) => (
<div key={index}>{instruction}</div> // Using <div> or <span> to wrap each instruction
<div key={index}>{instruction}</div>
))}
</DialogContentText>
))}
Expand All @@ -102,11 +77,24 @@ export const TransactionConfirmationModal: React.FC = () => {
</DialogContentText>
)}
<DialogContentText>
{t("balance-updates", { ns: "components" })}:
{simulation?.balanceUpdates?.map((update) => (
<BalanceUpdateView key={update.vaultAddress} {...update} />
))}
{t("balance-updates", { ns: "components" })}:{" "}
{Array.isArray(simulation?.balanceUpdates) && simulation.balanceUpdates.length > 0 ? (
simulation.balanceUpdates.map((update) => <BalanceUpdateView key={update.vaultAddress} {...update} />)
) : (
<span>{t("no-balance-update", { ns: "components" })}</span>
)}
</DialogContentText>
<DialogContentText>
{t("tx-simulation-status", { ns: "components" })}: {getTransactionStatusName(simulation?.transaction?.status)}
</DialogContentText>
{simulation?.transaction.errorMsg && (
<DialogContentText>
{t("tx-simulation-error-msg", { ns: "components" })}:{" "}
{typeof simulation?.transaction?.errorMsg === "string"
? simulation.transaction.errorMsg
: JSON.stringify(simulation?.transaction?.errorMsg)}
</DialogContentText>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} variant="contained">
Expand Down
31 changes: 31 additions & 0 deletions src/helpers/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { TransactionStatus } from "@tari-project/tarijs"
import { CallFunction, CallMethod } from "@tari-project/tarijs/dist/builders/types/Instruction"

interface InstructionWithArgs {
instructionName: string
args: any[]
}

export function getFunctionOrMethod(instructions: object[]): InstructionWithArgs[] {
let functionNames: InstructionWithArgs[] = []
instructions.forEach((instruction) => {
if (typeof instruction === "object" && instruction !== null) {
if ("CallFunction" in instruction) {
const callFunction = instruction as CallFunction
functionNames.push({
instructionName: callFunction.CallFunction.function,
args: callFunction.CallFunction.args,
})
} else if ("CallMethod" in instruction) {
const callMethod = instruction as CallMethod
functionNames.push({ instructionName: callMethod.CallMethod.method, args: callMethod.CallMethod.args })
}
}
})
return functionNames
}

export function getTransactionStatusName(status?: TransactionStatus): string {
if (!status) return ""
return TransactionStatus[status]
}
76 changes: 58 additions & 18 deletions src/store/provider/provider.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Transaction, TUProviderMethod } from "../transaction/transaction.types"
import { errorActions } from "../error/error.slice"
import { RootState } from "../store"
import { providerSelector } from "./provider.selector"
import { SubmitTransactionRequest } from "@tari-project/tarijs"
import { SubmitTransactionRequest, TransactionStatus } from "@tari-project/tarijs"
import { invoke } from "@tauri-apps/api/core"
import {
SubstateDiff,
Expand All @@ -20,9 +20,10 @@ import {
ResourceContainer,
ResourceAddress,
Amount,
RejectReason,
} from "@tari-project/typescript-bindings"
import { AccountsGetBalancesResponse } from "@tari-project/wallet_jrpc_client"
import { BalanceUpdate } from "../simulation/simulation.types"
import { BalanceUpdate, TxSimulation } from "../simulation/simulation.types"
import { ErrorSource } from "../error/error.types"

let handleMessage: typeof window.postMessage
Expand All @@ -31,6 +32,10 @@ const isAccept = (result: TransactionResult): result is { Accept: SubstateDiff }
return "Accept" in result
}

const isReject = (result: TransactionResult): result is { Reject: RejectReason } => {
return "Reject" in result
}

const isVaultId = (substateId: SubstateId): substateId is { Vault: VaultId } => {
return "Vault" in substateId
}
Expand Down Expand Up @@ -66,44 +71,78 @@ export const initializeAction = () => ({
}

const { methodName, args, id } = event.data
const _method = methodName as TUProviderMethod
const runSimulation = async () => {
const method = methodName as TUProviderMethod
// tx simulation
const runSimulation = async (): Promise<{ balanceUpdates: BalanceUpdate[]; txSimulation: TxSimulation }> => {
if (methodName !== "submitTransaction") {
return []
return {
balanceUpdates: [],
txSimulation: {
status: TransactionStatus.InvalidTransaction,
errorMsg: `Simulation for ${methodName} not supported`,
},
}
}
const transactionReq: SubmitTransactionRequest = { ...args[0], is_dry_run: true }
const tx = await provider.runOne(_method, [transactionReq])
const tx = await provider.runOne(method, [transactionReq])

await provider.client.waitForTransactionResult({
transaction_id: tx.transaction_id,
timeout_secs: 10,
})
const txReceipt = await provider.getTransactionResult(tx.transaction_id)
const txResult = txReceipt.result as FinalizeResult | null
if (!txResult?.result)
return {
balanceUpdates: [],
txSimulation: {
status: TransactionStatus.InvalidTransaction,
errorMsg: "Transaction result undefined",
},
}

const walletBalances: AccountsGetBalancesResponse = await invoke("get_balances", {})
const txResult = txReceipt.result as FinalizeResult
if (!isAccept(txResult.result)) return []
const txSimulation: TxSimulation = {
status: txReceipt.status,
errorMsg: isReject(txResult?.result) ? (txResult.result.Reject as string) : "",
}

const { up_substates } = txResult.result.Accept
if (!isAccept(txResult.result)) return { balanceUpdates: [], txSimulation }

let walletBalances: AccountsGetBalancesResponse

try {
walletBalances = await invoke("get_balances", {})
} catch (error) {
console.error(error)
const e = typeof error === "string" ? error : "Get balances error"
dispatch(errorActions.showError({ message: e, errorSource: ErrorSource.FRONTEND }))
}

const { up_substates } = txResult.result.Accept
const balanceUpdates: BalanceUpdate[] = up_substates
.map((upSubstate) => {
const [substateId, { substate }] = upSubstate
if (!isVaultId(substateId) || !isVaultSubstate(substate)) return
if (!isFungible(substate.Vault.resource_container)) return
if (!isVaultId(substateId) || !isVaultSubstate(substate)) return undefined
if (!isFungible(substate.Vault.resource_container)) return undefined
const userBalance = walletBalances.balances.find((balance) => {
if (!isVaultId(balance.vault_address)) return false
return balance.vault_address.Vault === substateId.Vault
})
if (!userBalance) return
if (!userBalance) return undefined
return {
vaultAddress: substateId.Vault,
tokenSymbol: userBalance.token_symbol || "",
currentBalance: userBalance.balance,
newBalance: substate.Vault.resource_container.Fungible.amount,
}
})
.filter((vault) => vault !== undefined)
return balanceUpdates
.filter((vault): vault is BalanceUpdate => vault !== undefined)
return { balanceUpdates, txSimulation }
}
// tx submit
const submit = async () => {
try {
const result = await provider.runOne(_method, args)
const result = await provider.runOne(method, args)
if (event.source) {
event.source.postMessage({ id, result, type: "provider-call" }, { targetOrigin: event.origin })
}
Expand All @@ -113,6 +152,7 @@ export const initializeAction = () => ({
dispatch(errorActions.showError({ message: e, errorSource: ErrorSource.FRONTEND }))
}
}
// tx cancel
const cancel = async () => {
if (event.source) {
event.source.postMessage(
Expand All @@ -126,11 +166,11 @@ export const initializeAction = () => ({
cancel,
runSimulation,
status: "pending",
methodName: _method,
methodName: method,
args,
id,
}
if (_method === "submitTransaction") {
if (method === "submitTransaction") {
dispatch(transactionActions.addTransaction({ transaction }))
} else {
dispatch(transactionActions.sendTransactionRequest({ transaction }))
Expand Down
27 changes: 23 additions & 4 deletions src/store/simulation/simulation.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ListenerEffectAPI, PayloadAction, ThunkDispatch, UnknownAction } from "
import { simulationActions } from "./simulation.slice"
import { SimulationRequestPayload } from "./simulation.types"
import { RootState } from "../store"
import { TransactionStatus } from "@tari-project/tarijs"

export const runTransactionSimulationAction = () => ({
actionCreator: simulationActions.runSimulationRequest,
Expand All @@ -16,16 +17,34 @@ export const runTransactionSimulationAction = () => ({
const { runSimulation } = state.transaction.entities[transactionId]

if (!provider) {
dispatch(simulationActions.runSimulationFailure({ transactionId, errorMsg: "Provider not found" }))
dispatch(
simulationActions.runSimulationFailure({
transactionId,
errorMsg: "Provider not found",
transaction: { status: TransactionStatus.InvalidTransaction, errorMsg: "Provider not found" },
})
)
return
}

try {
const balanceUpdates = await runSimulation()
dispatch(simulationActions.runSimulationSuccess({ transactionId, balanceUpdates }))
const simulationResult = await runSimulation()
dispatch(
simulationActions.runSimulationSuccess({
transactionId,
balanceUpdates: simulationResult.balanceUpdates,
transaction: simulationResult.txSimulation,
})
)
} catch (error) {
console.error(error)
dispatch(simulationActions.runSimulationFailure({ transactionId, errorMsg: String(error) }))
dispatch(
simulationActions.runSimulationFailure({
transactionId,
errorMsg: String(error),
transaction: { status: TransactionStatus.Rejected, errorMsg: String(error) },
})
)
}
},
})
23 changes: 21 additions & 2 deletions src/store/simulation/simulation.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "./simulation.types"
import { listenerMiddleware } from "../store.listener"
import { runTransactionSimulationAction } from "./simulation.action"
import { TransactionStatus } from "@tari-project/tarijs"

export const simulationAdapter = createEntityAdapter({
selectId: (simulation: Simulation) => simulation.transactionId,
Expand All @@ -23,18 +24,36 @@ const simulationSlice = createSlice({
status: "pending",
balanceUpdates: [],
errorMsg: "",
transaction: {
status: TransactionStatus.DryRun,
errorMsg: "",
},
})
},
runSimulationSuccess: (state, action: PayloadAction<SimulationSuccessPayload>) => {
simulationAdapter.updateOne(state, {
id: action.payload.transactionId,
changes: { status: "success", balanceUpdates: action.payload.balanceUpdates },
changes: {
status: "success",
balanceUpdates: action.payload.balanceUpdates,
transaction: {
errorMsg: action.payload.transaction.errorMsg,
status: action.payload.transaction.status,
},
},
})
},
runSimulationFailure: (state, action: PayloadAction<SimulationFailurePayload>) => {
simulationAdapter.updateOne(state, {
id: action.payload.transactionId,
changes: { status: "failure", errorMsg: action.payload.errorMsg },
changes: {
status: "failure",
errorMsg: action.payload.errorMsg,
transaction: {
errorMsg: action.payload.transaction.errorMsg,
status: action.payload.transaction.status,
},
},
})
},
},
Expand Down
Loading

0 comments on commit e225e0d

Please sign in to comment.