cyb/src/features/ibc-history/historyContext.tsx

/* eslint-disable */

import { Coin } from '@cosmjs/launchpad';
import { SigningStargateClient } from '@cosmjs/stargate';
import { parseRawLog } from '@cosmjs/stargate/build/logs';
import { SigningCyberClient } from '@cybercongress/cyber-js';
import { PromiseExtended } from 'dexie';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useAppSelector } from 'src/redux/hooks';
import { RootState } from 'src/redux/store';
import { Option } from 'src/types';
import { AccountValue } from 'src/types/defaultAccount';
import networkList from 'src/utils/networkListIbc';
import { db as dbIbcHistory } from './db';
import { HistoriesItem, StatusTx } from './HistoriesItem';
import PollingStatusSubscription from './polling-status-subscription';
import TracerTx from './tx/TracerTx';
import parseEvents from './utils';

const findRpc = (chainId: string): Option<string> => {
  if (networkList[chainId]) {
    return networkList[chainId].rpc;
  }

  return undefined;
};

type HistoryContext = {
  ibcHistory: Option<HistoriesItem[]>;
  changeHistory: () => void;
  addHistoriesItem: (itemHistories: HistoriesItem) => void;
  pingTxsIbc: (
    cliet: SigningStargateClient | SigningCyberClient,
    uncommitedTx: UncommitedTx
  ) => void;
  useGetHistoriesItems: () => Option<PromiseExtended<HistoriesItem[]>>;
  updateStatusByTxHash: (txHash: string, status: StatusTx) => void;
  traceHistoryStatus: (item: HistoriesItem) => Promise<StatusTx>;
};

const valueContext = {
  ibcHistory: undefined,
  changeHistory: () => {},
  addHistoriesItem: () => {},
  pingTxsIbc: () => {},
  useGetHistoriesItems: () => {},
  updateStatusByTxHash: () => {},
  traceHistoryStatus: () => {},
};

export const HistoryContext = React.createContext<HistoryContext>(valueContext);

export const useIbcHistory = () => {
  const context = useContext(HistoryContext);

  return context;
};

const _historiesItemsByAddress = (addressActive: AccountValue | null) => {
  if (addressActive) {
    return dbIbcHistory.historiesItems.where({ address: addressActive.bech32 }).toArray();
  }
  return [];
};

type UncommitedTx = {
  txHash: string;
  address: string;
  sourceChainId: string;
  destChainId: string;
  sender: string;
  recipient: string;
  createdAt: number;
  amount: Coin;
};

const blockSubscriberMap: Map<string, PollingStatusSubscription> = new Map();

function HistoryContextProvider({ children }: { children: React.ReactNode }) {
  const [ibcHistory, setIbcHistory] = useState<Option<HistoriesItem[]>>(undefined);
  const { defaultAccount } = useAppSelector((state: RootState) => state.pocket);
  const [_update, setUpdate] = useState(0);
  const addressActive = defaultAccount.account?.cyber || undefined;

  function getBlockSubscriber(chainId: string): PollingStatusSubscription {
    if (!blockSubscriberMap.has(chainId)) {
      const chainInfo = findRpc(chainId);
      if (chainInfo) {
        blockSubscriberMap.set(chainId, new PollingStatusSubscription(chainInfo));
      }
    }

    // eslint-disable-next-line
    return blockSubscriberMap.get(chainId)!;
  }

  function traceTimeoutTimestamp(
    statusSubscriber: PollingStatusSubscription,
    timeoutTimestamp: string
  ): {
    unsubscriber: () => void;
    promise: Promise<void>;
  } {
    let resolver: (value: PromiseLike<void> | void) => void;
    const promise = new Promise<void>((resolve) => {
      resolver = resolve;
    });
    const unsubscriber = statusSubscriber.subscribe((data) => {
      const blockTime = data?.result?.sync_info?.latest_block_time;
      if (
        blockTime &&
        new Date(blockTime).getTime() > Math.floor(parseInt(timeoutTimestamp, 10) / 1000000)
      ) {
        resolver();
        return;
      }
    });

    return {
      unsubscriber,
      promise,
    };
  }

  const traceHistoryStatus = useCallback(async (item: HistoriesItem): Promise<StatusTx> => {
    if (item.status === StatusTx.COMPLETE || item.status === StatusTx.REFUNDED) {
      return item.status;
    }

    // Check if packet was acknowledged on source chain (bostrom) via WebSocket.
    // This avoids CORS/connectivity issues with destination chain RPCs.
    // acknowledge_packet on source = recv_packet succeeded on destination.
    const sourceRpc = findRpc(item.sourceChainId);
    if (sourceRpc) {
      const ackTracer = new TracerTx(sourceRpc, '/websocket');
      try {
        const result = await ackTracer.traceTx(
          {
            'acknowledge_packet.packet_src_channel': item.sourceChannelId,
            'acknowledge_packet.packet_sequence': item.sequence,
          },
          { timeoutMs: 20000, connectionTimeoutMs: 10000 }
        );
        ackTracer.close();
        if (result) {
          return StatusTx.COMPLETE;
        }
      } catch {
        ackTracer.close();
        // continue with normal flow
      }
    }

    if (item.status === StatusTx.TIMEOUT) {
      const sourceChainId = findRpc(item.sourceChainId);
      if (!sourceChainId) return item.status;

      const txTracer = new TracerTx(sourceChainId, '/websocket');

      try {
        await txTracer.traceTx({
          'timeout_packet.packet_src_channel': item.sourceChannelId,
          'timeout_packet.packet_sequence': item.sequence,
        });
        txTracer.close();
        return StatusTx.REFUNDED;
      } catch {
        txTracer.close();
        return item.status;
      }
    }

    const blockSubscriber = getBlockSubscriber(item.destChainId);

    let timeoutUnsubscriber: (() => void) | undefined;

    const promises: Promise<any>[] = [];

    if (item.timeoutTimestamp && item.timeoutTimestamp !== '0') {
      promises.push(
        (async () => {
          const { promise, unsubscriber } = traceTimeoutTimestamp(
            blockSubscriber,
            // eslint-disable-next-line
            item.timeoutTimestamp!
          );
          timeoutUnsubscriber = unsubscriber;
          await promise;

          // Even though the block is reached to the timeout height,
          // the receiving packet event could be delivered before the block timeout if the network connection is unstable.
          // This it not the chain issue itself, just the issue from the frontend, it is impossible to ensure the network status entirely.
          // To reduce this problem, wait 30 seconds more for relay to complete (relay runs every 10s + indexing time).
          await new Promise((resolve) => {
            setTimeout(resolve, 30000);
          });
        })()
      );
    }

    const destChainId = findRpc(item.destChainId);

    if (!destChainId) return item.status;

    const txTracer = new TracerTx(destChainId, '/websocket');

    promises.push(
      txTracer.traceTx({
        'recv_packet.packet_dst_channel': item.destChannelId,
        'recv_packet.packet_sequence': item.sequence,
      })
    );

    try {
      const result = await Promise.race(promises);

      if (timeoutUnsubscriber) {
        timeoutUnsubscriber();
      }

      txTracer.close();

      if (result) {
        return StatusTx.COMPLETE;
      }

      return StatusTx.TIMEOUT;
    } catch {
      if (timeoutUnsubscriber) {
        timeoutUnsubscriber();
      }
      txTracer.close();
      return item.status;
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const useGetHistoriesItems = useCallback(() => {
    if (addressActive) {
      return dbIbcHistory.historiesItems
        .where({
          address: addressActive.bech32,
        })
        .toArray();
    }
    return undefined;
  }, [addressActive]);

  useEffect(() => {
    const getItem = async () => {
      if (addressActive) {
        const response = await dbIbcHistory.historiesItems
          .where({
            address: addressActive.bech32,
          })
          .toArray();
        if (response) {
          setIbcHistory(response.reverse());
        }
      }
    };
    getItem();
  }, [addressActive, _update]);

  const updateStatusByTxHash = useCallback(async (txHash: string, status: StatusTx) => {
    const itemCollection = dbIbcHistory.historiesItems.where({ txHash });
    const itemByTxHash = await itemCollection.toArray();
    if (itemByTxHash && itemByTxHash[0].status !== status) {
      itemCollection.modify({ status });
    }
  }, []);

  // Trace pending/timeout items on initial load (only when address changes)
  useEffect(() => {
    const tracePendingItems = async () => {
      if (addressActive) {
        const items = await dbIbcHistory.historiesItems
          .where({
            address: addressActive.bech32,
          })
          .toArray();

        items.forEach((item) => {
          if (item.status === StatusTx.PENDING || item.status === StatusTx.TIMEOUT) {
            traceHistoryStatus(item).then((newStatus) => {
              if (newStatus !== item.status) {
                updateStatusByTxHash(item.txHash, newStatus);
                setUpdate((i) => i + 1);
              }
            });
          }
        });
      }
    };
    tracePendingItems();
  }, [addressActive, traceHistoryStatus, updateStatusByTxHash]);

  const pingTxsIbc = async (
    cliet: SigningStargateClient | SigningCyberClient,
    uncommitedTx: UncommitedTx
  ) => {
    const MAX_RETRIES = 40; // ~60 seconds
    let retryCount = 0;

    const ping = async () => {
      const response = await cliet.getTx(uncommitedTx.txHash);
      if (response) {
        // Try parsing rawLog first (works for bostrom/older Cosmos SDK)
        let dataFromEvent = null;
        if (response.rawLog) {
          try {
            const result = parseRawLog(response.rawLog);
            dataFromEvent = parseEvents(result);
          } catch {
            // rawLog parsing failed, try events fallback
          }
        }

        // Fallback: parse events directly (newer Cosmos SDK like Osmosis)
        if (!dataFromEvent && response.events) {
          dataFromEvent = parseEvents([{ events: response.events, msg_index: 0, log: '' }]);
        }

        if (dataFromEvent) {
          const itemHistories = { ...uncommitedTx, ...dataFromEvent };
          addHistoriesItem({
            ...itemHistories,
            status: StatusTx.PENDING,
          });
        }
        return;
      }
      retryCount += 1;
      if (retryCount >= MAX_RETRIES) {
        console.warn(`pingTxsIbc: max retries reached for tx ${uncommitedTx.txHash}`);
        return;
      }
      setTimeout(ping, 1500);
    };
    ping();
  };

  const addHistoriesItem = async (itemHistories: HistoriesItem) => {
    await dbIbcHistory.historiesItems.add(itemHistories);
    setUpdate((item) => item + 1);

    // Automatically start tracing pending items
    if (itemHistories.status === StatusTx.PENDING) {
      traceHistoryStatus(itemHistories).then((newStatus) => {
        if (newStatus !== itemHistories.status) {
          updateStatusByTxHash(itemHistories.txHash, newStatus);
          setUpdate((item) => item + 1);
        }
      });
    }
  };

  const changeHistory = () => {
    // console.log('history', history);
    // setValue((item) => ({ ...item, history: { ...item.history, history } }));
  };

  return (
    <HistoryContext.Provider
      value={{
        ibcHistory,
        changeHistory,
        addHistoriesItem,
        pingTxsIbc,
        useGetHistoriesItems,
        updateStatusByTxHash,
        traceHistoryStatus,
      }}
    >
      {children}
    </HistoryContext.Provider>
  );
}

export default HistoryContextProvider;

Synonyms

pussy-ts/src/services/ibc-history/historyContext.tsx

Neighbours