Skip to content

Commit

Permalink
LIQ-732: POC websocket
Browse files Browse the repository at this point in the history
socket auction

integrate auction, order, drop

remove polling code

remove unused code

upadte comment

update env websocket

add socket url and trade event for sell
  • Loading branch information
vanminhtran committed Sep 28, 2022
1 parent 3eb900c commit c2e9549
Show file tree
Hide file tree
Showing 13 changed files with 386 additions and 273 deletions.
29 changes: 25 additions & 4 deletions core/ui/src/components/Drop/List/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { AnchorWallet } from '@solana/wallet-adapter-react';
import { CandyShop, fetchDropsByShopAddress } from '@liqnft/candy-shop-sdk';
import { Drop, DropStatus, ListBase } from '@liqnft/candy-shop-types';
Expand All @@ -12,7 +12,7 @@ import { DropFilter, FILTERS } from 'constant/drop';

import { useObserver } from 'hooks/useObserver';
import { LoadStatus } from 'constant';
import { removeDuplicate } from 'utils/helperFunc';
import { removeDuplicate, removeListeners } from 'utils/helperFunc';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';

Expand All @@ -21,6 +21,8 @@ import { DropFooter } from './DropFooter';
dayjs.extend(duration);
import './style.less';
import { Search } from 'components/Search';
import { EventName, useSocket } from 'public/Context/Socket';
import { useUpdateCandyShopContext } from 'public/Context/CandyShopDataValidator';

interface DropsProps {
wallet: AnchorWallet | undefined;
Expand All @@ -45,6 +47,9 @@ export const Drops: React.FC<DropsProps> = ({ candyShop, wallet, walletConnectCo
const dropQueries = useRef({ offset: 0, limit: 12 });
const prevFilterRef = useRef(filterOption);

useUpdateCandyShopContext({ candyShopAddress: candyShop.candyShopAddress, network: candyShop.env });
const { onSocketEvent } = useSocket();

const onUpdateDrops = (drop: Drop) => {
setDropNfts((list) =>
list?.map((item) => {
Expand All @@ -59,7 +64,7 @@ export const Drops: React.FC<DropsProps> = ({ candyShop, wallet, walletConnectCo
);
};

useEffect(() => {
useMemo(() => {
if (prevFilterRef.current !== filterOption || keyword !== undefined) {
prevFilterRef.current = filterOption;
dropQueries.current.offset = 0;
Expand Down Expand Up @@ -89,7 +94,7 @@ export const Drops: React.FC<DropsProps> = ({ candyShop, wallet, walletConnectCo
}
setHasMore(offset + result.length < totalCount);
dropQueries.current = { ...dropQueries.current, offset: offset + dropQueries.current.limit };
setDropNfts((list) => removeDuplicate<Drop>(list, result, 'txHashAtCreation'));
setDropNfts((list = []) => removeDuplicate<Drop>(list, result, 'txHashAtCreation'));
if (offset + result.length < totalCount && target && target.getBoundingClientRect().top > 0) {
fetchDrops();
}
Expand All @@ -104,6 +109,22 @@ export const Drops: React.FC<DropsProps> = ({ candyShop, wallet, walletConnectCo
});
};

// socket
useEffect(() => {
const controllers = [
onSocketEvent(EventName.dropCreatedOrUpdated, (drop: Drop) => {
setDropNfts((list) => {
if (!list?.length) return [drop];
const currentOne = list.find((item) => item.vaultAddress === drop.vaultAddress);
if (!currentOne) return removeDuplicate([drop], list, 'txHashAtCreation');
return list.map((item) => (item.vaultAddress === drop.vaultAddress ? drop : item));
});
})
];

return () => removeListeners(controllers);
}, [onSocketEvent]);

// handle update drop status
useEffect(() => {
if (!dropNfts?.length) return;
Expand Down
5 changes: 0 additions & 5 deletions core/ui/src/components/Payment/StripePayment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
PaymentCurrencyType,
PaymentInfo,
PaymentMethodType,
ShopStatusType,
SingleBase
} from '@liqnft/candy-shop-types';
import { CandyShopPay, safeAwait } from '@liqnft/candy-shop-sdk';
Expand All @@ -19,7 +18,6 @@ import { NftVerification } from 'components/Tooltip/NftVerification';
import { ShopExchangeInfo, BuyModalState, PaymentErrorDetails } from 'model';
import { notification, NotificationType } from 'utils/rc-notification';
import { getPrice } from 'utils/getPrice';
import { useUpdateCandyShopContext } from 'public/Context/CandyShopDataValidator';
import stripeLogo from '../../assets/stripe.png';

const Logger = 'CandyShopUI/StripePayment';
Expand Down Expand Up @@ -50,8 +48,6 @@ export const StripePayment: React.FC<StripePaymentProps> = ({
const stripePromise = loadStripe(stripePublicKey);
const [paymentEntityId, setPaymentEntityId] = useState<string>();

const { refreshSubject } = useUpdateCandyShopContext();

useEffect(() => {
const params: CreatePaymentParams = {
shopProgramId: order.programId,
Expand Down Expand Up @@ -116,7 +112,6 @@ export const StripePayment: React.FC<StripePaymentProps> = ({

const handlePaymentSucceed = () => {
onProcessingPay(BuyModalState.CONFIRMED);
refreshSubject(ShopStatusType.UserNft, Date.now());
};

const handlePaymentFailed = (confirmRes: SingleBase<PaymentInfo>) => {
Expand Down
4 changes: 2 additions & 2 deletions core/ui/src/constant/Orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ export const LOADING_SKELETON_COUNT = 4;
export const SORT_OPTIONS: { value: OrderSortBy; label: string }[] = [
{
value: {
column: 'blockTimeAtCreation',
column: 'createdAt',
order: 'desc'
},
label: 'Newest'
},
{
value: {
column: 'blockTimeAtCreation',
column: 'createdAt',
order: 'asc'
},
label: 'Oldest'
Expand Down
28 changes: 28 additions & 0 deletions core/ui/src/constant/SocketEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export enum EventName {
ping = 'ping',
pong = 'pong',
startSession = 'startSession',
stopSession = 'stopSession',
orderOpened = 'orderOpened',
orderFilled = 'orderFilled',
orderCanceled = 'orderCanceled',
traded = 'traded',
auctionCreated = 'auctionCreated',
auctionUpdated = 'auctionUpdated',
auctionUpdateBid = 'auctionUpdateBid',
auctionUpdateStatus = 'auctionUpdateStatus',
getAuctionByAddressAndWallet = 'getAuctionByAddressAndWallet',
dropCreatedOrUpdated = 'dropCreatedOrUpdated'
}

export function parseSocketMessage<T>(message: string): { event: EventName; data: T } | undefined {
try {
return JSON.parse(message);
} catch (error) {
console.log('Parse socket message error=', error);
}
}

export function stringifyMessage(data: { event: EventName; data: any }): string {
return JSON.stringify(data);
}
41 changes: 17 additions & 24 deletions core/ui/src/public/Activity/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,17 @@ import { IconExplorer } from 'assets/IconExplorer';
import InfiniteScroll from 'react-infinite-scroll-component';

import { CandyShop, ExplorerLinkBase } from '@liqnft/candy-shop-sdk';
import { Trade, ListBase, ShopStatusType, SortBy } from '@liqnft/candy-shop-types';
import { useValidateStatus } from 'hooks/useValidateStatus';
import { useUpdateSubject } from 'public/Context/CandyShopDataValidator';
import { ActivityActionsStatus } from 'constant';
import { Trade, ListBase, SortBy } from '@liqnft/candy-shop-types';
import { removeDuplicate, EMPTY_FUNCTION } from 'utils/helperFunc';
import { IconSolanaFM } from 'assets/IconSolanaFM';

import { EventName } from 'constant/SocketEvent';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);

import './style.less';
import { useSocket } from 'public/Context/Socket';
import { useUpdateCandyShopContext } from 'public/Context/CandyShopDataValidator';

interface ActivityProps {
candyShop: CandyShop;
Expand All @@ -34,9 +33,9 @@ export const Activity: React.FC<ActivityProps> = ({ candyShop, identifiers, orde
const [trades, setTrades] = useState<Trade[]>([]);
const [hasMore, setHasMore] = useState<boolean>(true);
const [offset, setOffset] = useState<number>(0);
const { onSocketEvent } = useSocket();

useUpdateSubject({ subject: ShopStatusType.Trade, candyShopAddress: candyShop.candyShopAddress });
const updateActivityStatus = useValidateStatus(ActivityActionsStatus);
useUpdateCandyShopContext({ candyShopAddress: candyShop.candyShopAddress, network: candyShop.env });

const getTrades = useCallback(
(offset: number, limit: number, firstLoad?: boolean) => () => {
Expand Down Expand Up @@ -69,25 +68,19 @@ export const Activity: React.FC<ActivityProps> = ({ candyShop, identifiers, orde
getTrades(0, LIMIT)();
}, [getTrades]);

//update hook
useEffect(() => {
if (!updateActivityStatus) return;

candyShop
.transactions({ identifiers, offset: 0, limit: 10 })
.then((res: ListBase<Trade>) => {
if (!res.success) return;
setTrades((list) => {
// prettier-ignore
const newItems = res.result.filter((item) => list.findIndex((i) => i.txHashAtCreation === item.txHashAtCreation) === -1);
if (newItems.length) return [...newItems, ...list];
return list;
});
})
.catch((error: any) => {
console.log(`${Logger}: candyShop.transactions failed, error=`, error);
const controller = onSocketEvent(EventName.traded, (data: Trade) => {
setTrades((list) => {
const trades = removeDuplicate([data], list, 'txHashAtCreation');
if (trades.length === list.length + 1) {
setOffset((offset) => offset + 1);
}
return trades;
});
}, [candyShop, identifiers, updateActivityStatus]);
});

return () => controller.abort();
}, [onSocketEvent]);

return (
<div className="candy-activity">
Expand Down
111 changes: 63 additions & 48 deletions core/ui/src/public/Auction/Auctions.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { AnchorWallet } from '@solana/wallet-adapter-react';
import { CandyShop, fetchAuctionsByShopAddress } from '@liqnft/candy-shop-sdk';
import InfiniteScroll from 'react-infinite-scroll-component';
import { LoadingSkeleton } from 'components/LoadingSkeleton';
import { AuctionCard } from 'components/Auction';
import { Auction, AuctionStatus, ListBase, ShopStatusType } from '@liqnft/candy-shop-types';
import { Auction, AuctionStatus, ListBase } from '@liqnft/candy-shop-types';
import { Empty } from 'components/Empty';
import { ORDER_FETCH_LIMIT, BACKGROUND_UPDATE_LIMIT } from 'constant/Orders';
import { AuctionActionsStatus, DEFAULT_LIST_AUCTION_STATUS } from 'constant';
import { useValidateStatus } from 'hooks/useValidateStatus';
import { useUpdateSubject } from 'public/Context/CandyShopDataValidator';
import { ORDER_FETCH_LIMIT } from 'constant/Orders';
import { DEFAULT_LIST_AUCTION_STATUS } from 'constant';
import { useUpdateCandyShopContext } from 'public/Context/CandyShopDataValidator';
import { EventName, useSocket } from 'public/Context/Socket';
import { removeDuplicate, removeListeners } from 'utils/helperFunc';

const Logger = 'CandyShopUI/Auctions';

Expand All @@ -26,16 +27,16 @@ export const Auctions: React.FC<AuctionsProps> = ({
candyShop,
statusFilters = DEFAULT_LIST_AUCTION_STATUS
}) => {
const [auctionedNfts, setAuctionedNfts] = useState<Auction[]>([]);
const [auctionedNfts, setAuctions] = useState<Auction[]>([]);
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [startIndex, setStartIndex] = useState(0);
const [loading, setLoading] = useState<boolean>(false);

useUpdateSubject({ subject: ShopStatusType.Auction, candyShopAddress: candyShop.candyShopAddress });
const updateStatus = useValidateStatus(AuctionActionsStatus);
const updateStatusRef = useRef<number>(updateStatus);

const { onSocketEvent, onSendEvent } = useSocket();
const walletKeyString = wallet?.publicKey.toString();
const candyShopAddress = candyShop.candyShopAddress;

useUpdateCandyShopContext({ candyShopAddress, network: candyShop.env });

const loadNextPage = (startIndex: number) => () => {
if (startIndex === 0) return;
Expand All @@ -46,7 +47,7 @@ export const Auctions: React.FC<AuctionsProps> = ({
(startIndex: number) => {
setHasNextPage(true);
setLoading(true);
fetchAuctionsByShopAddress(candyShop.candyShopAddress.toString(), {
fetchAuctionsByShopAddress(candyShopAddress, {
offset: startIndex,
limit: ORDER_FETCH_LIMIT,
status: statusFilters,
Expand All @@ -64,9 +65,9 @@ export const Auctions: React.FC<AuctionsProps> = ({
return prevIndex + ORDER_FETCH_LIMIT;
});
if (startIndex === 0) {
setAuctionedNfts(data.result);
setAuctions(data.result);
} else {
setAuctionedNfts((prevNfts) => [...prevNfts, ...data.result]);
setAuctions((prevNfts) => [...prevNfts, ...data.result]);
}
})
.catch((error: any) => {
Expand All @@ -77,48 +78,62 @@ export const Auctions: React.FC<AuctionsProps> = ({
setLoading(false);
});
},
[candyShop.candyShopAddress, statusFilters, walletKeyString]
[candyShopAddress, statusFilters, walletKeyString]
);

useEffect(() => {
if (updateStatus === updateStatusRef.current) return;
updateStatusRef.current = updateStatus;
fetchAuctions(0);
}, [fetchAuctions]);

const batches = Array.from({ length: Math.ceil(startIndex / BACKGROUND_UPDATE_LIMIT) });
Promise.all(
batches.map((_, idx) =>
fetchAuctionsByShopAddress(candyShop.candyShopAddress.toString(), {
offset: idx * BACKGROUND_UPDATE_LIMIT,
limit: BACKGROUND_UPDATE_LIMIT,
status: statusFilters,
walletAddress: walletKeyString
})
)
)
.then((responses: ListBase<Auction>[]) => {
const memo: { [key: string]: true } = {};
//socket
useEffect(() => {
if (!walletKeyString) return;

setAuctionedNfts(
responses.reduce((acc: Auction[], res: ListBase<Auction>) => {
if (!res.result?.length) return acc;
res.result.forEach((auction) => {
if (memo[auction.tokenAccount]) return;
memo[auction.tokenAccount] = true;
acc.push(auction);
const controllers = [
onSocketEvent(EventName.auctionCreated, (auction: Auction) => {
if (!statusFilters.includes(auction.status)) return;
setAuctions((list) => removeDuplicate([auction], list, 'auctionAddress'));
}),
onSocketEvent(EventName.auctionUpdateStatus, (auction: { auctionAddress: string; status: AuctionStatus }) => {
// status out of current Fe filter => remove it if exist
if (!statusFilters.includes(auction.status)) {
return setAuctions((list) => {
const newList = list.filter((item) => item.auctionAddress === auction.auctionAddress);
if (newList.length === list.length) return list;
return newList;
});
} else {
// find and update status for that auction
setAuctions((list) =>
list.map((item) => {
if (item.auctionAddress === auction.auctionAddress) {
return { ...item, status: auction.status };
}
return item;
})
);
}
}),
onSocketEvent(EventName.auctionUpdateBid, (data: { auctionAddress: string }) => {
// find current auction in list:
setAuctions((list) => {
const currentOne = list.find((item) => item.auctionAddress === data.auctionAddress);
if (currentOne && walletKeyString) {
onSendEvent(EventName.getAuctionByAddressAndWallet, {
auctionAddress: data.auctionAddress,
walletAddress: walletKeyString
});
return acc;
}, [])
);
setStartIndex(batches.length * BACKGROUND_UPDATE_LIMIT);
}
return list;
});
}),
onSocketEvent(EventName.getAuctionByAddressAndWallet, (data: Auction) => {
setAuctions((list) => list.map((item) => (item.auctionAddress === data.auctionAddress ? data : item)));
})
.catch((err: any) => {
console.log(`${Logger} BackgroundUpdate failed, error=`, err);
});
}, [candyShop.candyShopAddress, startIndex, statusFilters, updateStatus, walletKeyString]);
];

useEffect(() => {
fetchAuctions(0);
}, [fetchAuctions]);
return () => removeListeners(controllers);
}, [onSocketEvent, onSendEvent, statusFilters, walletKeyString]);

return (
<div className="candy-container">
Expand Down
Loading

0 comments on commit c2e9549

Please sign in to comment.