From bfb63843c51706003a8f66758b3849d4e21a4bbd Mon Sep 17 00:00:00 2001 From: Velenir Date: Wed, 9 Dec 2020 14:28:50 +0300 Subject: [PATCH] Orders state (#42) * copy state/index to custom * create sample of Operator actions * create sample of Operator reducer * add operator reducer to state * create sample of Operator hooks * changeto absolute import { AppDispatch, AppState } from 'state/' * change to absolute import store from 'state/' * minor organisational changes * from 'state/' -> from 'state' * special var for UNISWAP_REDUCERS * simplify types * fix import error * create some actions for Orders * create some reducers for Orders * hook up Orders reducer * create some hooks for Orders * differentiate orders by chainId * add clearOrders action * expand Orders types * fix orders/hooks * add useClearOrders hook * wrap hook function returns in useCallback * scaffold orders/updater * don't use const enum * fix build * remove unnecessary stuff * setup mock event watcher * extend types * use mock EventUpdater * change type names * try different decoding methods * improve event watcher * better types * normalize decoder interface * differentiate Orders by status * fullfillOrder action + reducer * hooks for different Orders by state * useFulfillOrder hook * persist state.orders in localStorage * separate pending and fulfilled orders more * prefill optional state for convenience * rename action to explicitly say pendingOrder * refactorhooks fornew state shape * rename some types * fix build * remove updater for now * move isTruthy to utils * remove Updater reference * OrderKind enum in line with api * more comments for types * OrderStatus enum values as strings,for readability when serialized * add Order.summary prop Co-authored-by: David Sato --- src/custom/state/index.ts | 6 +- src/custom/state/orders/actions.ts | 57 ++++++++++++++++++ src/custom/state/orders/hooks.ts | 97 ++++++++++++++++++++++++++++++ src/custom/state/orders/reducer.ts | 93 ++++++++++++++++++++++++++++ src/custom/utils/misc.ts | 1 + 5 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 src/custom/state/orders/actions.ts create mode 100644 src/custom/state/orders/hooks.ts create mode 100644 src/custom/state/orders/reducer.ts create mode 100644 src/custom/utils/misc.ts diff --git a/src/custom/state/index.ts b/src/custom/state/index.ts index 6a445058e..c617c56b5 100644 --- a/src/custom/state/index.ts +++ b/src/custom/state/index.ts @@ -13,6 +13,7 @@ import burn from '@src/state/burn/reducer' import multicall from '@src/state/multicall/reducer' // CUSTOM REDUCERS import operator from './operator/reducer' +import orders from './orders/reducer' const UNISWAP_REDUCERS = { application, @@ -25,12 +26,13 @@ const UNISWAP_REDUCERS = { lists } -const PERSISTED_KEYS: string[] = ['user', 'transactions', 'lists'] +const PERSISTED_KEYS: string[] = ['user', 'transactions', 'lists', 'orders'] const store = configureStore({ reducer: { ...UNISWAP_REDUCERS, - operator + operator, + orders }, middleware: [...getDefaultMiddleware({ thunk: false }), save({ states: PERSISTED_KEYS })], preloadedState: load({ states: PERSISTED_KEYS }) diff --git a/src/custom/state/orders/actions.ts b/src/custom/state/orders/actions.ts new file mode 100644 index 000000000..f8890e1c1 --- /dev/null +++ b/src/custom/state/orders/actions.ts @@ -0,0 +1,57 @@ +import { createAction } from '@reduxjs/toolkit' +import { ChainId } from '@uniswap/sdk' + +enum OrderKind { + SELL = 'sell', + BUY = 'buy' +} + +// posted to /api/v1/orders on Order creation +// serializable, so no BigNumbers +export interface OrderCreation { + sellToken: string // address, without '0x' prefix + buyToken: string // address, without '0x' prefix + sellAmount: string // in atoms + buyAmount: string // in atoms + validTo: number // unix timestamp, seconds, use new Date(validTo * 1000) + appData: number // arbitrary identifier sent along with the order + tip: string // in atoms + orderType: OrderKind + partiallyFillable: boolean + signature: string // 65 bytes encoded as hex without `0x` prefix. v + r + s from the spec +} + +export enum OrderStatus { + PENDING = 'pending', + FULFILLED = 'fulfilled' +} + +// used internally by dapp +export interface Order extends OrderCreation { + id: OrderID // it is special :), Unique identifier for the order: 56 bytes encoded as hex without 0x + owner: string // address, without '0x' prefix + status: OrderStatus + fulfillmentTime?: string + creationTime: string + summary: string // for dapp use only, readable by user +} + +// gotten from querying /api/v1/orders +export interface OrderFromApi extends OrderCreation { + creationTime: string // Creation time of the order. Encoded as ISO 8601 UTC + owner: string // address, without '0x' prefix +} + +/** + * Unique identifier for the order, calculated by keccak256(orderDigest, ownerAddress, validTo), + where orderDigest = keccak256(orderStruct). bytes32. + */ +export type OrderID = string + +export const addPendingOrder = createAction<{ id: OrderID; chainId: ChainId; order: Order }>('order/addPendingOrder') +export const removeOrder = createAction<{ id: OrderID; chainId: ChainId }>('order/removeOrder') +// fulfillmentTime from event timestamp +export const fulfillOrder = createAction<{ id: OrderID; chainId: ChainId; fulfillmentTime: string }>( + 'order/fulfillOrder' +) +export const clearOrders = createAction<{ chainId: ChainId }>('order/clearOrders') diff --git a/src/custom/state/orders/hooks.ts b/src/custom/state/orders/hooks.ts new file mode 100644 index 000000000..cc6d38884 --- /dev/null +++ b/src/custom/state/orders/hooks.ts @@ -0,0 +1,97 @@ +import { ChainId } from '@uniswap/sdk' +import { useCallback, useMemo } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { AppDispatch, AppState } from 'state' +import { addPendingOrder, removeOrder, clearOrders, fulfillOrder, Order, OrderID } from './actions' +import { OrdersState, PartialOrdersMap } from './reducer' +import { isTruthy } from 'utils/misc' + +interface AddPendingOrderParams extends GetRemoveOrderParams { + order: Order +} + +interface FulfillOrderParams extends GetRemoveOrderParams { + fulfillmentTime: string +} +interface GetRemoveOrderParams { + id: OrderID + chainId: ChainId +} + +interface ClearOrdersParams { + chainId: ChainId +} + +type GetOrdersParams = Pick + +type AddOrderCallback = (addOrderParams: AddPendingOrderParams) => void +type RemoveOrderCallback = (clearOrderParams: GetRemoveOrderParams) => void +type FulfillOrderCallback = (fulfillOrderParams: FulfillOrderParams) => void +type ClearOrdersCallback = (clearOrdersParams: ClearOrdersParams) => void + +export const useOrder = ({ id, chainId }: GetRemoveOrderParams): Order | undefined => { + const state = useSelector(state => state.orders[chainId]) + + return state?.fulfilled[id]?.order || state?.pending[id]?.order +} + +export const useOrders = ({ chainId }: GetOrdersParams): Order[] => { + const state = useSelector(state => state.orders?.[chainId]) + + return useMemo(() => { + if (!state) return [] + + const allOrders = Object.values(state.fulfilled) + .concat(Object.values(state.pending)) + .map(orderObject => orderObject?.order) + .filter(isTruthy) + return allOrders + }, [state]) +} + +export const usePendingOrders = ({ chainId }: GetOrdersParams): Order[] => { + const state = useSelector(state => state.orders?.[chainId]?.pending) + + return useMemo(() => { + if (!state) return [] + + const allOrders = Object.values(state) + .map(orderObject => orderObject?.order) + .filter(isTruthy) + return allOrders + }, [state]) +} + +export const useFulfilledOrders = ({ chainId }: GetOrdersParams): Order[] => { + const state = useSelector(state => state.orders?.[chainId]?.fulfilled) + + return useMemo(() => { + if (!state) return [] + + const allOrders = Object.values(state) + .map(orderObject => orderObject?.order) + .filter(isTruthy) + return allOrders + }, [state]) +} + +export const useAddPendingOrder = (): AddOrderCallback => { + const dispatch = useDispatch() + return useCallback((addOrderParams: AddPendingOrderParams) => dispatch(addPendingOrder(addOrderParams)), [dispatch]) +} + +export const useFulfillOrder = (): FulfillOrderCallback => { + const dispatch = useDispatch() + return useCallback((fulfillOrderParams: FulfillOrderParams) => dispatch(fulfillOrder(fulfillOrderParams)), [dispatch]) +} + +export const useRemoveOrder = (): RemoveOrderCallback => { + const dispatch = useDispatch() + return useCallback((removeOrderParams: GetRemoveOrderParams) => dispatch(removeOrder(removeOrderParams)), [dispatch]) +} + +export const useClearOrders = (): ClearOrdersCallback => { + const dispatch = useDispatch() + return useCallback((clearOrdersParams: ClearOrdersParams) => dispatch(clearOrders(clearOrdersParams)), [dispatch]) +} diff --git a/src/custom/state/orders/reducer.ts b/src/custom/state/orders/reducer.ts new file mode 100644 index 000000000..907e48f70 --- /dev/null +++ b/src/custom/state/orders/reducer.ts @@ -0,0 +1,93 @@ +import { createReducer, PayloadAction } from '@reduxjs/toolkit' +import { ChainId } from '@uniswap/sdk' +import { addPendingOrder, removeOrder, Order, OrderID, clearOrders, fulfillOrder, OrderStatus } from './actions' + +export interface OrderObject { + id: OrderID + order: Order +} + +// {order uuid => OrderObject} mapping +type OrdersMap = Record +export type PartialOrdersMap = Partial + +export type OrdersState = { + readonly [chainId in ChainId]?: { + pending: PartialOrdersMap + fulfilled: PartialOrdersMap + } +} + +interface PrefillStateRequired { + chainId: ChainId +} + +type Writable = { + -readonly [K in keyof T]: T[K] +} + +// makes sure there's always an object at state[chainId], state[chainId].pending | .fulfilled +function prefillState( + state: Writable, + { payload: { chainId } }: PayloadAction +): asserts state is Required { + // asserts that state[chainId].pending | .fulfilled is ok to access + const stateAtChainId = state[chainId] + + if (!stateAtChainId) { + state[chainId] = { + pending: {}, + fulfilled: {} + } + return + } + + if (!stateAtChainId.pending) { + stateAtChainId.pending = {} + } + + if (!stateAtChainId.fulfilled) { + stateAtChainId.fulfilled = {} + } +} + +const initialState: OrdersState = {} + +export default createReducer(initialState, builder => + builder + .addCase(addPendingOrder, (state, action) => { + prefillState(state, action) + const { order, id, chainId } = action.payload + + state[chainId].pending[id] = { order, id } + }) + .addCase(removeOrder, (state, action) => { + prefillState(state, action) + const { id, chainId } = action.payload + delete state[chainId].pending[id] + delete state[chainId].fulfilled[id] + }) + .addCase(fulfillOrder, (state, action) => { + prefillState(state, action) + const { id, chainId, fulfillmentTime } = action.payload + + const orderObject = state[chainId].pending[id] + + if (orderObject) { + delete state[chainId].pending[id] + + orderObject.order.status = OrderStatus.FULFILLED + orderObject.order.fulfillmentTime = fulfillmentTime + + state[chainId].fulfilled[id] = orderObject + } + }) + .addCase(clearOrders, (state, action) => { + const { chainId } = action.payload + + state[chainId] = { + pending: {}, + fulfilled: {} + } + }) +) diff --git a/src/custom/utils/misc.ts b/src/custom/utils/misc.ts new file mode 100644 index 000000000..a2a59f04e --- /dev/null +++ b/src/custom/utils/misc.ts @@ -0,0 +1 @@ +export const isTruthy = (value: T | null | undefined | false): value is T => !!value