diff --git a/components/collection/CollectionTrades.vue b/components/collection/CollectionTrades.vue index be9de7d98c..821a7af9c7 100644 --- a/components/collection/CollectionTrades.vue +++ b/components/collection/CollectionTrades.vue @@ -12,7 +12,21 @@ :key="key" :query="tradeQuery" :type="tradeType" - /> + > + + @@ -20,6 +34,9 @@ diff --git a/components/swap/Preview.vue b/components/swap/Preview.vue index 1b913bdddd..c3c1566055 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" + :removable="!(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..a0a690c53b 100644 --- a/components/swap/PreviewItem.vue +++ b/components/swap/PreviewItem.vue @@ -22,6 +22,7 @@ () +withDefaults( + defineProps<{ + name?: string + image: string + imageClass?: string + removable?: boolean + }>(), + { + removable: true, + }, +) 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/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/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 ff7ef6805a..d61a30440c 100644 --- a/components/swap/review.vue +++ b/components/swap/review.vue @@ -55,7 +55,11 @@ {{ $t('swap.reviewCounterpartyAccept') }}

+ import { NeoIcon, NeoButton } from '@kodadot1/brick' import { successMessage } from '@/utils/notification' +import { SwapStep } from '@/components/swap/types' const router = useRouter() const { $i18n } = useNuxtApp() @@ -131,6 +136,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..9a4ab4a4b5 100644 --- a/components/trade/TradeActivityTable.vue +++ b/components/trade/TradeActivityTable.vue @@ -22,6 +22,7 @@ +
@@ -111,7 +112,6 @@ const props = defineProps<{ const route = useRoute() const { replaceUrl } = useReplaceUrl() - const dataKey = TRADES_QUERY_MAP[props.type].dataKey const selectedTrade = ref() diff --git a/components/trade/makeOffer/CreateCollectionOfferButton.vue b/components/trade/makeOffer/CreateCollectionOfferButton.vue new file mode 100644 index 0000000000..3e72c570e0 --- /dev/null +++ b/components/trade/makeOffer/CreateCollectionOfferButton.vue @@ -0,0 +1,66 @@ + + + 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/transaction/transactionCreateSwap.ts b/composables/transaction/transactionCreateSwap.ts index db0e08ea13..7a52181fb7 100644 --- a/composables/transaction/transactionCreateSwap.ts +++ b/composables/transaction/transactionCreateSwap.ts @@ -18,7 +18,7 @@ async function execCreateSwapStatmine({ item, api, executeTransaction, isLoading const swap = api.tx.nfts.createSwap( offeredCollectionId, - offeredItem, + offeredItem as string, desiredCollectionId, desiredItem, item.surcharge diff --git a/composables/transaction/types.ts b/composables/transaction/types.ts index 21813d1121..cd8a105887 100644 --- a/composables/transaction/types.ts +++ b/composables/transaction/types.ts @@ -190,9 +190,9 @@ export type SwapSurchargeDirection = 'Send' | 'Receive' export type SwapSurcharge = { amount: string, direction: SwapSurchargeDirection } export type TokenToSwap = { - id: string + id: string | null collectionId: string - sn: string + sn: string | null } export type ActionSwap = { diff --git a/composables/useTradeActionClick.ts b/composables/useTradeActionClick.ts new file mode 100644 index 0000000000..f2d0a4ef2b --- /dev/null +++ b/composables/useTradeActionClick.ts @@ -0,0 +1,28 @@ +import { useIdentityStore } from '@/stores/identity' +import { doAfterCheckCurrentChainVM } from '@/components/common/ConnectWallet/openReconnectWalletModal' + +export default function (disabled?: ComputedRef) { + const identityStore = useIdentityStore() + const { doAfterLogin } = useDoAfterlogin() + + const isLogIn = computed(() => Boolean(identityStore.getAuthAddress)) + + const onTradeActionClick = (cb: () => void) => { + const fn = () => { + if (!disabled?.value) { + cb() + } + } + + if (isLogIn.value) { + doAfterCheckCurrentChainVM(fn) + } + else { + doAfterLogin({ onLoginSuccess: fn }) + } + } + + return { + onTradeActionClick, + } +} diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 398f82345d..9b485416a1 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -1521,9 +1521,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", @@ -1838,13 +1840,17 @@ "swap": { "acceptSwap": "Accepting Swap", "addToken": "Add Token", + "anyNftFromCollection": "Any NFT from \"{0}\" Collection", "beginSwap": "Begin Swap offer", "cantSwapWithYourself": "You can't swap with yourself", "clickOnNft": "Click on any NFT to add it to your swap list.", + "collectionSwapDescription": "Any NFT from this collection will be offered in the swap", + "collectionSwapPreview": "Collection Swap", "connectTrader": "Connect with a trader", "connectTraderInfo": "Enter the wallet address of the trader you want to engage with, and we’ll guide you through the secure process of making a swap offer.", "counterSwap": "Counter Swap", "counterparty": "Counterparty", + "createCollectionSwap": "Create Collection Swap", "createSwap": "Create Swap", "created": "Swap Created", "creatingSwap": "Creating Swap", diff --git a/middleware/redirects.global.ts b/middleware/redirects.global.ts index 63396564ce..9f1dd8ff63 100644 --- a/middleware/redirects.global.ts +++ b/middleware/redirects.global.ts @@ -1,5 +1,6 @@ import { type Prefix } from '@kodadot1/static' import type { RouteLocationRaw, RouteLocationNormalizedLoadedGeneric } from 'vue-router' +import { isAddress } from '@polkadot/util-crypto' import { createVisible, transferVisible, teleportVisible, migrateVisible, swapVisible } from '@/utils/config/permission.config' type ReplaceRouteItemCondition = (route: RouteLocationNormalizedLoadedGeneric) => boolean @@ -14,6 +15,10 @@ const getFormatAddressRouteCondition = (cond: ReplaceRouteItemCondition, { addre cond, replaceRoute: ({ params, name, query }) => { const address = params[addressKey].toString() + + if (!isAddress(address)) { + return + } const prefix = params.prefix.toString() as Prefix return execByVm({ diff --git a/middleware/swap.ts b/middleware/swap.ts index 810d7e2cce..537ecb790c 100644 --- a/middleware/swap.ts +++ b/middleware/swap.ts @@ -1,4 +1,5 @@ import { type Prefix } from '@kodadot1/static' +import { isAddress } from '@polkadot/util-crypto' import { SwapStep } from '@/components/swap/types' export default defineNuxtRouteMiddleware((to) => { @@ -9,13 +10,10 @@ export default defineNuxtRouteMiddleware((to) => { const swapId = to.query.swapId?.toString() const id = to.params.id?.toString() const routeName = to.name?.toString() - if (!id || !routeName) { return navigateTo({ name: getSwapStepRouteName(SwapStep.COUNTERPARTY) }) } - const routeStep = SWAP_ROUTE_NAME_STEP_MAP[routeName] - const foundSwap = items.value .filter(item => item.counterparty === id @@ -24,24 +22,28 @@ export default defineNuxtRouteMiddleware((to) => { )[0] if (!foundSwap) { - return navigateTo({ - name: getSwapStepRouteName(SwapStep.DESIRED), - params: { id, prefix }, - query: { swapId: swapStore.createSwap(id).id }, - }) + if (isAddress(id)) { + return navigateTo({ + name: getSwapStepRouteName(SwapStep.DESIRED), + params: { id, prefix }, + query: { swapId: swapStore.createSwap(id).id }, + }) + } + return navigateTo({ name: getSwapStepRouteName(SwapStep.COUNTERPARTY, true), params: { id, prefix } }) } + const isCollectionSwap = foundSwap.isCollectionSwap swap.value = foundSwap const swapStep = getSwapStep(swap.value) if (swapStep === SwapStep.CREATED) { - return navigateTo({ name: getSwapStepRouteName(SwapStep.COUNTERPARTY), params: { prefix } }) + return navigateTo({ name: getSwapStepRouteName(SwapStep.COUNTERPARTY, isCollectionSwap), params: { prefix } }) } - + const routeStep = getRouteNameStepMap(isCollectionSwap)[routeName] step.value = routeStep if (routeStep > swapStep) { - return navigateTo({ name: getSwapStepRouteName(swapStep), params: { id, prefix }, query: { swapId: swap.value.id } }) + return navigateTo({ name: getSwapStepRouteName(swapStep, isCollectionSwap), params: { id, prefix }, query: { swapId: swap.value.id } }) } }) diff --git a/pages/[prefix]/swap/collection/[id]/index.vue b/pages/[prefix]/swap/collection/[id]/index.vue new file mode 100644 index 0000000000..f6ba535df6 --- /dev/null +++ b/pages/[prefix]/swap/collection/[id]/index.vue @@ -0,0 +1,10 @@ + + + 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 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 }, })