Skip to content

Commit

Permalink
Merge pull request #11204 from hassnian/issue-offer-reuse-nft
Browse files Browse the repository at this point in the history
feat: Reuse unused tokens for the next offer
  • Loading branch information
vikiival authored Jan 8, 2025
2 parents 1073e8a + facfc9d commit b589f34
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 43 deletions.
3 changes: 3 additions & 0 deletions components/common/listingCart/ListingCartModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<div>
<SigningModal
v-if="!autoTeleport"
ref="signingModal"
:title="$t('listingCart.listingNft', itemCount)"
:is-loading="isLoading"
:status="status"
Expand Down Expand Up @@ -139,6 +140,7 @@ const { isTransactionSuccessful } = useTransactionSuccessful({
const { chainSymbol, decimals } = useChain()
const signingModal = ref()
const fixedPrice = ref()
const floorPricePercentAdjustment = ref()
Expand Down Expand Up @@ -180,6 +182,7 @@ const getAction = (items: ListCartItem[]): Actions => {
const { action, autoTeleport, autoTeleportButton, autoTeleportLoaded, formattedTxFees } = useAutoTeleportActionButton({
getActionFn: () => getAction(listingCartStore.itemsInChain),
signingModal,
})
const actions = computed<AutoTeleportAction[]>(() => [
Expand Down
5 changes: 3 additions & 2 deletions components/common/userCart/UserCartModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<ListCartItem[]>([])
const { $i18n } = useNuxtApp()
Expand All @@ -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<AutoTeleportAction[]>(() =>
Expand Down
79 changes: 69 additions & 10 deletions components/trade/makeOffer/MakeOfferModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<div>
<SigningModal
v-if="!autoTeleport"
ref="signingModal"
:title="$t('transaction.offer')"
:is-loading="isLoading"
:status="status"
Expand Down Expand Up @@ -59,11 +60,12 @@

<script setup lang="ts">
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'
Expand All @@ -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()
Expand All @@ -97,17 +101,23 @@ const { itemsInChain, hasInvalidOfferPrices, count } = storeToRefs(offerStore)
const { decimals, chainSymbol } = useChain()
const { $i18n } = useNuxtApp()
const signingModal = ref()
const items = ref<MakingOfferItem[]>([])
const unusedOfferedItemsSubscription = ref(() => {})
const usedOfferedItems = ref<string[]>([])
const offeredItem = ref<string>()
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)),
}
}
Expand All @@ -120,15 +130,16 @@ const teleportTransitionTxFees = computed(() =>
),
)
const { action, autoTeleport, autoTeleportButton, autoTeleportLoaded } = useAutoTeleportActionButton({
const { action, autoTeleport, autoTeleportButton, autoTeleportLoaded } = useAutoTeleportActionButton<ActionOffer>({
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<AutoTeleportAction[]>(() => [
{
Expand Down Expand Up @@ -176,6 +187,7 @@ const submitOffer = () => {
async function confirm({ autoteleport }: AutoTeleportActionButtonConfirmEvent) {
try {
clearTransaction()
unusedOfferedItemsSubscription.value()
autoTeleport.value = autoteleport
items.value = [...itemsInChain.value]
Expand Down Expand Up @@ -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
}
},
})
Expand Down
4 changes: 3 additions & 1 deletion composables/autoTeleport/useAutoTeleportActionButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import type { Actions } from '@/composables/transaction/types'
type AutoTeleportActionButtonParams<T> = {
getActionFn: () => T
disabled?: MaybeRef<boolean>
signingModal: Ref<{ isModalActive: boolean } | undefined>
}

export default <T = Actions>({
getActionFn,
signingModal,
disabled = false,
}: AutoTeleportActionButtonParams<T>) => {
const { decimals, chainSymbol } = useChain()
Expand All @@ -30,7 +32,7 @@ export default <T = Actions>({
)

watchSyncEffect(() => {
if (!autoTeleport.value && !unref(disabled)) {
if (!autoTeleport.value && !unref(disabled) && !signingModal.value?.isModalActive) {
action.value = getActionFn()
}
})
Expand Down
3 changes: 2 additions & 1 deletion composables/autoTeleport/utils.ts
Original file line number Diff line number Diff line change
@@ -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 = (
Expand All @@ -24,6 +24,7 @@ const checkIfActionNeedsRefetch = (
const validityMap: Record<string, (...params: any) => 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
Expand Down
51 changes: 31 additions & 20 deletions composables/transaction/transactionOffer.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
}),
)

Expand All @@ -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)
}
}
9 changes: 5 additions & 4 deletions composables/transaction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ export type TokenToList = {

export type TokenToOffer = {
price: string
collectionId: string
nftSn: string
desiredItem: string
desiredCollectionId: string
offeredItem?: string
duration: number
}

Expand Down Expand Up @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions composables/transaction/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export const verifyRoyalty = (
}

export function isActionValid(action: Actions): boolean {
const hasContent = <T>(v: T | T[]): boolean =>
Array.isArray(v) ? v.length > 0 : Boolean(v)
const hasContent = <T>(v: T | T[]): boolean => Array.isArray(v) ? v.length > 0 : Boolean(v)
const hasEvery = <T>(v: T[], cb: (item: T) => boolean): boolean => v.every(cb)

const validityMap: Record<string, (action) => boolean> = {
[Interaction.BUY]: (action: ActionBuy) => hasContent(action.nfts),
Expand All @@ -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) =>
Expand Down
4 changes: 4 additions & 0 deletions utils/config/chain.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
15 changes: 13 additions & 2 deletions utils/transactionExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'>
Expand Down Expand Up @@ -143,6 +144,16 @@ const estimateEvm = async ({ arg, abi, functionName, account, prefix, address }:

const preProcessedAction: Partial<Record<ActionsInteractions, (params: { action: Actions, account: string }) => 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 ({
Expand All @@ -155,7 +166,7 @@ export const getActionTransactionFee = async ({
prefix: Prefix
}): Promise<string> => {
// 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'
}
Expand Down

0 comments on commit b589f34

Please sign in to comment.