From d670f92a917a785574f343c7d8e7f29a78661865 Mon Sep 17 00:00:00 2001 From: Jarsen <31397967+Jarsen136@users.noreply.github.com> Date: Sun, 16 Feb 2025 18:58:39 +0100 Subject: [PATCH 1/5] feat: Create Collection Atomic Swap --- components/collection/CollectionTrades.vue | 3 +- .../GalleryItemTrade.vue | 21 +------- .../swap/CreateCollectionSwapButton.vue | 49 +++++++++++++++++ components/swap/Preview.vue | 44 ++++++++------- components/swap/PreviewItem.vue | 2 + components/swap/banner/CollectionPreview.vue | 53 +++++++++++++++++++ components/swap/banner/accounts.vue | 12 ++++- .../swap/collection/DestinationProfile.vue | 35 ++++++++++++ components/swap/layout/index.vue | 1 + components/swap/review.vue | 40 +++++++++++++- components/trade/TradeActivityTable.vue | 4 +- .../transaction/transactionCreateSwap.ts | 2 +- composables/transaction/types.ts | 4 +- composables/useTradeActionClick.ts | 28 ++++++++++ i18n/locales/en.json | 4 ++ middleware/redirects.global.ts | 5 ++ middleware/swap.ts | 24 +++++---- pages/[prefix]/swap/collection/[id]/index.vue | 10 ++++ stores/atomicSwaps.ts | 6 ++- utils/swap.ts | 21 ++++++-- 20 files changed, 304 insertions(+), 64 deletions(-) create mode 100644 components/swap/CreateCollectionSwapButton.vue create mode 100644 components/swap/banner/CollectionPreview.vue create mode 100644 components/swap/collection/DestinationProfile.vue create mode 100644 composables/useTradeActionClick.ts create mode 100644 pages/[prefix]/swap/collection/[id]/index.vue diff --git a/components/collection/CollectionTrades.vue b/components/collection/CollectionTrades.vue index be9de7d98c..05c2c65588 100644 --- a/components/collection/CollectionTrades.vue +++ b/components/collection/CollectionTrades.vue @@ -12,6 +12,7 @@ :key="key" :query="tradeQuery" :type="tradeType" + :enable-collection-swap="tradeType === TradeType.SWAP" /> @@ -19,7 +20,7 @@ diff --git a/components/swap/Preview.vue b/components/swap/Preview.vue index 1b913bdddd..7782c5de64 100644 --- a/components/swap/Preview.vue +++ b/components/swap/Preview.vue @@ -24,7 +24,7 @@ {{ $t('shoppingCart.clearAll') }} @@ -46,6 +46,7 @@ :name="nft.name" :image="sanitizeIpfsUrl(nft.meta.image)" image-class="border border-k-shade" + :hide-remove="isCollectionSwap && nft.id === null" @remove="swapStore.removeStepItem(nft.id)" /> @@ -107,7 +108,7 @@ size="large" label="Next" variant="primary" - :disabled + :disabled="disabled" no-shadow expanded @click="onNext" @@ -133,23 +134,6 @@ type StepDetails = { surchargeDirection: SwapSurchargeDirection } -const stepDetailsMap: Partial> = { - [SwapStep.DESIRED]: { - title: 'swap.yourSwapList', - surchargeTitle: 'swap.requestToken', - nextRouteName: getSwapStepRouteName(SwapStep.OFFERED), - backRouteName: getSwapStepRouteName(SwapStep.COUNTERPARTY), - surchargeDirection: 'Receive', - }, - [SwapStep.OFFERED]: { - title: 'swap.yourOffer', - surchargeTitle: 'swap.addToken', - nextRouteName: getSwapStepRouteName(SwapStep.REVIEW), - backRouteName: getSwapStepRouteName(SwapStep.DESIRED), - surchargeDirection: 'Send', - }, -} - const props = defineProps<{ step: SwapStep }>() @@ -161,6 +145,24 @@ const { accountId } = useAuth() const { decimals } = useChain() const { urlPrefix } = usePrefix() const { getChainIcon } = useIcon() +const isCollectionSwap = computed(() => swap.value.isCollectionSwap) + +const stepDetailsMap: ComputedRef>> = computed(() => ({ + [SwapStep.DESIRED]: { + title: 'swap.yourSwapList', + surchargeTitle: 'swap.requestToken', + nextRouteName: getSwapStepRouteName(SwapStep.OFFERED, isCollectionSwap.value), + backRouteName: getSwapStepRouteName(SwapStep.COUNTERPARTY, isCollectionSwap.value), + surchargeDirection: 'Receive', + }, + [SwapStep.OFFERED]: { + title: 'swap.yourOffer', + surchargeTitle: 'swap.addToken', + nextRouteName: getSwapStepRouteName(SwapStep.REVIEW, isCollectionSwap.value), + backRouteName: getSwapStepRouteName(SwapStep.DESIRED, isCollectionSwap.value), + surchargeDirection: 'Send', + }, +})) const target = ref() const amount = ref() @@ -168,13 +170,15 @@ const itemsContainer = ref() const isTargetVisible = useElementVisibility(target) const stepItems = computed(() => swapStore.getStepItems(props.step)) -const stepDetails = computed(() => stepDetailsMap[props.step] as StepDetails) +const stepDetails = computed(() => stepDetailsMap.value[props.step] as StepDetails) const title = computed(() => $i18n.t(stepDetails.value.title)) const surchargeTitle = computed(() => $i18n.t(stepDetails.value.surchargeTitle)) const surchargeDisabled = computed(() => Boolean(swap.value.surcharge)) const stepHasSurcharge = computed(() => swap.value.surcharge?.direction === stepDetails.value.surchargeDirection) const count = computed(() => stepItems.value.length + (stepHasSurcharge.value ? 1 : 0)) const isOverOneToOneSwap = computed(() => swap.value.offered.length > swap.value.desired.length && props.step === SwapStep.OFFERED) +const isCollectionSwapDesired = computed(() => isCollectionSwap.value && props.step === SwapStep.DESIRED) + const disabled = computed(() => { if ((!accountId.value && props.step === SwapStep.OFFERED) || isOverOneToOneSwap.value) { return true diff --git a/components/swap/PreviewItem.vue b/components/swap/PreviewItem.vue index 64a29f9e85..f49a6dfb98 100644 --- a/components/swap/PreviewItem.vue +++ b/components/swap/PreviewItem.vue @@ -22,6 +22,7 @@ () diff --git a/components/swap/banner/CollectionPreview.vue b/components/swap/banner/CollectionPreview.vue new file mode 100644 index 0000000000..8001b397aa --- /dev/null +++ b/components/swap/banner/CollectionPreview.vue @@ -0,0 +1,53 @@ + + + diff --git a/components/swap/banner/accounts.vue b/components/swap/banner/accounts.vue index 74bd74fdde..888508f9ec 100644 --- a/components/swap/banner/accounts.vue +++ b/components/swap/banner/accounts.vue @@ -7,6 +7,7 @@ @@ -21,7 +22,15 @@
{{ $t('swap.counterparty') }}
- + + @@ -32,5 +41,6 @@ import { NeoIcon } from '@kodadot1/brick' defineProps<{ creator?: string counterparty: string + isCollectionSwap?: boolean }>() diff --git a/components/swap/collection/DestinationProfile.vue b/components/swap/collection/DestinationProfile.vue new file mode 100644 index 0000000000..fa344e98ce --- /dev/null +++ b/components/swap/collection/DestinationProfile.vue @@ -0,0 +1,35 @@ + + + diff --git a/components/swap/layout/index.vue b/components/swap/layout/index.vue index 7c7a3e8037..b473919865 100644 --- a/components/swap/layout/index.vue +++ b/components/swap/layout/index.vue @@ -6,6 +6,7 @@ diff --git a/components/swap/review.vue b/components/swap/review.vue index 28b831bfaf..9a8089c04b 100644 --- a/components/swap/review.vue +++ b/components/swap/review.vue @@ -55,7 +55,36 @@ {{ $t('swap.reviewCounterpartyAccept') }}

+
+ + + + + +
import { NeoIcon, NeoButton } from '@kodadot1/brick' import { successMessage } from '@/utils/notification' +import ItemsGridImage from '@/components/items/ItemsGrid/ItemsGridImage.vue' +import type { NFTWithMetadata } from '@/composables/useNft' +import { SwapStep } from '@/components/swap/types' const router = useRouter() const { $i18n } = useNuxtApp() @@ -122,6 +154,8 @@ const { swap } = storeToRefs(swapStore) const offeredQuery = computed(() => ({ id_in: swap.value?.offered.map(item => item.id) })) const desiredQuery = computed(() => ({ id_in: swap.value?.desired.map(item => item.id) })) +const desiredNfts = computed(() => swap.value?.desired as NFTWithMetadata[]) + const toTokenToSwap = (item: SwapItem) => ({ id: item.id, collectionId: item.collectionId, @@ -130,6 +164,10 @@ const toTokenToSwap = (item: SwapItem) => ({ const surcharge = computed(() => swap.value?.surcharge) +const onModifyOfferClick = () => { + router.push({ name: getSwapStepRouteName(SwapStep.DESIRED, swap.value?.isCollectionSwap), params: { id: swap.value?.counterparty }, query: { swapId: swap.value?.id } }) +} + const submit = () => { if (!swap.value) { return diff --git a/components/trade/TradeActivityTable.vue b/components/trade/TradeActivityTable.vue index 90aa9ad09b..0b13eb2bd4 100644 --- a/components/trade/TradeActivityTable.vue +++ b/components/trade/TradeActivityTable.vue @@ -22,6 +22,7 @@ +
@@ -100,6 +101,7 @@ diff --git a/stores/atomicSwaps.ts b/stores/atomicSwaps.ts index 92f6ca7a94..a4beb5b9ea 100644 --- a/stores/atomicSwaps.ts +++ b/stores/atomicSwaps.ts @@ -18,13 +18,14 @@ export type AtomicSwap = { surcharge?: SwapSurcharge duration: number blockNumber?: string + isCollectionSwap?: boolean } & CartItem export type SwapItem = { - id: string + id: string | null name: string collectionId: string - sn: string + sn: string | null meta: any } @@ -62,6 +63,7 @@ export const useAtomicSwapStore = defineStore('atomicSwap', () => { const newAtomicSwap: AtomicSwap = { id: window.crypto.randomUUID().split('-')[0], counterparty, + isCollectionSwap: false, offered: [], desired: [], createdAt: Date.now(), diff --git a/utils/swap.ts b/utils/swap.ts index f94c3d53ac..083fe702de 100644 --- a/utils/swap.ts +++ b/utils/swap.ts @@ -2,22 +2,33 @@ import { SwapStep } from '@/components/swap/types' import type { TradeToken, TradeNftItem } from '@/components/trade/types' import type { NFT } from '@/types' -export const SWAP_ROUTE_NAME_STEP_MAP = { +const SWAP_ROUTE_NAME_STEP_MAP = { 'prefix-swap': SwapStep.COUNTERPARTY, 'prefix-swap-id': SwapStep.DESIRED, 'prefix-swap-id-offer': SwapStep.OFFERED, 'prefix-swap-id-review': SwapStep.REVIEW, } +const COLLECTION_SWAP_ROUTE_NAME_STEP_MAP = { + 'prefix-collection-id-swaps': SwapStep.COUNTERPARTY, + 'prefix-swap-collection-id': SwapStep.DESIRED, + 'prefix-swap-id-offer': SwapStep.OFFERED, + 'prefix-swap-id-review': SwapStep.REVIEW, +} + export const ATOMIC_SWAP_PAGES = [ 'prefix-swap-id', + 'prefix-swap-collection-id', 'prefix-swap-id-offer', 'prefix-swap-id-review', ] -export const getSwapStepRouteName = (step: SwapStep) => { - const index = Object.values(SWAP_ROUTE_NAME_STEP_MAP).findIndex(name => name === step) - return Object.keys(SWAP_ROUTE_NAME_STEP_MAP)[index] +export const getRouteNameStepMap = (isCollectionSwap?: boolean) => isCollectionSwap ? COLLECTION_SWAP_ROUTE_NAME_STEP_MAP : SWAP_ROUTE_NAME_STEP_MAP + +export const getSwapStepRouteName = (step: SwapStep, isCollectionSwap?: boolean) => { + const routeNameStepMap = getRouteNameStepMap(isCollectionSwap) + const index = Object.values(routeNameStepMap).findIndex(name => name === step) + return Object.keys(routeNameStepMap)[index] } export const getSwapStep = (swap: AtomicSwap): SwapStep => { @@ -48,7 +59,7 @@ export const getStepItemsKey = (step: SwapStep) => { export const navigateToSwap = (swap: AtomicSwap) => { navigateTo({ - name: 'prefix-swap-id', + name: getSwapStepRouteName(SwapStep.DESIRED, swap.isCollectionSwap), params: { id: swap.counterparty }, query: { swapId: swap.id }, }) From 8366465a4e7dda2f5638ddf441ccf6b04f15fe13 Mon Sep 17 00:00:00 2001 From: Jarsen <31397967+Jarsen136@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:38:21 +0100 Subject: [PATCH 2/5] refactor: props --- components/collection/CollectionTrades.vue | 11 ++++-- components/collection/drop/ItemsGrid.vue | 2 +- components/items/ItemsGrid/ItemsGrid.vue | 6 +-- components/items/ItemsGrid/ItemsGridImage.vue | 4 +- components/shared/nftCard/NftCard.vue | 4 +- components/shared/nftCard/NftMediaInfo.vue | 6 +-- components/swap/Preview.vue | 2 +- components/swap/PreviewItem.vue | 17 +++++--- components/swap/collection/SwapGridList.vue | 39 +++++++++++++++++++ components/swap/review.vue | 33 +--------------- components/trade/TradeActivityTable.vue | 4 +- 11 files changed, 73 insertions(+), 55 deletions(-) create mode 100644 components/swap/collection/SwapGridList.vue diff --git a/components/collection/CollectionTrades.vue b/components/collection/CollectionTrades.vue index 05c2c65588..1c4b0b9605 100644 --- a/components/collection/CollectionTrades.vue +++ b/components/collection/CollectionTrades.vue @@ -12,15 +12,20 @@ :key="key" :query="tradeQuery" :type="tradeType" - :enable-collection-swap="tradeType === TradeType.SWAP" - /> + > + + diff --git a/components/swap/collection/SwapGridList.vue b/components/swap/collection/SwapGridList.vue new file mode 100644 index 0000000000..750ba60a78 --- /dev/null +++ b/components/swap/collection/SwapGridList.vue @@ -0,0 +1,39 @@ + + + diff --git a/components/swap/review.vue b/components/swap/review.vue index 9a8089c04b..39331091bc 100644 --- a/components/swap/review.vue +++ b/components/swap/review.vue @@ -55,34 +55,9 @@ {{ $t('swap.reviewCounterpartyAccept') }}

-
- - - - - -
+ /> import { NeoIcon, NeoButton } from '@kodadot1/brick' import { successMessage } from '@/utils/notification' -import ItemsGridImage from '@/components/items/ItemsGrid/ItemsGridImage.vue' -import type { NFTWithMetadata } from '@/composables/useNft' import { SwapStep } from '@/components/swap/types' const router = useRouter() @@ -154,8 +127,6 @@ const { swap } = storeToRefs(swapStore) const offeredQuery = computed(() => ({ id_in: swap.value?.offered.map(item => item.id) })) const desiredQuery = computed(() => ({ id_in: swap.value?.desired.map(item => item.id) })) -const desiredNfts = computed(() => swap.value?.desired as NFTWithMetadata[]) - const toTokenToSwap = (item: SwapItem) => ({ id: item.id, collectionId: item.collectionId, diff --git a/components/trade/TradeActivityTable.vue b/components/trade/TradeActivityTable.vue index 0b13eb2bd4..9a4ab4a4b5 100644 --- a/components/trade/TradeActivityTable.vue +++ b/components/trade/TradeActivityTable.vue @@ -22,7 +22,7 @@
- +
@@ -101,7 +101,6 @@ diff --git a/components/trade/makeOffer/MakeOfferModal.vue b/components/trade/makeOffer/MakeOfferModal.vue index c89d2c5a6a..0a27abecbd 100644 --- a/components/trade/makeOffer/MakeOfferModal.vue +++ b/components/trade/makeOffer/MakeOfferModal.vue @@ -26,7 +26,10 @@
- +
diff --git a/components/trade/makeOffer/MakeOfferSingleItem.vue b/components/trade/makeOffer/MakeOfferSingleItem.vue index 09c642c301..d7f6bf1151 100644 --- a/components/trade/makeOffer/MakeOfferSingleItem.vue +++ b/components/trade/makeOffer/MakeOfferSingleItem.vue @@ -2,7 +2,10 @@
@@ -61,6 +64,7 @@ const emit = defineEmits([ const props = defineProps<{ offerPrice?: number + showPrice?: boolean }>() const offerPrice = useVModel(props, 'offerPrice') diff --git a/components/trade/types.ts b/components/trade/types.ts index 1dc3658c69..467cfe3aea 100644 --- a/components/trade/types.ts +++ b/components/trade/types.ts @@ -11,13 +11,13 @@ export type MakingOfferItem = { highestOffer?: string offerPrice?: string offerExpiration?: number - id: string + id: string | null name: string currentOwner: string collection: EntityWithId & CollectionFloorPrice meta?: NFTMetadata metadata: string - sn: string + sn: string | null } export enum TradeStatus { diff --git a/composables/useTradeActionClick.ts b/composables/useTradeActionClick.ts index b84bc9be63..4242acad34 100644 --- a/composables/useTradeActionClick.ts +++ b/composables/useTradeActionClick.ts @@ -1,7 +1,7 @@ import { useIdentityStore } from '@/stores/identity' import { doAfterCheckCurrentChainVM } from '@/components/common/ConnectWallet/openReconnectWalletModal' -export default function (isOwner: ComputedRef) { +export default function (disabled?: ComputedRef) { const identityStore = useIdentityStore() const { doAfterLogin } = useDoAfterlogin() @@ -9,7 +9,7 @@ export default function (isOwner: ComputedRef) { const onTradeActionClick = (cb: () => void) => { const fn = () => { - if (!isOwner.value) { + if (!disabled || !disabled.value) { cb() } } diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 4fc99a2d2c..c79cedabe0 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -1522,9 +1522,11 @@ "notifications": "Notifications" }, "offer": { + "anyNftFromCollection": "Any NFT", "bestOffer": "Best Offer", "cancelOffer": "Cancel Offer", "collectionFloorPrice": "Collection Floor", + "createCollectionOffer": "Create Collection Offer", "emptyInput": "Please enter your offer", "expiration": "Offer Expiration", "floorDifference": "Floor Difference", diff --git a/queries/subsquid/general/collectionByIdMinimalWithRoyalty.graphql b/queries/subsquid/general/collectionByIdMinimalWithRoyalty.graphql index 20cb384607..d0aed4649b 100644 --- a/queries/subsquid/general/collectionByIdMinimalWithRoyalty.graphql +++ b/queries/subsquid/general/collectionByIdMinimalWithRoyalty.graphql @@ -19,5 +19,12 @@ query collectionByIdMinimalWithRoyalty($id: String!) { name currentOwner createdAt + floorPrice: nfts( + where: { burned_eq: false, price_not_eq: "0" } + orderBy: price_ASC + limit: 1 + ) { + price + } } } diff --git a/queries/subsquid/general/highestOfferByCollectionId.graphql b/queries/subsquid/general/highestOfferByCollectionId.graphql new file mode 100644 index 0000000000..5e66a57662 --- /dev/null +++ b/queries/subsquid/general/highestOfferByCollectionId.graphql @@ -0,0 +1,8 @@ +query highestOfferByCollectionId($id: String!) { + offers(where: {status_eq: ACTIVE, desired: {collectionId_eq: $id}}, orderBy: price_DESC, limit: 1) { + expiration + status + price + id + } +} \ No newline at end of file From aa621ec31af122dd2d6adce86316aaae29a28110 Mon Sep 17 00:00:00 2001 From: Jarsen <31397967+Jarsen136@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:25:35 +0100 Subject: [PATCH 4/5] chore: types --- components/collection/CollectionTrades.vue | 9 ++++++--- .../trade/makeOffer/CreateCollectionOfferButton.vue | 4 ++-- composables/useTradeActionClick.ts | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/components/collection/CollectionTrades.vue b/components/collection/CollectionTrades.vue index 107efcba06..821a7af9c7 100644 --- a/components/collection/CollectionTrades.vue +++ b/components/collection/CollectionTrades.vue @@ -13,13 +13,16 @@ :query="tradeQuery" :type="tradeType" > -