diff --git a/components/common/listingCart/ListingCartModal.vue b/components/common/listingCart/ListingCartModal.vue
index 6468a9ad44..70f4edfb4c 100644
--- a/components/common/listingCart/ListingCartModal.vue
+++ b/components/common/listingCart/ListingCartModal.vue
@@ -2,6 +2,7 @@
{
const { action, autoTeleport, autoTeleportButton, autoTeleportLoaded, formattedTxFees } = useAutoTeleportActionButton({
getActionFn: () => getAction(listingCartStore.itemsInChain),
+ signingModal,
})
const actions = computed(() => [
diff --git a/components/common/userCart/UserCartModal.vue b/components/common/userCart/UserCartModal.vue
index a46d9b137f..879fd7040b 100644
--- a/components/common/userCart/UserCartModal.vue
+++ b/components/common/userCart/UserCartModal.vue
@@ -106,7 +106,7 @@ const props = withDefaults(defineProps<{
const preferencesStore = usePreferencesStore()
const listingCartStore = useListingCartStore()
-const signingModal = ref<{ isModalActive: boolean }>()
+const signingModal = ref()
const items = ref([])
const { $i18n } = useNuxtApp()
@@ -120,11 +120,12 @@ const isModalActive = computed(() => Boolean(preferencesStore.userCartModal?.ope
const nft = computed(() => items.value[0])
const abi = useCollectionAbi(computed(() => nft.value?.collection.id), { disabled: !isEvm.value })
const hasAbi = computed(() => isEvm.value ? Boolean(abi.value) : true)
-const actionDisabled = computed(() => !hasAbi.value || Boolean(signingModal.value?.isModalActive))
+const actionDisabled = computed(() => !hasAbi.value)
const { action, autoTeleport, autoTeleportButton, autoTeleportLoaded, formattedTxFees, isActionReady } = useAutoTeleportActionButton({
getActionFn: props.getAction,
disabled: actionDisabled,
+ signingModal,
})
const actions = computed(() =>
diff --git a/components/trade/makeOffer/MakeOfferModal.vue b/components/trade/makeOffer/MakeOfferModal.vue
index 27cff78a5b..c89d2c5a6a 100644
--- a/components/trade/makeOffer/MakeOfferModal.vue
+++ b/components/trade/makeOffer/MakeOfferModal.vue
@@ -2,6 +2,7 @@
import { NeoModal } from '@kodadot1/brick'
+import { shuffle } from 'lodash'
import type { MakingOfferItem } from '@/components/trade/types'
import MakeOfferSingleItem from '@/components/trade/makeOffer/MakeOfferSingleItem.vue'
import ModalBody from '@/components/shared/modals/ModalBody.vue'
import { usePreferencesStore } from '@/stores/preferences'
-import type { Actions, TokenToOffer } from '@/composables/transaction/types'
+import type { ActionOffer, TokenToOffer } from '@/composables/transaction/types'
import { useMakingOfferStore } from '@/stores/makeOffer'
import format, { calculateBalance } from '@/utils/format/balance'
import { warningMessage } from '@/utils/notification'
@@ -76,7 +78,9 @@ import { hasOperationsDisabled } from '@/utils/prefix'
import { offerVisible } from '@/utils/config/permission.config'
import useAutoTeleportActionButton from '@/composables/autoTeleport/useAutoTeleportActionButton'
import { sum } from '@/utils/math'
-import { OFFER_MINT_PRICE } from '@/composables/transaction/transactionOffer'
+import { OFFER_MINT_PRICE, getOfferCollectionId } from '@/composables/transaction/transactionOffer'
+
+const DEFAULT_OFFER_EXPIRATION_DURATION = 7
const { urlPrefix } = usePrefix()
const preferencesStore = usePreferencesStore()
@@ -97,17 +101,23 @@ const { itemsInChain, hasInvalidOfferPrices, count } = storeToRefs(offerStore)
const { decimals, chainSymbol } = useChain()
const { $i18n } = useNuxtApp()
+
+const signingModal = ref()
const items = ref([])
+const unusedOfferedItemsSubscription = ref(() => {})
+const usedOfferedItems = ref([])
+const offeredItem = ref()
-const getAction = (items: MakingOfferItem[]): Actions => {
+const getAction = (items: MakingOfferItem[]): ActionOffer => {
return {
interaction: ShoppingActions.MAKE_OFFER,
urlPrefix: urlPrefix.value,
- token: items.map(item => ({
- price: String(calculateBalance(Number(item.offerPrice), decimals.value)),
- collectionId: item.collection.id,
- duration: item.offerExpiration || 7,
- nftSn: item.sn,
+ tokens: items.map(item => ({
+ price: item.offerPrice ? String(Number(calculateBalance(Number(item.offerPrice), decimals.value))) : '',
+ desiredItem: item.sn,
+ desiredCollectionId: item.collection.id,
+ offeredItem: offeredItem.value,
+ duration: item.offerExpiration || DEFAULT_OFFER_EXPIRATION_DURATION,
} as TokenToOffer)),
}
}
@@ -120,15 +130,16 @@ const teleportTransitionTxFees = computed(() =>
),
)
-const { action, autoTeleport, autoTeleportButton, autoTeleportLoaded } = useAutoTeleportActionButton({
+const { action, autoTeleport, autoTeleportButton, autoTeleportLoaded } = useAutoTeleportActionButton({
getActionFn: () => getAction(itemsInChain.value),
+ signingModal,
})
const totalOfferAmount = computed(
() => calculateBalance(sum(itemsInChain.value.map(nft => Number(nft.offerPrice))), decimals.value),
)
-const totalNeededAmount = computed(() => totalOfferAmount.value + OFFER_MINT_PRICE)
+const totalNeededAmount = computed(() => totalOfferAmount.value + (!offeredItem.value ? OFFER_MINT_PRICE : 0))
const actions = computed(() => [
{
@@ -176,6 +187,7 @@ const submitOffer = () => {
async function confirm({ autoteleport }: AutoTeleportActionButtonConfirmEvent) {
try {
clearTransaction()
+ unusedOfferedItemsSubscription.value()
autoTeleport.value = autoteleport
items.value = [...itemsInChain.value]
@@ -210,6 +222,53 @@ useModalIsOpenTracker({
isOpen: computed(() => preferencesStore.makeOfferModalOpen),
onChange: () => {
offerStore.clear()
+ unusedOfferedItemsSubscription.value()
+ },
+})
+
+useModalIsOpenTracker({
+ isOpen: computed(() => preferencesStore.makeOfferModalOpen),
+ onClose: false,
+ onChange: () => {
+ unusedOfferedItemsSubscription.value = useSubscriptionGraphql({
+ query: `
+ offers(where: {
+ status_not_in: [ACTIVE, ACCEPTED]
+ caller_eq: "${accountId.value}"
+ nft: {
+ currentOwner_eq: "${accountId.value}"
+ collection: {
+ id_eq: "${getOfferCollectionId(urlPrefix.value)}"
+ }
+ }
+ }) {
+ nft {
+ sn
+ }
+ }`,
+ onChange: ({ data: { offers: items } }) => {
+ const tokensSn = items
+ .map(({ nft }) => nft.sn)
+ .filter((tokenSn: string) => !usedOfferedItems.value.includes(tokenSn))
+
+ const unusedOfferedItems = shuffle(tokensSn)
+
+ offeredItem.value = unusedOfferedItems[0]
+ },
+ })
+ },
+})
+
+useTransactionTracker({
+ transaction: {
+ isError,
+ status,
+ },
+ onSuccess: () => {
+ if (offeredItem.value) {
+ usedOfferedItems.value.push(offeredItem.value)
+ offeredItem.value = undefined
+ }
},
})
diff --git a/composables/autoTeleport/useAutoTeleportActionButton.ts b/composables/autoTeleport/useAutoTeleportActionButton.ts
index 934b9d1a39..61e6bd7139 100644
--- a/composables/autoTeleport/useAutoTeleportActionButton.ts
+++ b/composables/autoTeleport/useAutoTeleportActionButton.ts
@@ -3,10 +3,12 @@ import type { Actions } from '@/composables/transaction/types'
type AutoTeleportActionButtonParams = {
getActionFn: () => T
disabled?: MaybeRef
+ signingModal: Ref<{ isModalActive: boolean } | undefined>
}
export default ({
getActionFn,
+ signingModal,
disabled = false,
}: AutoTeleportActionButtonParams) => {
const { decimals, chainSymbol } = useChain()
@@ -30,7 +32,7 @@ export default ({
)
watchSyncEffect(() => {
- if (!autoTeleport.value && !unref(disabled)) {
+ if (!autoTeleport.value && !unref(disabled) && !signingModal.value?.isModalActive) {
action.value = getActionFn()
}
})
diff --git a/composables/autoTeleport/utils.ts b/composables/autoTeleport/utils.ts
index f0196c8796..03495d7da2 100644
--- a/composables/autoTeleport/utils.ts
+++ b/composables/autoTeleport/utils.ts
@@ -1,7 +1,7 @@
import { Interaction } from '@kodadot1/minimark/v1'
import { teleportExistentialDeposit } from '@kodadot1/static'
import type { AutoTeleportAction } from './types'
-import type { ActionList, Actions, ActionSend } from '@/composables/transaction/types'
+import type { ActionList, Actions, ActionSend, ActionOffer } from '@/composables/transaction/types'
import type { Chain } from '@/utils/teleport'
export const getChainExistentialDeposit = (
@@ -24,6 +24,7 @@ const checkIfActionNeedsRefetch = (
const validityMap: Record boolean> = {
[Interaction.LIST]: (curent: ActionList, prev: ActionList) => lengthChanged(curent.token, prev.token),
[Interaction.SEND]: (curent: ActionSend, prev: ActionSend) => lengthChanged(curent.nfts, prev.nfts),
+ [ShoppingActions.MAKE_OFFER]: (curent: ActionOffer, prev: ActionOffer) => lengthChanged(curent.tokens, prev.tokens),
}
const checker = validityMap[action.interaction] || null
diff --git a/composables/transaction/transactionOffer.ts b/composables/transaction/transactionOffer.ts
index a3b769f39c..178cf410ef 100644
--- a/composables/transaction/transactionOffer.ts
+++ b/composables/transaction/transactionOffer.ts
@@ -1,3 +1,5 @@
+import type { ApiPromise } from '@polkadot/api'
+import type { SubmittableExtrinsic } from '@polkadot/api-base/types'
import type { Prefix } from '@kodadot1/static'
import type { ActionOffer } from './types'
import { generateId } from '@/services/dyndata'
@@ -17,35 +19,44 @@ export const OFFER_MINT_PRICE = 5e8
export const BLOCKS_PER_DAY = 300 * 24 // 12sec /block --> 300blocks/hr
-async function execMakingOffer(item: ActionOffer, api, executeTransaction) {
+async function execMakingOffer(item: ActionOffer, api: ApiPromise, executeTransaction) {
const { accountId } = useAuth()
- const nfts = Array.isArray(item.token) ? item.token : [item.token]
+
const transactions = await Promise.all(
- nfts.map(async ({ price, nftSn, collectionId, duration }) => {
- const offerId = getOfferCollectionId(item.urlPrefix as Prefix)
- const nextId = Number.parseInt(await generateId())
- const create = api.tx.nfts.mint(
- offerId,
- nextId,
- accountId.value,
- {
- mintPrice: String(OFFER_MINT_PRICE),
- },
- )
+ item.tokens.map(async ({ price, desiredItem, desiredCollectionId, duration, offeredItem: offeredSn }) => {
+ const offeredCollectionId = getOfferCollectionId(item.urlPrefix)
+ let offeredItem = Number(offeredSn)
+
+ const transactions: SubmittableExtrinsic<'promise'>[] = []
+
+ if (!offeredItem) {
+ offeredItem = Number.parseInt(await generateId())
+ const create = api.tx.nfts.mint(
+ offeredCollectionId,
+ offeredItem,
+ accountId.value,
+ {
+ mintPrice: String(OFFER_MINT_PRICE),
+ },
+ )
+ transactions.push(create)
+ }
const offer = api.tx.nfts.createSwap(
- offerId,
- nextId,
- collectionId,
- nftSn,
+ offeredCollectionId,
+ offeredItem,
+ desiredCollectionId,
+ desiredItem,
{
- amount: Number(price) || 0,
+ amount: Number(price),
direction: 'Send',
},
BLOCKS_PER_DAY * duration,
)
- return [create, offer]
+ transactions.push(offer)
+
+ return transactions
}),
)
@@ -58,7 +69,7 @@ async function execMakingOffer(item: ActionOffer, api, executeTransaction) {
}
export async function execMakingOfferTx(item: ActionOffer, api, executeTransaction) {
- if (item.urlPrefix === 'ahk' || item.urlPrefix === 'ahp') {
+ if (isAssetHub(item.urlPrefix)) {
await execMakingOffer(item, api, executeTransaction)
}
}
diff --git a/composables/transaction/types.ts b/composables/transaction/types.ts
index e37f444dc4..9ad960867b 100644
--- a/composables/transaction/types.ts
+++ b/composables/transaction/types.ts
@@ -150,8 +150,9 @@ export type TokenToList = {
export type TokenToOffer = {
price: string
- collectionId: string
- nftSn: string
+ desiredItem: string
+ desiredCollectionId: string
+ offeredItem?: string
duration: number
}
@@ -183,8 +184,8 @@ export type ActionSend = {
export type ActionOffer = {
interaction: typeof ShoppingActions.MAKE_OFFER
- urlPrefix: string
- token: TokenToOffer | TokenToOffer[]
+ urlPrefix: Prefix
+ tokens: TokenToOffer[]
successMessage?: string | ((blockNumber: string) => string)
errorMessage?: string
}
diff --git a/composables/transaction/utils.ts b/composables/transaction/utils.ts
index ecc1b3ffb6..096858119f 100644
--- a/composables/transaction/utils.ts
+++ b/composables/transaction/utils.ts
@@ -41,8 +41,8 @@ export const verifyRoyalty = (
}
export function isActionValid(action: Actions): boolean {
- const hasContent = (v: T | T[]): boolean =>
- Array.isArray(v) ? v.length > 0 : Boolean(v)
+ const hasContent = (v: T | T[]): boolean => Array.isArray(v) ? v.length > 0 : Boolean(v)
+ const hasEvery = (v: T[], cb: (item: T) => boolean): boolean => v.every(cb)
const validityMap: Record boolean> = {
[Interaction.BUY]: (action: ActionBuy) => hasContent(action.nfts),
@@ -52,7 +52,7 @@ export function isActionValid(action: Actions): boolean {
[Interaction.SEND]: (action: ActionSend) => Boolean(action.nfts.length),
[Interaction.CONSUME]: (action: ActionConsume) => hasContent(action.nftIds),
[ShoppingActions.MAKE_OFFER]: (action: ActionOffer) =>
- hasContent(action.token),
+ hasContent(action.tokens) && hasEvery(action.tokens, token => Boolean(Number(token.price))),
[ShoppingActions.CANCEL_OFFER]: (action: ActionCancelOffer) =>
Boolean(action.offeredId),
[ShoppingActions.CANCEL_SWAP]: (action: ActionCancelSwap) =>
diff --git a/utils/config/chain.config.ts b/utils/config/chain.config.ts
index 09b4ef140e..fa5f3dab76 100644
--- a/utils/config/chain.config.ts
+++ b/utils/config/chain.config.ts
@@ -18,6 +18,10 @@ export const ss58Of = (prefix: Prefix): number => {
return chainPropListOf(prefix).ss58Format
}
+export const decimalsOf = (prefix: Prefix): number => {
+ return chainPropListOf(prefix).tokenDecimals
+}
+
export const blockExplorerOf = (prefix: Prefix): string | undefined => {
return chainPropListOf(prefix).blockExplorer
}
diff --git a/utils/transactionExecutor.ts b/utils/transactionExecutor.ts
index 9404795710..b2d2bbd661 100644
--- a/utils/transactionExecutor.ts
+++ b/utils/transactionExecutor.ts
@@ -12,7 +12,8 @@ import type { KeyringAccount } from '@/utils/types/types'
import { getAddress } from '@/utils/extension'
import { toDefaultAddress } from '@/utils/account'
import { KODADOT_DAO } from '@/utils/support'
-import type { Actions, ActionsInteractions, ExecuteEvmTransactionParams } from '@/composables/transaction/types'
+import type { Actions, ActionsInteractions, ExecuteEvmTransactionParams, ActionOffer } from '@/composables/transaction/types'
+import { decimalsOf } from '@/utils/config/chain.config'
export type ExecResult = UnsubscribeFn | string
export type Extrinsic = SubmittableExtrinsic<'promise'>
@@ -143,6 +144,16 @@ const estimateEvm = async ({ arg, abi, functionName, account, prefix, address }:
const preProcessedAction: Partial Actions>> = {
[Interaction.SEND]: ({ action, account }) => ({ ...action, address: account }),
+ [ShoppingActions.MAKE_OFFER]: ({ action }) => {
+ action = action as ActionOffer
+ return {
+ ...action,
+ tokens: action.tokens.map(token => ({
+ ...token,
+ price: String(calculateBalance(1, decimalsOf(action.urlPrefix))),
+ })),
+ }
+ },
}
export const getActionTransactionFee = async ({
@@ -155,7 +166,7 @@ export const getActionTransactionFee = async ({
prefix: Prefix
}): Promise => {
// Keep in mind atm actions with ipfs file will be uploadeed
- if ([Interaction.MINT, Interaction.MINTNFT].includes(action.interaction)) {
+ if ([Interaction.MINT, Interaction.MINTNFT].includes(action.interaction as Interaction)) {
console.log('[ACTION FEE]: Fee not allowed', action.interaction)
return '0'
}