polkadot-js/apps

View on GitHub
packages/page-parachains/src/useWinningData.ts

Summary

Maintainability
A
3 hrs
Test Coverage
// Copyright 2017-2024 @polkadot/app-parachains authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Option, StorageKey } from '@polkadot/types';
import type { BlockNumber, WinningData } from '@polkadot/types/interfaces';
import type { AuctionInfo, WinnerData, Winning } from './types.js';

import { useEffect, useRef, useState } from 'react';

import { createNamedHook, useApi, useBestNumber, useCall, useEventTrigger, useIsMountedRef } from '@polkadot/react-hooks';
import { BN, BN_ONE, BN_ZERO, u8aEq } from '@polkadot/util';

import { CROWD_PREFIX } from './constants.js';
import { useLeaseRanges } from './useLeaseRanges.js';

const FIRST_PARAM = [0];

function isNewWinners (a: WinnerData[], b: WinnerData[]): boolean {
  return JSON.stringify({ w: a }) !== JSON.stringify({ w: b });
}

function isNewOrdering (a: WinnerData[], b: WinnerData[]): boolean {
  return a.length !== b.length ||
    a.some(({ firstSlot, lastSlot, paraId }, index) =>
      !paraId.eq(b[index].paraId) ||
      !firstSlot.eq(b[index].firstSlot) ||
      !lastSlot.eq(b[index].lastSlot)
    );
}

function extractWinners (ranges: [number, number][], auctionInfo: AuctionInfo, optData: Option<WinningData>): WinnerData[] {
  return optData.isNone
    ? []
    : optData.unwrap().reduce<WinnerData[]>((winners, optEntry, index): WinnerData[] => {
      if (optEntry.isSome) {
        const [accountId, paraId, value] = optEntry.unwrap();
        const period = auctionInfo.leasePeriod || BN_ZERO;
        const [first, last] = ranges[index];

        winners.push({
          accountId: accountId.toString(),
          firstSlot: period.addn(first),
          isCrowdloan: u8aEq(CROWD_PREFIX, accountId.subarray(0, CROWD_PREFIX.length)),
          key: paraId.toString(),
          lastSlot: period.addn(last),
          paraId,
          value
        });
      }

      return winners;
    }, []);
}

function createWinning ({ endBlock }: AuctionInfo, blockOffset: BN | null | undefined, winners: WinnerData[]): Winning {
  return {
    blockNumber: endBlock && blockOffset
      ? blockOffset.add(endBlock)
      : blockOffset || BN_ZERO,
    blockOffset: blockOffset || BN_ZERO,
    total: winners.reduce((total, { value }) => total.iadd(value), new BN(0)),
    winners
  };
}

function extractData (ranges: [number, number][], auctionInfo: AuctionInfo, values: [StorageKey<[BlockNumber]>, Option<WinningData>][]): Winning[] {
  return values
    .sort(([{ args: [a] }], [{ args: [b] }]) => a.cmp(b))
    .reduce((all: Winning[], [{ args: [blockOffset] }, optData]): Winning[] => {
      const winners = extractWinners(ranges, auctionInfo, optData);

      winners.length && (
        all.length === 0 ||
        isNewWinners(winners, all[all.length - 1].winners)
      ) && all.push(createWinning(auctionInfo, blockOffset, winners));

      return all;
    }, [])
    .reverse();
}

function mergeCurrent (ranges: [number, number][], auctionInfo: AuctionInfo, prev: Winning[] | undefined, optCurrent: Option<WinningData>, blockOffset: BN): Winning[] | undefined {
  const current = createWinning(auctionInfo, blockOffset, extractWinners(ranges, auctionInfo, optCurrent));

  if (current.winners.length) {
    if (!prev?.length) {
      return [current];
    }

    if (isNewWinners(current.winners, prev[0].winners)) {
      if (isNewOrdering(current.winners, prev[0].winners)) {
        return [current, ...prev];
      }

      prev[0] = current;

      return [...prev];
    }
  }

  return prev;
}

function mergeFirst (ranges: [number, number][], auctionInfo: AuctionInfo, prev: Winning[] | undefined, optFirstData: Option<WinningData>): Winning[] | undefined {
  if (prev && prev.length <= 1) {
    const updated: Winning[] = prev || [];
    const firstEntry = createWinning(auctionInfo, null, extractWinners(ranges, auctionInfo, optFirstData));

    if (!firstEntry.winners.length) {
      return updated;
    } else if (!updated.length) {
      return [firstEntry];
    }

    updated[updated.length - 1] = firstEntry;

    return updated.slice();
  }

  return prev;
}

function useWinningDataImpl (auctionInfo?: AuctionInfo): Winning[] | undefined {
  const { api } = useApi();
  const mountedRef = useIsMountedRef();
  const ranges = useLeaseRanges();
  const [result, setResult] = useState<Winning[] | undefined>();
  const bestNumber = useBestNumber();
  const trigger = useEventTrigger([api.events.auctions?.BidAccepted]);
  const triggerRef = useRef(trigger);
  const initialEntries = useCall<[StorageKey<[BlockNumber]>, Option<WinningData>][]>(api.query.auctions?.winning.entries);
  const optFirstData = useCall<Option<WinningData>>(api.query.auctions?.winning, FIRST_PARAM);

  // should be fired once, all entries as an initial round
  useEffect((): void => {
    mountedRef.current && auctionInfo && initialEntries && setResult(
      extractData(ranges, auctionInfo, initialEntries)
    );
  }, [auctionInfo, initialEntries, mountedRef, ranges]);

  // when block 0 changes, update (typically in non-ending-period, static otherwise)
  useEffect((): void => {
    mountedRef.current && auctionInfo && optFirstData && setResult((prev) =>
      mergeFirst(ranges, auctionInfo, prev, optFirstData)
    );
  }, [auctionInfo, optFirstData, mountedRef, ranges]);

  // on a bid event, get the new entry (assuming the event really triggered, i.e. not just a block)
  // and add it to the list when not duplicated. Additionally we cleanup after ourselves when endBlock
  // gets cleared
  useEffect((): void => {
    if (auctionInfo?.endBlock && bestNumber && bestNumber.gt(auctionInfo.endBlock) && triggerRef.current !== trigger) {
      const blockOffset = bestNumber.sub(auctionInfo.endBlock).iadd(BN_ONE);

      triggerRef.current = trigger;

      api.query.auctions
        ?.winning<Option<WinningData>>(blockOffset)
        .then((optCurrent) =>
          mountedRef.current && setResult((prev) =>
            mergeCurrent(ranges, auctionInfo, prev, optCurrent, blockOffset)
          )
        )
        .catch(console.error);
    }
  }, [api, bestNumber, auctionInfo, mountedRef, ranges, trigger, triggerRef]);

  return result;
}

export default createNamedHook('useWinningData', useWinningDataImpl);