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' }