import produce from 'immer'; import type { Transaction } from './connectors'; import { isWithdrawTransaction, isOrderSubmissionTransaction, isOrderCancellationTransaction, isOrderAmendmentTransaction, isBatchMarketInstructionsTransaction, isTransferTransaction, isStopOrdersSubmissionTransaction, isStopOrdersCancellationTransaction, } from './connectors'; import { determineId } from './utils'; import { create } from 'zustand'; import type { TransactionEventFieldsFragment, WithdrawalBusEventFieldsFragment, OrderTxUpdateFieldsFragment, } from './__generated__/TransactionResult'; import type { WithdrawalApprovalQuery } from './__generated__/WithdrawalApproval'; import { subscribeWithSelector } from 'zustand/middleware'; import type { VegaTxState } from './types'; import { VegaTxStatus } from './types'; export interface VegaStoredTxState extends VegaTxState { id: number; createdAt: Date; updatedAt: Date; body: Transaction; transactionResult?: TransactionEventFieldsFragment; withdrawal?: WithdrawalBusEventFieldsFragment; withdrawalApproval?: WithdrawalApprovalQuery['erc20WithdrawalApproval']; order?: OrderTxUpdateFieldsFragment; } export interface VegaTransactionStore { transactions: (VegaStoredTxState | undefined)[]; create: (tx: Transaction, order?: OrderTxUpdateFieldsFragment) => number; update: ( index: number, update: Partial< Pick > ) => void; dismiss: (index: number) => void; delete: (index: number) => void; updateWithdrawal: ( withdrawal: NonNullable, withdrawalApproval: NonNullable ) => void; updateOrder: (order: OrderTxUpdateFieldsFragment) => void; updateTransactionResult: ( transactionResult: TransactionEventFieldsFragment ) => void; } export const useVegaTransactionStore = create()( subscribeWithSelector((set, get) => ({ transactions: [] as VegaStoredTxState[], create: (body: Transaction, order?: OrderTxUpdateFieldsFragment) => { const transactions = get().transactions; const now = new Date(); const transaction: VegaStoredTxState = { id: transactions.length, createdAt: now, updatedAt: now, body, error: null, txHash: null, signature: null, status: VegaTxStatus.Requested, dialogOpen: true, order, }; set({ transactions: transactions.concat(transaction) }); return transaction.id; }, update: (index: number, update: Partial) => { set( produce((state: VegaTransactionStore) => { const transaction = state.transactions[index]; if (transaction) { Object.assign(transaction, update); transaction.dialogOpen = true; transaction.updatedAt = new Date(); } }) ); }, dismiss: (index: number) => { set( produce((state: VegaTransactionStore) => { const transaction = state.transactions[index]; if (transaction) { transaction.dialogOpen = false; transaction.updatedAt = new Date(); } }) ); }, delete: (index: number) => { set( produce((state: VegaTransactionStore) => { delete state.transactions[index]; }) ); }, updateWithdrawal: ( withdrawal: NonNullable, withdrawalApproval: NonNullable ) => { set( produce((state: VegaTransactionStore) => { const transaction = state.transactions.find( (transaction) => transaction && transaction.status === VegaTxStatus.Pending && transaction.signature && isWithdrawTransaction(transaction?.body) && withdrawal.id === determineId(transaction.signature) ); if (transaction) { transaction.withdrawal = withdrawal; transaction.withdrawalApproval = withdrawalApproval; transaction.status = VegaTxStatus.Complete; transaction.dialogOpen = true; transaction.updatedAt = new Date(); } }) ); }, updateOrder: (order) => { set( produce((state: VegaTransactionStore) => { const transaction = state.transactions.find((transaction) => { if (!transaction || transaction.status !== VegaTxStatus.Pending) { return false; } if ( isOrderSubmissionTransaction(transaction?.body) && transaction.signature && order.id === determineId(transaction.signature) ) { return true; } if ( isOrderCancellationTransaction(transaction?.body) && order.id === transaction.body.orderCancellation.orderId ) { return true; } if ( isOrderAmendmentTransaction(transaction?.body) && order.id === transaction.body.orderAmendment.orderId ) { return true; } if ( isBatchMarketInstructionsTransaction(transaction?.body) && transaction.signature && order.id === determineId(transaction.signature) ) { return true; } return false; }); if (transaction) { // TODO: handle multiple orders from batch market instructions // Note: If multiple orders are submitted the first order ID is determined by hashing the signature of the transaction // (see determineId function). For each subsequent order's ID, a hash of the previous orders ID is used transaction.order = isOrderAmendmentTransaction(transaction?.body) && transaction.order ? transaction.order // the transaction.order would be the original order amended : order; transaction.status = VegaTxStatus.Complete; transaction.dialogOpen = true; transaction.updatedAt = new Date(); } }) ); }, updateTransactionResult: ( transactionResult: TransactionEventFieldsFragment ) => { set( produce((state: VegaTransactionStore) => { const transaction = state.transactions.find( (transaction) => transaction?.txHash && transaction.txHash.toLowerCase() === transactionResult.hash.toLowerCase() ); if (transaction) { transaction.transactionResult = transactionResult; const isConfirmedOrderCancellation = isOrderCancellationTransaction(transaction.body) && !transaction.body.orderCancellation.orderId; const isConfirmedTransfer = isTransferTransaction(transaction.body); const isConfirmedStopOrderCancellation = isStopOrdersCancellationTransaction(transaction.body); const isConfirmedStopOrderSubmission = isStopOrdersSubmissionTransaction(transaction.body); if ( (isConfirmedOrderCancellation || isConfirmedTransfer || isConfirmedStopOrderCancellation || isConfirmedStopOrderSubmission) && !transactionResult.error && transactionResult.status ) { transaction.status = VegaTxStatus.Complete; } transaction.dialogOpen = true; transaction.updatedAt = new Date(); } }) ); }, })) );