Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ui): show tx simulation #147

Merged
merged 5 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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