From 7d3c2b50a59ceb71e855fd2b195f05ae8b1bdf11 Mon Sep 17 00:00:00 2001 From: hassnian <44554284+hassnian@users.noreply.github.com> Date: Wed, 1 Jan 2025 16:53:56 +0500 Subject: [PATCH 01/52] add: `offers` tab in collections & init support for non specific token atomic swaps --- ...llectionSwaps.vue => CollectionTrades.vue} | 4 +- components/explore/tab/TabOnCollection.vue | 6 +- components/shared/filters/Filters.vue | 4 +- .../shared/filters/modules/TradeFilter.vue | 7 +- components/trade/TradeActivityTableRow.vue | 16 ++- components/trade/TradeOwnerButton.vue | 4 +- .../overviewModal/CollectionItemDetails.vue | 15 +++ components/trade/overviewModal/Details.vue | 2 +- .../trade/overviewModal/TokenItemDetails.vue | 28 +++++ .../overviewModal/TradeOverviewModal.vue | 2 +- components/trade/overviewModal/TypeOffer.vue | 32 ++--- components/trade/overviewModal/TypeSwap.vue | 55 ++------- components/trade/overviewModal/utils.ts | 4 +- composables/transaction/utils.ts | 2 +- composables/useIsTrade.ts | 4 +- composables/useTrades.ts | 113 +++++++++++++----- locales/en.json | 1 + pages/[prefix]/collection/[id]/offers.vue | 13 ++ pages/[prefix]/collection/[id]/swaps.vue | 4 +- 19 files changed, 202 insertions(+), 114 deletions(-) rename components/collection/{CollectionSwaps.vue => CollectionTrades.vue} (96%) create mode 100644 components/trade/overviewModal/CollectionItemDetails.vue create mode 100644 components/trade/overviewModal/TokenItemDetails.vue create mode 100644 pages/[prefix]/collection/[id]/offers.vue diff --git a/components/collection/CollectionSwaps.vue b/components/collection/CollectionTrades.vue similarity index 96% rename from components/collection/CollectionSwaps.vue rename to components/collection/CollectionTrades.vue index 8a96140457..d93c4ed94f 100644 --- a/components/collection/CollectionSwaps.vue +++ b/components/collection/CollectionTrades.vue @@ -20,7 +20,9 @@ diff --git a/components/shared/filters/modules/TradeFilter.vue b/components/shared/filters/modules/TradeFilter.vue index 5bcf35a439..1591c22b8b 100644 --- a/components/shared/filters/modules/TradeFilter.vue +++ b/components/shared/filters/modules/TradeFilter.vue @@ -1,6 +1,6 @@ diff --git a/components/trade/overviewModal/TypeSwap.vue b/components/trade/overviewModal/TypeSwap.vue index b7f02a72c5..b8cfc92ca9 100644 --- a/components/trade/overviewModal/TypeSwap.vue +++ b/components/trade/overviewModal/TypeSwap.vue @@ -8,37 +8,25 @@ 'flex-col-reverse': isMyTrade, }" > - + - - - + :nft="desired" + /> - - - + +
@@ -52,9 +40,9 @@ diff --git a/components/trade/overviewModal/utils.ts b/components/trade/overviewModal/utils.ts index 14ccf0e5b1..2b43eecf52 100644 --- a/components/trade/overviewModal/utils.ts +++ b/components/trade/overviewModal/utils.ts @@ -2,9 +2,9 @@ export type OverviewMode = 'owner' | 'incoming' export const useIsTradeOverview = (trade: ComputedRef) => { const { accountId } = useAuth() - const { isOwnerOfNft } = useIsTrade(trade, accountId) + const { isTargetOfTrade } = useIsTrade(trade, accountId) - const mode = computed(() => isOwnerOfNft.value ? 'incoming' : 'owner') + const mode = computed(() => isTargetOfTrade.value ? 'incoming' : 'owner') const isMyTrade = computed(() => mode.value === 'owner') const isIncomingTrade = computed(() => mode.value === 'incoming') diff --git a/composables/transaction/utils.ts b/composables/transaction/utils.ts index ecc1b3ffb6..9128b5d18b 100644 --- a/composables/transaction/utils.ts +++ b/composables/transaction/utils.ts @@ -60,7 +60,7 @@ export function isActionValid(action: Actions): boolean { [ShoppingActions.ACCEPT_SWAP]: (action: ActionAcceptSwap) => Boolean(action.receiveItem) && Boolean(action.receiveCollection) && Boolean(action.sendItem) && Boolean(action.sendCollection), [ShoppingActions.ACCEPT_OFFER]: (action: ActionAcceptOffer) => - Boolean(action.sendItem && action.sendCollection && action.price && action.receiveItem), + Boolean(action.sendCollection && action.price && action.receiveItem), [Interaction.MINT]: (action: ActionMintCollection) => Boolean(action.collection), [Collections.DELETE]: (action: ActionDeleteCollection) => diff --git a/composables/useIsTrade.ts b/composables/useIsTrade.ts index e2621319a5..9cfa169f97 100644 --- a/composables/useIsTrade.ts +++ b/composables/useIsTrade.ts @@ -1,9 +1,9 @@ export default function (trade: ComputedRef, target: MaybeRef) { const isCreatorOfTrade = computed(() => trade.value?.caller === unref(target)) - const isOwnerOfNft = computed(() => (trade.value?.isEntireCollectionDesired ? trade.value?.considered : trade.value?.desired)?.currentOwner === unref(target)) + const isTargetOfTrade = computed(() => trade.value?.targets.includes(unref(target))) return { isCreatorOfTrade, - isOwnerOfNft, + isTargetOfTrade, } } diff --git a/composables/useTrades.ts b/composables/useTrades.ts index 2284150d89..fc4c496f2b 100644 --- a/composables/useTrades.ts +++ b/composables/useTrades.ts @@ -40,9 +40,9 @@ type BaseTrade = { considered: TradeConsidered } -export enum TradeDesiredType { - TOKEN, - COLLECTION, +export enum TradeDesiredTokenType { + SPECIFIC, + ANY_IN_COLLECTION, } export enum TradeType { @@ -59,10 +59,11 @@ type Offer = BaseTrade type Trade = Swap | Offer export type TradeNftItem = T & { - expirationDate?: Date + expirationDate: Date type: TradeType - desiredType: TradeDesiredType - isEntireCollectionDesired: boolean + desiredType: TradeDesiredTokenType + isAnyTokenInCollectionDesired: boolean + targets: string[] } export const TRADES_QUERY_MAP: Record = { @@ -85,51 +86,103 @@ export default function ({ where = {}, limit = 100, disabled = computed(() => fa disabled?: ComputedRef type?: TradeType }) { - const variables = computed(() => ({ - where: unref(where), - limit: limit, - })) - const { queryDocument, dataKey } = TRADES_QUERY_MAP[type] + const items = ref([]) + const targetsOfTrades = ref>() + const ownersSubscription = ref(() => {}) const { result: data, loading: fetching, } = useQuery<{ offers: Offer[] } | { swpas: Swap[] }>( queryDocument, - variables, + computed(() => ({ + where: unref(where), + limit: limit, + })), computed(() => ({ enabled: !disabled.value, })), ) - const items = computed(() => { - return data.value?.[dataKey]?.map((trade) => { - const desiredType = trade.desired ? TradeDesiredType.TOKEN : TradeDesiredType.COLLECTION - - return { - ...trade, - expirationDate: currentBlock.value ? addHours(new Date(), (Number(trade.expiration) - currentBlock.value) / BLOCKS_PER_HOUR) : undefined, - offered: trade.nft, - desiredType: desiredType, - isEntireCollectionDesired: desiredType === TradeDesiredType.COLLECTION, - type, - } as TradeNftItem - }) || [] - }) - - async function getCurrentBlock() { + const getCurrentBlock = async () => { const api = await useApi().apiInstance.value const { number } = await api.rpc.chain.getHeader() return number.toNumber() } - const loading = computed(() => !currentBlock.value || fetching.value) - if (!currentBlock.value) { getCurrentBlock().then(b => currentBlock.value = b) } + const dataItems = computed(() => data.value?.[dataKey] || []) + const hasTargetsOfTrades = computed(() => targetsOfTrades.value?.size) + const tradeKeys = computed(() => dataItems.value.map(item => item.id).join('-')) + const loading = computed(() => !currentBlock.value || fetching.value || !hasTargetsOfTrades.value) + + const subscribeToTargetsOfTrades = (trades: BaseTrade[]) => { + ownersSubscription.value = useSubscriptionGraphql({ + query: ` + collectionEntities(where: { + id_in: ${JSON.stringify(trades.map(item => item.considered.id))} + }) { + id + nfts { + id + currentOwner + } + } + `, + onChange: ({ data: { collectionEntities: collections } }) => { + const map = new Map() + + const collectionMap = collections.reduce((acc, collection) => { + acc[collection.id] = collection.nfts + return acc + }, {}) + + trades.forEach((trade) => { + const tradeDesired = trade.desired + map.set(trade.id, + tradeDesired + ? [collectionMap[tradeDesired.collection.id].find(nft => nft.id === tradeDesired.id)?.currentOwner] + : collectionMap[trade.considered.id].map(nft => nft.currentOwner), + ) + }) + + targetsOfTrades.value = map + }, + }) + } + + watch(tradeKeys, (key) => { + if (key) { + ownersSubscription.value() + targetsOfTrades.value = undefined + subscribeToTargetsOfTrades(dataItems.value) + } + }) + + watchEffect(() => { + if (!hasTargetsOfTrades.value || !currentBlock.value) { + return + } + + items.value = dataItems.value.map((trade) => { + const desiredType = trade.desired ? TradeDesiredTokenType.SPECIFIC : TradeDesiredTokenType.ANY_IN_COLLECTION + + return { + ...trade, + expirationDate: addHours(new Date(), (Number(trade.expiration) - currentBlock.value) / BLOCKS_PER_HOUR), + offered: trade.nft, + desiredType: desiredType, + isAnyTokenInCollectionDesired: desiredType === TradeDesiredTokenType.ANY_IN_COLLECTION, + type, + targets: targetsOfTrades.value?.get(trade.id) || [], + } as TradeNftItem + }) + }) + return { items, loading, diff --git a/locales/en.json b/locales/en.json index dfb2974e4c..999f13d438 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1517,6 +1517,7 @@ "invalidPrice": "Your offer must greater than 0.0001", "manageOffers": "Manage Offers", "newOffer": "New Offer", + "offer": "Offer", "offerAccept": "Accepting Offer", "offerCancellation": "Offer Cancellation", "offerCreation": "Offer Creation", diff --git a/pages/[prefix]/collection/[id]/offers.vue b/pages/[prefix]/collection/[id]/offers.vue new file mode 100644 index 0000000000..bc7d305c0a --- /dev/null +++ b/pages/[prefix]/collection/[id]/offers.vue @@ -0,0 +1,13 @@ + + + diff --git a/pages/[prefix]/collection/[id]/swaps.vue b/pages/[prefix]/collection/[id]/swaps.vue index d2bb971e23..1d79c37446 100644 --- a/pages/[prefix]/collection/[id]/swaps.vue +++ b/pages/[prefix]/collection/[id]/swaps.vue @@ -1,10 +1,12 @@ + + diff --git a/components/trade/TradeOwnerButton.vue b/components/trade/TradeOwnerButton.vue index 47be3446da..fee962ff82 100644 --- a/components/trade/TradeOwnerButton.vue +++ b/components/trade/TradeOwnerButton.vue @@ -10,7 +10,12 @@ import type { ButtonConfig } from '../profile/types' const emit = defineEmits(['click']) -const props = defineProps<{ trade: TradeNftItem, loading?: boolean }>() +const props = defineProps<{ + trade: TradeNftItem + loading?: boolean + disabled?: boolean + label?: string +}>() const { accountId } = useAuth() const { $i18n } = useNuxtApp() @@ -32,7 +37,7 @@ const details = { }, } -const buttonConfig = computed(() => { +const tradeButtonConfig = computed(() => { if (props.trade.status === TradeStatus.EXPIRED) { return isCreatorOfTrade.value ? { @@ -59,4 +64,18 @@ const buttonConfig = computed(() => { return null }) + +const buttonConfig = computed(() => { + if (!tradeButtonConfig.value) { + return null + } + + const config = tradeButtonConfig.value + + Object.assign(config, { disabled: props.disabled }) + + props.label && Object.assign(config, { label: props.label }) + + return config +}) diff --git a/components/trade/overviewModal/SubmitButton.vue b/components/trade/overviewModal/SubmitButton.vue new file mode 100644 index 0000000000..7f35ccffb4 --- /dev/null +++ b/components/trade/overviewModal/SubmitButton.vue @@ -0,0 +1,20 @@ + + + diff --git a/components/trade/overviewModal/TokenInCollection.vue b/components/trade/overviewModal/TokenInCollection.vue new file mode 100644 index 0000000000..65a7bf92bc --- /dev/null +++ b/components/trade/overviewModal/TokenInCollection.vue @@ -0,0 +1,71 @@ + + + diff --git a/components/trade/overviewModal/TokenSearchInput.vue b/components/trade/overviewModal/TokenSearchInput.vue new file mode 100644 index 0000000000..b2b671b4f4 --- /dev/null +++ b/components/trade/overviewModal/TokenSearchInput.vue @@ -0,0 +1,60 @@ + + + diff --git a/components/trade/overviewModal/TradeOverviewModal.vue b/components/trade/overviewModal/TradeOverviewModal.vue index 488c43f28f..a6ab8a8688 100644 --- a/components/trade/overviewModal/TradeOverviewModal.vue +++ b/components/trade/overviewModal/TradeOverviewModal.vue @@ -31,8 +31,11 @@ /> @@ -42,8 +45,10 @@ class="!pt-5" > @@ -54,12 +59,18 @@ diff --git a/components/trade/overviewModal/utils.ts b/components/trade/overviewModal/utils.ts index 2b43eecf52..49cb4d0d22 100644 --- a/components/trade/overviewModal/utils.ts +++ b/components/trade/overviewModal/utils.ts @@ -1,3 +1,5 @@ +import type { Prefix } from '@kodadot1/static' + export type OverviewMode = 'owner' | 'incoming' export const useIsTradeOverview = (trade: ComputedRef) => { @@ -15,3 +17,54 @@ export const useIsTradeOverview = (trade: ComputedRef) mode, } } + +export type ExecTxParams = { + trade: TradeNftItem + sendItem?: string + transaction: ReturnType['transaction'] + urlPrefix: Prefix +} + +export const TradeTypeTx: Record void>> = { + [TradeType.SWAP]: { + owner: ({ trade, urlPrefix, transaction }) => { + transaction({ + interaction: ShoppingActions.CANCEL_SWAP, + urlPrefix: urlPrefix, + offeredId: trade.offered.sn, + offeredCollectionId: trade.offered.collection.id, + }) + }, + incoming: ({ trade, sendItem, urlPrefix, transaction }) => { + transaction({ + interaction: ShoppingActions.ACCEPT_SWAP, + urlPrefix: urlPrefix, + receiveItem: trade.offered.sn, + receiveCollection: trade.offered.collection.id, + sendCollection: trade.considered.id, + sendItem: sendItem, + price: trade.price, + surcharge: (trade as TradeNftItem).surcharge, + }) + }, + }, + [TradeType.OFFER]: { + owner: ({ trade, transaction, urlPrefix }) => { + transaction({ + interaction: ShoppingActions.CANCEL_OFFER, + urlPrefix: urlPrefix, + offeredId: trade.offered.sn, + }) + }, + incoming: ({ trade, sendItem, transaction, urlPrefix }) => { + transaction({ + interaction: ShoppingActions.ACCEPT_OFFER, + urlPrefix: urlPrefix, + receiveItem: trade.offered.sn, + sendCollection: trade.considered.id, + sendItem: sendItem, + price: trade.price, + }) + }, + }, +} diff --git a/locales/en.json b/locales/en.json index 999f13d438..bcaaa292fc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -930,6 +930,7 @@ "resending": "Resending", "resetAll": "Reset All", "search": "Search", + "searchNoResults": "No results", "searchNoResultsText": "Give it another shot with different parameters or check back later - New awesome NFTs are added all the time", "searchNoResultsTitle": "Whoops, Couldn't find anything matching your search", "searchPlaceholder": "Search by Collection or NFT", @@ -1987,6 +1988,7 @@ "week": "7D" } }, + "trade": { "selectSendItem": "Select NFT to send" }, "transaction": { "acceptOffer": "Accept Offer", "acceptSwap": "Accept Swap", From 80d782b1f32d0809354cb1f771e6ffa33a0413e6 Mon Sep 17 00:00:00 2001 From: hassnian <44554284+hassnian@users.noreply.github.com> Date: Sat, 4 Jan 2025 10:18:46 +0500 Subject: [PATCH 04/52] ref(transactionMintToken.ts): use `isAssetHub` util --- composables/transaction/transactionMintToken.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composables/transaction/transactionMintToken.ts b/composables/transaction/transactionMintToken.ts index f21e3a48cb..ff9b98b3c7 100644 --- a/composables/transaction/transactionMintToken.ts +++ b/composables/transaction/transactionMintToken.ts @@ -1,9 +1,9 @@ +import type { Prefix } from '@kodadot1/static' import type { MintTokenParams, SubstrateMintTokenParams } from './types' import { execMintStatemine } from './mintToken/transactionMintStatemine' export function execMintToken({ item, ...params }: MintTokenParams) { - // item.urlPrefix === 'ahr' - if (item.urlPrefix === 'ahk' || item.urlPrefix === 'ahp') { + if (isAssetHub(item.urlPrefix as Prefix)) { return execMintStatemine({ item, ...params, From 8af29cc7de000f4a02f225e6c5bcf1472718fd5e Mon Sep 17 00:00:00 2001 From: hassnian <44554284+hassnian@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:39:49 +0500 Subject: [PATCH 05/52] fix(TradeOwnerButton.vue): button config not reactive --- components/trade/TradeOwnerButton.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/trade/TradeOwnerButton.vue b/components/trade/TradeOwnerButton.vue index 3030afd4fe..e190b2a02b 100644 --- a/components/trade/TradeOwnerButton.vue +++ b/components/trade/TradeOwnerButton.vue @@ -95,7 +95,7 @@ const buttonConfig = computed(() => { return null } - const config = tradeButtonConfig.value + const config = { ...tradeButtonConfig.value } Object.assign(config, { disabled: props.disabled }) From 65aea48b3886d7a98e09c1410040bf1a071967d3 Mon Sep 17 00:00:00 2001 From: hassnian <44554284+hassnian@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:43:44 +0500 Subject: [PATCH 06/52] add(TokenInCollection.vue): show cart item skeleton while loading --- .../common/BaseCartItemDetailsSkeleton.vue | 42 +++++++++++++++++++ components/common/SearchInput.vue | 7 +++- .../trade/overviewModal/TokenInCollection.vue | 2 +- .../trade/overviewModal/TokenItemDetails.vue | 20 ++++++--- 4 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 components/common/BaseCartItemDetailsSkeleton.vue diff --git a/components/common/BaseCartItemDetailsSkeleton.vue b/components/common/BaseCartItemDetailsSkeleton.vue new file mode 100644 index 0000000000..6556cce639 --- /dev/null +++ b/components/common/BaseCartItemDetailsSkeleton.vue @@ -0,0 +1,42 @@ + + + diff --git a/components/common/SearchInput.vue b/components/common/SearchInput.vue index 09e1772e3a..05f30c4103 100644 --- a/components/common/SearchInput.vue +++ b/components/common/SearchInput.vue @@ -9,7 +9,6 @@ clearable item-class="hover:!bg-k-accent-light" :placeholder="placeholder" - @focus="onSearchFn" @typing="onSearchFn" @select="onSelect" > @@ -17,7 +16,9 @@ v-if="loading" #header > -
laoding...
+
+ {{ $t('loading') }}... +