From 95a37fb281fb708d5d38d61a5c6512862490024b Mon Sep 17 00:00:00 2001 From: Geoffrey MERRAN Date: Tue, 25 Apr 2023 14:39:09 +0100 Subject: [PATCH 1/4] feat: add claim n mint button to fxparams, allow to claim the chepeast ticket and mint in one batch op --- .../Tables/TableMintTickets/MintTicketRow.tsx | 24 +-- .../Tables/TableMintTickets/index.tsx | 14 +- .../MintWithTicket/MintWithTicketPage.tsx | 39 ++-- .../Panel/ButtonClaimAndMint.tsx | 75 +++++++ .../Panel/PanelControls.module.scss | 5 +- .../MintWithTicket/Panel/PanelControls.tsx | 16 +- src/hooks/useNow.ts | 16 ++ .../slug/[slug]/ticket/[ticketId]/mint.tsx | 57 ++++-- src/queries/generative-token.ts | 35 ++++ src/services/contract-operations/Mint.ts | 2 - .../contract-operations/MintV3Abstraction.ts | 191 ++++++++++++------ .../contract-operations/TicketClaimV3.ts | 2 - .../parameters-builder/BuildParameters.ts | 8 +- .../parameters-builder/claim-ticket/input.ts | 8 + .../parameters-builder/claim-ticket/type.json | 45 +++++ .../mint-with-ticket/input.ts | 6 + .../mint-with-ticket/type.json | 40 ++++ src/utils/mint-ticket.ts | 21 ++ 18 files changed, 467 insertions(+), 137 deletions(-) create mode 100644 src/containers/MintWithTicket/Panel/ButtonClaimAndMint.tsx create mode 100644 src/hooks/useNow.ts create mode 100644 src/services/parameters-builder/claim-ticket/input.ts create mode 100644 src/services/parameters-builder/claim-ticket/type.json create mode 100644 src/services/parameters-builder/mint-with-ticket/input.ts create mode 100644 src/services/parameters-builder/mint-with-ticket/type.json diff --git a/src/components/Tables/TableMintTickets/MintTicketRow.tsx b/src/components/Tables/TableMintTickets/MintTicketRow.tsx index fc1f5aa28..01f0cc515 100644 --- a/src/components/Tables/TableMintTickets/MintTicketRow.tsx +++ b/src/components/Tables/TableMintTickets/MintTicketRow.tsx @@ -2,12 +2,9 @@ import { memo, useContext, useEffect, useRef } from "react" import Link from "next/link" import cs from "classnames" import { - addDays, - differenceInSeconds, format, formatDistanceToNow, isBefore, - subSeconds, } from "date-fns" import { Image } from "components/Image" import { UserContext } from "containers/UserProvider" @@ -19,25 +16,8 @@ import { Button } from "components/Button" import { ButtonUpdatePriceMintTicket } from "components/MintTicket/ButtonUpdatePriceMintTicket" import { ButtonClaimMintTicket } from "components/MintTicket/ButtonClaimMintTicket" import { ApolloCache, useApolloClient } from "@apollo/client" -import { Qu_genTokenMintTickets } from "queries/generative-token" import { GenerativeToken } from "types/entities/GenerativeToken" -import { Frag_MintTicketFull } from "queries/fragments/mint-ticket" - -const getDAPrice = (dateNow: Date, dateEnd: Date, price: number) => { - const dateNowOffset = subSeconds(dateNow, 60) - const dateEndDayLater = addDays(dateEnd, 1) - // end of auction - if (dateEndDayLater < dateNowOffset) { - return 100000 - } - // start of auction - if (dateNowOffset < dateEnd) { - return price - } - const elapsedTimeInSeconds = differenceInSeconds(dateNowOffset, dateEnd) - const daProgressMultiplier = (elapsedTimeInSeconds * 100) / 86400 / 100 - return 100000 + (price - 100000) * (1 - daProgressMultiplier) -} +import { getMintTicketDAPrice } from "../../../utils/mint-ticket"; const moveMintTicketToUnderAuction = ( cache: ApolloCache, @@ -92,7 +72,7 @@ const _MintTicketRow = ({ const price = !isUnderAuction ? mintTicket.price - : getDAPrice(now, dateTaxPaidUntil, mintTicket.price) + : getMintTicketDAPrice(now, dateTaxPaidUntil, mintTicket.price) useEffect(() => { if (!updateCacheOnForeclosure) return diff --git a/src/components/Tables/TableMintTickets/index.tsx b/src/components/Tables/TableMintTickets/index.tsx index e9cea9951..47d5778fb 100644 --- a/src/components/Tables/TableMintTickets/index.tsx +++ b/src/components/Tables/TableMintTickets/index.tsx @@ -1,10 +1,11 @@ -import React, { memo, useEffect, useState } from "react" +import React, { memo } from "react" import cs from "classnames" import { GenTokFlag } from "types/entities/GenerativeToken" import { MintTicket } from "types/entities/MintTicket" import Skeleton from "../../Skeleton" import style from "../TableUser.module.scss" import { MintTicketRow } from "./MintTicketRow" +import useNow from "../../../hooks/useNow"; interface TableMintTicketsProps { firstColName?: string @@ -23,16 +24,7 @@ const _TableMintTickets = ({ refreshEveryMs = 15000, updateCacheOnForeclosure = false, }: TableMintTicketsProps) => { - const [now, setNow] = useState(new Date()) - - useEffect(() => { - const interval = setInterval(() => { - setNow(new Date()) - }, refreshEveryMs) - return () => { - clearInterval(interval) - } - }, [refreshEveryMs]) + const now = useNow(refreshEveryMs) const isInForeclosure = (mintTicket: MintTicket) => { const dateTaxPaidUntil = new Date(mintTicket.taxationPaidUntil) diff --git a/src/containers/MintWithTicket/MintWithTicketPage.tsx b/src/containers/MintWithTicket/MintWithTicketPage.tsx index da0cc2c47..974dc1b65 100644 --- a/src/containers/MintWithTicket/MintWithTicketPage.tsx +++ b/src/containers/MintWithTicket/MintWithTicketPage.tsx @@ -43,21 +43,26 @@ import { ParamConfigurationList } from "./ParamConfigurationList" import { PanelSubmitMode } from "./Panel/PanelControls" import { format } from "date-fns" import { truncateEnd } from "utils/strings" +import { MintTicket } from "../../types/entities/MintTicket" -export type TOnMintHandler = (ticketId: number | number[] | null) => void +export type TOnMintHandler = ( + ticket: MintTicket | MintTicket[] | null, + claimTicket?: boolean +) => void interface Props { token: GenerativeToken - ticketId?: number + ticket?: MintTicket mode?: PanelSubmitMode } -export function MintWithTicketPageRoot({ token, ticketId, mode }: Props) { +export function MintWithTicketPageRoot({ token, ticket, mode }: Props) { const { showTicketPreMintWarning } = useSettingsContext() const [showLoadConfigModal, setShowLoadConfigModal] = useState(false) const [showPreMintWarningView, setShowPreMintWarningView] = useState(false) - const [selectedTicketId, setSelectedTicketId] = useState< - number | number[] | null - >(null) + const [selectedTicket, setSelectedTicket] = useState<{ + ticket: MintTicket | MintTicket[] + claimTicket?: boolean + } | null>(null) const panelParamsRef = useRef(null) const historyContext = useContext(ParamsHistoryContext) const artworkIframeRef = useRef(null) @@ -78,7 +83,7 @@ export function MintWithTicketPageRoot({ token, ticketId, mode }: Props) { const handleClosePreMintView = useCallback(() => { setShowPreMintWarningView(false) - setSelectedTicketId(null) + setSelectedTicket(null) }, []) const { data, setData, hash, setHash, inputBytes } = useFxParams(params) @@ -149,11 +154,12 @@ export function MintWithTicketPageRoot({ token, ticketId, mode }: Props) { // call contract v3 mint with ticket const handleMint: TOnMintHandler = useCallback( - (_ticketId) => { + (_ticket, claimTicket) => { if (inputBytes) { call({ token: token, - ticketId: _ticketId, + ticket: _ticket, + claimTicket, inputBytes: inputBytes, }) } @@ -162,22 +168,21 @@ export function MintWithTicketPageRoot({ token, ticketId, mode }: Props) { ) const handleClickSubmit: TOnMintHandler = useCallback( - (_ticketId) => { - const ticketIdToMint = - mode === "with-ticket" && ticketId ? ticketId : _ticketId + (_ticket, claimTicket) => { + const ticketToMint = mode === "with-ticket" && ticket ? ticket : _ticket if (showTicketPreMintWarning) { setShowPreMintWarningView(true) - setSelectedTicketId(ticketIdToMint) + setSelectedTicket(ticketToMint && { ticket: ticketToMint, claimTicket }) } else { - handleMint(ticketIdToMint) + handleMint(ticketToMint, claimTicket) } }, - [handleMint, mode, showTicketPreMintWarning, ticketId] + [handleMint, mode, showTicketPreMintWarning, ticket] ) const handleValidatePreMint = useCallback(() => { - handleMint(selectedTicketId) - }, [handleMint, selectedTicketId]) + handleMint(selectedTicket?.ticket || null, selectedTicket?.claimTicket) + }, [handleMint, selectedTicket]) const handleSaveConfiguration = () => { if (paramConfigExists) return diff --git a/src/containers/MintWithTicket/Panel/ButtonClaimAndMint.tsx b/src/containers/MintWithTicket/Panel/ButtonClaimAndMint.tsx new file mode 100644 index 000000000..cfbb49bb6 --- /dev/null +++ b/src/containers/MintWithTicket/Panel/ButtonClaimAndMint.tsx @@ -0,0 +1,75 @@ +import React, { memo, useCallback, useContext, useMemo } from "react" +import { useQuery } from "@apollo/client" +import { Qu_genTokenClaimableMintTickets } from "../../../queries/generative-token" +import { BaseButton } from "../../../components/FxParams/BaseInput" +import style from "./PanelControls.module.scss" +import { GenerativeToken } from "../../../types/entities/GenerativeToken" +import useNow from "../../../hooks/useNow" +import { MintTicket } from "../../../types/entities/MintTicket" +import { getMintTicketDAPrice } from "../../../utils/mint-ticket" +import { DisplayTezos } from "../../../components/Display/DisplayTezos" +import { displayMutez } from "../../../utils/units" +import { UserContext } from "../../UserProvider" + +type GenerativeTokenWithDaMintTickets = GenerativeToken & { + daMintTickets: MintTicket[] +} +interface ClaimAndMintProps { + token: GenerativeToken + onClick: (ticket: MintTicket) => void +} + +const _ClaimAndMint = ({ token, onClick }: ClaimAndMintProps) => { + const { user } = useContext(UserContext) + const now = useNow(15000) + const { data } = useQuery<{ + generativeToken: GenerativeTokenWithDaMintTickets + }>(Qu_genTokenClaimableMintTickets, { + variables: { + id: token.id, + ownerId: user?.id || "nobody", + }, + }) + const cheapestTicket = useMemo(() => { + if (!data?.generativeToken) return null + const [cheapestDaTicket] = data.generativeToken.daMintTickets + .map((ticket, idx) => { + return { + ...ticket, + price: getMintTicketDAPrice( + now, + new Date(ticket.taxationPaidUntil), + ticket.price + ), + } + }) + .sort((a, b) => (a.price > b.price ? 1 : -1)) + const [cheapestClaimableTicket] = data.generativeToken.mintTickets + if (cheapestDaTicket && cheapestClaimableTicket) { + return cheapestDaTicket.price < cheapestClaimableTicket.price + ? cheapestDaTicket + : cheapestClaimableTicket + } + return cheapestDaTicket || cheapestClaimableTicket || null + }, [data, now]) + const handleClickClaimAndMint = useCallback(() => { + if (cheapestTicket) { + onClick(cheapestTicket) + } + }, [cheapestTicket, onClick]) + return ( + cheapestTicket && ( + + claim & mint{" "} + + + ) + ) +} + +export const ButtonClaimAndMint = memo(_ClaimAndMint) diff --git a/src/containers/MintWithTicket/Panel/PanelControls.module.scss b/src/containers/MintWithTicket/Panel/PanelControls.module.scss index 496273a8a..7a9397f5f 100644 --- a/src/containers/MintWithTicket/Panel/PanelControls.module.scss +++ b/src/containers/MintWithTicket/Panel/PanelControls.module.scss @@ -9,8 +9,11 @@ .buttonsWrapper { flex-grow: 1; display: flex; + align-items: center; gap: var(--fxl-spacing-sm); .submitButton { + min-height: 30px; + height: auto; flex-grow: 1; } } @@ -34,4 +37,4 @@ & > * { flex: 1 1 0; } -} \ No newline at end of file +} diff --git a/src/containers/MintWithTicket/Panel/PanelControls.tsx b/src/containers/MintWithTicket/Panel/PanelControls.tsx index d031b8fda..469d02c66 100644 --- a/src/containers/MintWithTicket/Panel/PanelControls.tsx +++ b/src/containers/MintWithTicket/Panel/PanelControls.tsx @@ -17,6 +17,7 @@ import { displayMutez } from "utils/units" import { TOnMintHandler } from "../MintWithTicketPage" import { isBefore } from "date-fns" import { useMintingState } from "hooks/useMintingState" +import { ButtonClaimAndMint } from "./ButtonClaimAndMint" export type PanelSubmitMode = "with-ticket" | "free" | "none" @@ -65,9 +66,15 @@ export function PanelControls(props: PanelControlsProps) { }, [onSubmit]) const handleClickUseTicket = useCallback(() => { if (userTickets) { - onSubmit(userTickets.map((ticket) => ticket.id)) + onSubmit(userTickets) } }, [onSubmit, userTickets]) + const handleClickClaimMint = useCallback( + (ticket) => { + onSubmit(ticket, true) + }, + [onSubmit] + ) return (
@@ -96,7 +103,7 @@ export function PanelControls(props: PanelControlsProps) { ) : mode === "free" ? (
- {showMintButton && ( + {showMintButton ? ( mint + ) : ( + )} {userTickets && ( (new Date()) + + useEffect(() => { + const interval = setInterval(() => { + setNow(new Date()) + }, refreshEveryMs) + return () => { + clearInterval(interval) + } + }, [refreshEveryMs]) + + return now +} diff --git a/src/pages/generative/slug/[slug]/ticket/[ticketId]/mint.tsx b/src/pages/generative/slug/[slug]/ticket/[ticketId]/mint.tsx index 53de8ef93..8d04dabad 100644 --- a/src/pages/generative/slug/[slug]/ticket/[ticketId]/mint.tsx +++ b/src/pages/generative/slug/[slug]/ticket/[ticketId]/mint.tsx @@ -2,18 +2,19 @@ import Head from "next/head" import { GetServerSideProps, NextPage } from "next" import { createApolloClient } from "services/ApolloClient" import { GenerativeToken } from "types/entities/GenerativeToken" -import { Spacing } from "components/Layout/Spacing" import { truncateEnd } from "utils/strings" -import { Qu_genToken } from "queries/generative-token" import { getImageApiUrl, OG_IMAGE_SIZE } from "components/Image" import { MintWithTicketPage } from "containers/MintWithTicket/MintWithTicketPage" +import { MintTicket } from "../../../../../../types/entities/MintTicket" +import { gql } from "@apollo/client" +import { Frag_GenTokenInfo } from "../../../../../../queries/fragments/generative-token" interface Props { token: GenerativeToken - ticketId: string + ticket: MintTicket } -const MintWithTicket: NextPage = ({ token, ticketId }) => { +const MintWithTicket: NextPage = ({ token, ticket }) => { // get the display url for og:image const displayUrl = token.captureMedia?.cid && @@ -56,36 +57,50 @@ const MintWithTicket: NextPage = ({ token, ticketId }) => { /> - + ) } export const getServerSideProps: GetServerSideProps = async (context) => { const { ticketId, slug } = context?.params || {} - let token = null const apolloClient = createApolloClient() - if (slug) { + if (slug && ticketId && !Array.isArray(ticketId)) { const { data } = await apolloClient.query({ - query: Qu_genToken, + query: gql` + ${Frag_GenTokenInfo} + query GenerativeTokenParam($slug: String, $ticketId: Float!) { + generativeToken(slug: $slug) { + ...TokenInfo + tags + moderationReason + mintOpensAt + lockEnd + metadata + metadataUri + version + } + mintTicket(id: $ticketId) { + id + price + taxationPaidUntil + } + } + `, fetchPolicy: "no-cache", - variables: { slug }, + variables: { slug, ticketId: parseInt(ticketId) }, }) - if (data) { - token = data.generativeToken + if (data.generativeToken && data.mintTicket) { + return { + props: { + token: data.generativeToken, + ticket: data.mintTicket, + }, + } } } - return { - props: { - token: token, - ticketId, - }, - notFound: !token || !ticketId, + notFound: true, } } diff --git a/src/queries/generative-token.ts b/src/queries/generative-token.ts index 26ea2e0b8..139d13f27 100644 --- a/src/queries/generative-token.ts +++ b/src/queries/generative-token.ts @@ -231,6 +231,41 @@ export const Qu_genTokenMintTickets = gql` } ` +export const Qu_genTokenClaimableMintTickets = gql` + query Query($id: Float, $slug: String, $ownerId: String) { + generativeToken(id: $id, slug: $slug) { + id + daMintTickets: mintTickets( + sort: { price: "ASC" } + skip: 0 + take: 3 + filters: { + inGracePeriod_eq: false + underAuction_eq: true + owner_ne: $ownerId + } + ) { + id + taxationPaidUntil + price + } + mintTickets( + sort: { price: "ASC" } + skip: 0 + take: 3 + filters: { + inGracePeriod_eq: false + underAuction_eq: false + owner_ne: $ownerId + } + ) { + id + price + } + } + } +` + export const Qu_genTokenAllIterations = gql` ${Frag_MediaImage} query GenerativeTokenIterations($id: Float!) { diff --git a/src/services/contract-operations/Mint.ts b/src/services/contract-operations/Mint.ts index d2cbcb11d..eb61a0e40 100644 --- a/src/services/contract-operations/Mint.ts +++ b/src/services/contract-operations/Mint.ts @@ -1,10 +1,8 @@ import { ContractAbstraction, OpKind, - TransactionWalletOperation, Wallet, WalletOperation, - WalletOperationBatch, WalletParamsWithKind, } from "@taquito/taquito" import { FxhashContracts } from "../../types/Contracts" diff --git a/src/services/contract-operations/MintV3Abstraction.ts b/src/services/contract-operations/MintV3Abstraction.ts index 543e43079..c06fa34fc 100644 --- a/src/services/contract-operations/MintV3Abstraction.ts +++ b/src/services/contract-operations/MintV3Abstraction.ts @@ -1,13 +1,24 @@ -import { GenerativeToken } from "./../../types/entities/GenerativeToken" +import { GenerativeToken } from "../../types/entities/GenerativeToken" import { ContractAbstraction, + OpKind, TransactionWalletOperation, Wallet, + WalletOperation, + WalletParamsWithKind, } from "@taquito/taquito" import { FxhashContracts } from "../../types/Contracts" import { ContractOperation } from "./ContractOperation" import { genTokCurrentPrice } from "utils/generative-token" import { isTicketOwner, isTicketUsed } from "services/Blockchain" +import { + buildParameters, + EBuildableParams, +} from "../parameters-builder/BuildParameters" +import { MintTicket } from "../../types/entities/MintTicket" +import { getMintTicketHarbergerTax } from "../../utils/math" +import { getMintTicketDAPrice } from "../../utils/mint-ticket" +import { coverage } from "browserslist"; const isValidTicket = async ( pkh: string, @@ -18,20 +29,22 @@ const isValidTicket = async ( const getFirstTicketAvailable = async ( pkh: string, - ticketIds: number[] -): Promise => { - for (const ticketId of ticketIds) { - const [isUsed, isOwner] = await isValidTicket(pkh, ticketId) - if (!isUsed && isOwner) { - return ticketId + tickets: MintTicket[], + claimTicket?: boolean +): Promise<[ticketId: MintTicket | null, claimTicket?: boolean]> => { + for (const ticket of tickets) { + const [isUsed, isOwner] = await isValidTicket(pkh, ticket.id) + if (!isUsed && (claimTicket || isOwner)) { + return [ticket, !isOwner] } } - return null + return [null] } export type TMintV3AbstractionOperationParams = { - // if a ticket ID or array of ticketID is provided, uses the first ticket available; otherwise mints on issuer - ticketId: number | number[] | null + // if a ticket or array of ticket is provided, uses the first ticket available; otherwise mints on issuer + ticket: MintTicket | MintTicket[] | null + claimTicket?: boolean token: GenerativeToken inputBytes: string } @@ -39,78 +52,140 @@ export type TMintV3AbstractionOperationParams = { /** * Provides a single entity to either: * - mint with a ticket, if provided + * - claim the ticket first if claimTicket = true * - mint directly on the issuer, with input bytes */ export class MintV3AbstractionOperation extends ContractOperation { contract: ContractAbstraction | null = null useTicket: boolean | null = null - ticketId: number | null = null + claimTicket?: boolean = false + ticket: MintTicket | null = null async prepare() { - this.useTicket = this.params.ticketId !== null + this.useTicket = this.params.ticket !== null this.contract = await this.manager.getContract(FxhashContracts.ISSUER_V3) } + async validateMultipleTickets(tickets: MintTicket[]): Promise { + const pkh = await this.manager.getBeaconWallet().getPKH() + const [availableTicket, mustClaimTicket] = await getFirstTicketAvailable( + pkh, + tickets, + this.params.claimTicket + ) + if (availableTicket) { + this.ticket = availableTicket + if (mustClaimTicket) { + this.claimTicket = true + } + return true + } + throw new Error("No tickets remaining.") + } + + async validateSingleTicket(ticket: MintTicket): Promise { + const pkh = await this.manager.getBeaconWallet().getPKH() + const [isUsed, isOwner] = await isValidTicket(pkh, ticket.id) + if (isUsed) throw new Error("Ticket is already used.") + if (!isOwner) { + if (this.params.claimTicket) { + this.claimTicket = true + } else { + throw new Error("Ticket is not owned by you.") + } + } + this.ticket = ticket + return true + } + async validate(): Promise { if (this.useTicket) { - const pkh = await this.manager.getBeaconWallet().getPKH() - if (this.params.ticketId instanceof Array) { - const availableTicketId = await getFirstTicketAvailable( - pkh, - this.params.ticketId - ) - if (availableTicketId) { - this.ticketId = availableTicketId - return true - } - throw new Error("No tickets remaining.") + if (this.params.ticket instanceof Array) { + return this.validateMultipleTickets(this.params.ticket) } else { - const [isUsed, isOwner] = await isValidTicket( - pkh, - this.params.ticketId! - ) - - if (isUsed) throw new Error("Ticket is already used.") - if (!isOwner) throw new Error("Ticket is not owned by you.") - - this.ticketId = this.params.ticketId + return this.validateSingleTicket(this.params.ticket!) } } return true } - async call(): Promise { + async call(): Promise { await this.validate() - - const ep = this.useTicket ? "mint_with_ticket" : "mint" - const params = this.useTicket - ? { - issuer_id: this.params.token.id, - ticket_id: this.ticketId, - input_bytes: this.params.inputBytes, - recipient: null, - } - : { - issuer_id: this.params.token.id, - referrer: null, - reserve_input: null, - create_ticket: null, - recipient: null, - input_bytes: this.params.inputBytes, - } - const options = this.useTicket - ? {} - : { - amount: genTokCurrentPrice(this.params.token), + if (this.useTicket) { + const mintWithTicketParams = { + issuer_id: this.params.token.id, + ticket_id: this.ticket!.id, + input_bytes: this.params.inputBytes, + recipient: null, + } + if (this.claimTicket) { + const ops: WalletParamsWithKind[] = [] + const ticketPrice = getMintTicketDAPrice( + new Date(), + new Date(this.ticket!.taxationPaidUntil), + this.ticket!.price + ) + const amount = ticketPrice + getMintTicketHarbergerTax(ticketPrice, 1) + const val = buildParameters( + { + token_id: this.ticket!.id, + transfer_to: null, + taxation: { + price: ticketPrice, + coverage: 1, + }, + }, + EBuildableParams.CLAIM_TICKET + ) + ops.push({ + kind: OpKind.TRANSACTION, + to: FxhashContracts.MINT_TICKETS_V3, + amount, mutez: true, - } - - return this.contract!.methodsObject[ep](params).send(options) + parameter: { + entrypoint: "claim", + value: val, + }, + storageLimit: 300, + }) + ops.push({ + kind: OpKind.TRANSACTION, + to: FxhashContracts.ISSUER_V3, + amount: 0, + parameter: { + entrypoint: "mint_with_ticket", + value: buildParameters( + mintWithTicketParams, + EBuildableParams.MINT_WITH_TICKET + ), + }, + storageLimit: 650, + }) + return this.manager.tezosToolkit.wallet.batch().with(ops).send() + } + return this.contract!.methodsObject["mint_with_ticket"]( + mintWithTicketParams + ).send({}) + } else { + return this.contract!.methodsObject["mint"]({ + issuer_id: this.params.token.id, + referrer: null, + reserve_input: null, + create_ticket: null, + recipient: null, + input_bytes: this.params.inputBytes, + }).send({ + amount: genTokCurrentPrice(this.params.token), + mutez: true, + }) + } } success(): string { return this.useTicket - ? `You have successfully exchanged one ticket for an iteration of "${this.params.token.name}".` + ? `You have successfully ${ + this.claimTicket ? "claimed" : "exchanged" + } one ticket for an iteration of "${this.params.token.name}".` : `You have successfully minted an iteration of "${this.params.token.name}".` } } diff --git a/src/services/contract-operations/TicketClaimV3.ts b/src/services/contract-operations/TicketClaimV3.ts index 476347300..a1a247a67 100644 --- a/src/services/contract-operations/TicketClaimV3.ts +++ b/src/services/contract-operations/TicketClaimV3.ts @@ -1,10 +1,8 @@ -import { MintTicket } from "./../../types/entities/MintTicket" import { ContractAbstraction, TransactionWalletOperation, Wallet, } from "@taquito/taquito" -import { NFTArticle } from "../../types/entities/Article" import { FxhashContracts } from "../../types/Contracts" import { ContractOperation } from "./ContractOperation" diff --git a/src/services/parameters-builder/BuildParameters.ts b/src/services/parameters-builder/BuildParameters.ts index 26ff7ddf5..40625b188 100644 --- a/src/services/parameters-builder/BuildParameters.ts +++ b/src/services/parameters-builder/BuildParameters.ts @@ -25,8 +25,10 @@ import reserveMintPassType from "./reserve-mint-pass/type.json" import reserveMintInputType from "./reserve-mint-input/type.json" import reserveMintPassInputType from "./reserve-mint-pass-input/type.json" import mintPassConsumeType from "./mint-pass-consume/type.json" +import claimTicketType from "./claim-ticket/type.json" +import mintWithTicketType from "./mint-with-ticket/type.json" import { Schema } from "@taquito/michelson-encoder" -import { packData, packDataBytes, unpackDataBytes } from "@taquito/michel-codec" +import { packDataBytes, unpackDataBytes } from "@taquito/michel-codec" /** * An Enumeration of the different parameter builders available @@ -58,6 +60,8 @@ export enum EBuildableParams { RESERVE_MINT_PASS_INPUT = "RESERVE_MINT_PASS_INPUT", RESERVE_MINT_INPUT = "RESERVE_MINT_INPUT", MINT_PASS_CONSUME = "MINT_PASS_CONSUME", + CLAIM_TICKET = "CLAIM_TICKET", + MINT_WITH_TICKET = "MINT_WITH_TICKET", } // maps a builadable param type with the actual type in json @@ -88,6 +92,8 @@ const buildableParamTypes: Record = { RESERVE_MINT_INPUT: reserveMintInputType, RESERVE_MINT_PASS_INPUT: reserveMintPassInputType, MINT_PASS_CONSUME: mintPassConsumeType, + CLAIM_TICKET: claimTicketType, + MINT_WITH_TICKET: mintWithTicketType, } /** diff --git a/src/services/parameters-builder/claim-ticket/input.ts b/src/services/parameters-builder/claim-ticket/input.ts new file mode 100644 index 000000000..76df16e2a --- /dev/null +++ b/src/services/parameters-builder/claim-ticket/input.ts @@ -0,0 +1,8 @@ +export type TClaimTicket = { + token_id: number + transfer_to: string | null + taxation: { + price: number + coverage: number + } +} diff --git a/src/services/parameters-builder/claim-ticket/type.json b/src/services/parameters-builder/claim-ticket/type.json new file mode 100644 index 000000000..7b4a4c9df --- /dev/null +++ b/src/services/parameters-builder/claim-ticket/type.json @@ -0,0 +1,45 @@ +{ + "prim": "pair", + "args": [ + { + "prim": "pair", + "args": [ + { + "prim": "nat", + "annots": ["%coverage"] + }, + { + "prim": "mutez", + "annots": [ + "%price" + ] + } + ], + "annots": [ + "%taxation" + ] + }, + { + "prim": "pair", + "args": [ + { + "prim": "nat", + "annots": [ + "%token_id" + ] + }, + { + "prim": "option", + "args": [{ + "prim": "address" + }], + "annots": [ + "%transfer_to" + ] + } + ] + } + ] +} + + diff --git a/src/services/parameters-builder/mint-with-ticket/input.ts b/src/services/parameters-builder/mint-with-ticket/input.ts new file mode 100644 index 000000000..e6795848f --- /dev/null +++ b/src/services/parameters-builder/mint-with-ticket/input.ts @@ -0,0 +1,6 @@ +export type TMintWithTicket = { + issuer_id: number + ticket_id: number + input_bytes: string + recipient: string +} diff --git a/src/services/parameters-builder/mint-with-ticket/type.json b/src/services/parameters-builder/mint-with-ticket/type.json new file mode 100644 index 000000000..691dd8aa5 --- /dev/null +++ b/src/services/parameters-builder/mint-with-ticket/type.json @@ -0,0 +1,40 @@ +{ + "prim": "pair", + "args": [ + { + "prim": "nat", + "annots": ["%issuer_id"] + }, + { + "prim": "pair", + "args": [ + { + "prim": "nat", + "annots": [ + "%ticket_id" + ] + }, + { + "prim": "pair", + "args": [ + { + "prim": "bytes", + "annots": ["%input_bytes"] + }, + { + "prim": "option", + "args": [{ + "prim": "address" + }], + "annots": [ + "%recipient" + ] + } + ] + } + ] + } + ] +} + + diff --git a/src/utils/mint-ticket.ts b/src/utils/mint-ticket.ts index 388c74632..4e5b3cb59 100644 --- a/src/utils/mint-ticket.ts +++ b/src/utils/mint-ticket.ts @@ -5,6 +5,7 @@ import { User } from "../types/entities/User" import { getTaxPaidUntil } from "./math" import { GenerativeToken } from "../types/entities/GenerativeToken" import { TzktOperation } from "../types/Tzkt" +import { addDays, differenceInSeconds, subSeconds } from "date-fns" export const generateMintTicketFromMintAction = ( opData: TzktOperation[], @@ -41,3 +42,23 @@ export const generateMintTicketFromMintAction = ( settings: token.mintTicketSettings!, } } + +export const getMintTicketDAPrice = ( + dateNow: Date, + dateEnd: Date, + price: number +) => { + const dateNowOffset = subSeconds(dateNow, 60) + const dateEndDayLater = addDays(dateEnd, 1) + // end of auction + if (dateEndDayLater < dateNowOffset) { + return 100000 + } + // start of auction + if (dateNowOffset < dateEnd) { + return price + } + const elapsedTimeInSeconds = differenceInSeconds(dateNowOffset, dateEnd) + const daProgressMultiplier = (elapsedTimeInSeconds * 100) / 86400 / 100 + return 100000 + (price - 100000) * (1 - daProgressMultiplier) +} From 9a454a0cdb657c1436962805b9af41b880b2be6a Mon Sep 17 00:00:00 2001 From: Geoffrey MERRAN Date: Tue, 25 Apr 2023 15:03:24 +0100 Subject: [PATCH 2/4] feat: add button explore params / use ticket if ticket available on token page --- .../Display/ButtonExploreParams.tsx | 53 +++++++++++++++++++ .../Generative/Display/GenerativeDisplay.tsx | 26 +++++---- src/queries/mint-ticket.ts | 14 +++++ 3 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 src/containers/Generative/Display/ButtonExploreParams.tsx create mode 100644 src/queries/mint-ticket.ts diff --git a/src/containers/Generative/Display/ButtonExploreParams.tsx b/src/containers/Generative/Display/ButtonExploreParams.tsx new file mode 100644 index 000000000..6e0877c0f --- /dev/null +++ b/src/containers/Generative/Display/ButtonExploreParams.tsx @@ -0,0 +1,53 @@ +import React, { memo, useContext } from "react" +import { GenerativeToken } from "../../../types/entities/GenerativeToken" +import Link from "next/link" +import { getGenerativeTokenUrl } from "../../../utils/generative-token" +import { Button } from "../../../components/Button" +import { UserContext } from "../../UserProvider" +import { useQuery } from "@apollo/client" +import { Qu_MintTickets } from "../../../queries/mint-ticket" +import style from "./GenerativeDisplay.module.scss"; + +interface ButtonExploreParamsProps { + token: GenerativeToken +} + +const _ButtonExploreParams = ({ token }: ButtonExploreParamsProps) => { + const { user } = useContext(UserContext) + const { data } = useQuery(Qu_MintTickets, { + variables: { + take: 1, + filters: { + token_eq: token.id, + owner_eq: user?.id, + }, + }, + skip: !user, + }) + const hasAtLeastOneTicket = user && data?.mintTickets.length > 0 + return ( + + + + ) +} + +export const ButtonExploreParams = memo(_ButtonExploreParams) diff --git a/src/containers/Generative/Display/GenerativeDisplay.tsx b/src/containers/Generative/Display/GenerativeDisplay.tsx index 5556dee18..8cd615217 100644 --- a/src/containers/Generative/Display/GenerativeDisplay.tsx +++ b/src/containers/Generative/Display/GenerativeDisplay.tsx @@ -28,6 +28,7 @@ import { ListReserves } from "../../../components/List/ListReserves" import { GenTokArticleMentions } from "./GenTokArticleMentions" import { Clamp } from "../../../components/Clamp/Clamp" import { useCallback, useState } from "react" +import { ButtonExploreParams } from "./ButtonExploreParams" /** * This is the Core component resposible for the display logic of a Generative @@ -89,16 +90,21 @@ export function GenerativeDisplay({ token, offlineMode = false }: Props) { - - - + <> + {token.inputBytesSize > 0 && ( + + )} + + + + diff --git a/src/queries/mint-ticket.ts b/src/queries/mint-ticket.ts new file mode 100644 index 000000000..d00e80fa9 --- /dev/null +++ b/src/queries/mint-ticket.ts @@ -0,0 +1,14 @@ +import { gql } from "@apollo/client" + +export const Qu_MintTickets = gql` + query MintTickets( + $take: Int + $skip: Int + $sort: MintTicketSortInput + $filters: MintTicketFilter + ) { + mintTickets(take: $take, skip: $skip, sort: $sort, filters: $filters) { + id + } + } +` From 92840b6dd880baee1931bf8fa1371533c7c549e0 Mon Sep 17 00:00:00 2001 From: Geoffrey MERRAN Date: Tue, 25 Apr 2023 15:08:55 +0100 Subject: [PATCH 3/4] run lint fix --- src/components/Tables/TableMintTickets/MintTicketRow.tsx | 8 ++------ src/components/Tables/TableMintTickets/index.tsx | 2 +- src/containers/Generative/Display/ButtonExploreParams.tsx | 2 +- src/services/contract-operations/MintV3Abstraction.ts | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/Tables/TableMintTickets/MintTicketRow.tsx b/src/components/Tables/TableMintTickets/MintTicketRow.tsx index 01f0cc515..810c60fe8 100644 --- a/src/components/Tables/TableMintTickets/MintTicketRow.tsx +++ b/src/components/Tables/TableMintTickets/MintTicketRow.tsx @@ -1,11 +1,7 @@ import { memo, useContext, useEffect, useRef } from "react" import Link from "next/link" import cs from "classnames" -import { - format, - formatDistanceToNow, - isBefore, -} from "date-fns" +import { format, formatDistanceToNow, isBefore } from "date-fns" import { Image } from "components/Image" import { UserContext } from "containers/UserProvider" import { MintTicket } from "types/entities/MintTicket" @@ -17,7 +13,7 @@ import { ButtonUpdatePriceMintTicket } from "components/MintTicket/ButtonUpdateP import { ButtonClaimMintTicket } from "components/MintTicket/ButtonClaimMintTicket" import { ApolloCache, useApolloClient } from "@apollo/client" import { GenerativeToken } from "types/entities/GenerativeToken" -import { getMintTicketDAPrice } from "../../../utils/mint-ticket"; +import { getMintTicketDAPrice } from "../../../utils/mint-ticket" const moveMintTicketToUnderAuction = ( cache: ApolloCache, diff --git a/src/components/Tables/TableMintTickets/index.tsx b/src/components/Tables/TableMintTickets/index.tsx index 47d5778fb..fab15a4fb 100644 --- a/src/components/Tables/TableMintTickets/index.tsx +++ b/src/components/Tables/TableMintTickets/index.tsx @@ -5,7 +5,7 @@ import { MintTicket } from "types/entities/MintTicket" import Skeleton from "../../Skeleton" import style from "../TableUser.module.scss" import { MintTicketRow } from "./MintTicketRow" -import useNow from "../../../hooks/useNow"; +import useNow from "../../../hooks/useNow" interface TableMintTicketsProps { firstColName?: string diff --git a/src/containers/Generative/Display/ButtonExploreParams.tsx b/src/containers/Generative/Display/ButtonExploreParams.tsx index 6e0877c0f..962037135 100644 --- a/src/containers/Generative/Display/ButtonExploreParams.tsx +++ b/src/containers/Generative/Display/ButtonExploreParams.tsx @@ -6,7 +6,7 @@ import { Button } from "../../../components/Button" import { UserContext } from "../../UserProvider" import { useQuery } from "@apollo/client" import { Qu_MintTickets } from "../../../queries/mint-ticket" -import style from "./GenerativeDisplay.module.scss"; +import style from "./GenerativeDisplay.module.scss" interface ButtonExploreParamsProps { token: GenerativeToken diff --git a/src/services/contract-operations/MintV3Abstraction.ts b/src/services/contract-operations/MintV3Abstraction.ts index c06fa34fc..efb390864 100644 --- a/src/services/contract-operations/MintV3Abstraction.ts +++ b/src/services/contract-operations/MintV3Abstraction.ts @@ -18,7 +18,7 @@ import { import { MintTicket } from "../../types/entities/MintTicket" import { getMintTicketHarbergerTax } from "../../utils/math" import { getMintTicketDAPrice } from "../../utils/mint-ticket" -import { coverage } from "browserslist"; +import { coverage } from "browserslist" const isValidTicket = async ( pkh: string, From f2bd27c9bb414fe844e342f5e0dbb2602e087771 Mon Sep 17 00:00:00 2001 From: Geoffrey MERRAN Date: Tue, 25 Apr 2023 15:55:06 +0100 Subject: [PATCH 4/4] fix: claim n mint contract work with values rounded, only show use ticket for avail tickets not under auctions --- .../Display/ButtonExploreParams.tsx | 6 ++- .../Panel/ButtonClaimAndMint.tsx | 38 +++++++++---------- .../MintWithTicket/Panel/PanelControls.tsx | 28 +++++++------- src/queries/generative-token.ts | 1 + src/queries/mint-ticket.ts | 4 +- .../contract-operations/MintV3Abstraction.ts | 15 +++++--- src/types/entities/MintTicket.ts | 1 + 7 files changed, 52 insertions(+), 41 deletions(-) diff --git a/src/containers/Generative/Display/ButtonExploreParams.tsx b/src/containers/Generative/Display/ButtonExploreParams.tsx index 962037135..fbcbba0c8 100644 --- a/src/containers/Generative/Display/ButtonExploreParams.tsx +++ b/src/containers/Generative/Display/ButtonExploreParams.tsx @@ -5,7 +5,7 @@ import { getGenerativeTokenUrl } from "../../../utils/generative-token" import { Button } from "../../../components/Button" import { UserContext } from "../../UserProvider" import { useQuery } from "@apollo/client" -import { Qu_MintTickets } from "../../../queries/mint-ticket" +import { Qu_mintTickets } from "../../../queries/mint-ticket" import style from "./GenerativeDisplay.module.scss" interface ButtonExploreParamsProps { @@ -14,12 +14,14 @@ interface ButtonExploreParamsProps { const _ButtonExploreParams = ({ token }: ButtonExploreParamsProps) => { const { user } = useContext(UserContext) - const { data } = useQuery(Qu_MintTickets, { + const { data } = useQuery(Qu_mintTickets, { variables: { take: 1, + skip: 0, filters: { token_eq: token.id, owner_eq: user?.id, + underAuction_eq: false, }, }, skip: !user, diff --git a/src/containers/MintWithTicket/Panel/ButtonClaimAndMint.tsx b/src/containers/MintWithTicket/Panel/ButtonClaimAndMint.tsx index cfbb49bb6..bad79304c 100644 --- a/src/containers/MintWithTicket/Panel/ButtonClaimAndMint.tsx +++ b/src/containers/MintWithTicket/Panel/ButtonClaimAndMint.tsx @@ -19,7 +19,7 @@ interface ClaimAndMintProps { onClick: (ticket: MintTicket) => void } -const _ClaimAndMint = ({ token, onClick }: ClaimAndMintProps) => { +const _ButtonClaimAndMint = ({ token, onClick }: ClaimAndMintProps) => { const { user } = useContext(UserContext) const now = useNow(15000) const { data } = useQuery<{ @@ -33,20 +33,20 @@ const _ClaimAndMint = ({ token, onClick }: ClaimAndMintProps) => { const cheapestTicket = useMemo(() => { if (!data?.generativeToken) return null const [cheapestDaTicket] = data.generativeToken.daMintTickets - .map((ticket, idx) => { + .map((ticket) => { return { ...ticket, - price: getMintTicketDAPrice( + daPrice: getMintTicketDAPrice( now, new Date(ticket.taxationPaidUntil), ticket.price ), } }) - .sort((a, b) => (a.price > b.price ? 1 : -1)) + .sort((a, b) => (a.daPrice > b.daPrice ? 1 : -1)) const [cheapestClaimableTicket] = data.generativeToken.mintTickets if (cheapestDaTicket && cheapestClaimableTicket) { - return cheapestDaTicket.price < cheapestClaimableTicket.price + return cheapestDaTicket.daPrice < cheapestClaimableTicket.price ? cheapestDaTicket : cheapestClaimableTicket } @@ -57,19 +57,19 @@ const _ClaimAndMint = ({ token, onClick }: ClaimAndMintProps) => { onClick(cheapestTicket) } }, [cheapestTicket, onClick]) - return ( - cheapestTicket && ( - - claim & mint{" "} - - - ) - ) + const cheapestTicketPrice = + cheapestTicket && (cheapestTicket.daPrice || cheapestTicket.price) + return cheapestTicketPrice ? ( + + claim & mint{" "} + + + ) : null } -export const ButtonClaimAndMint = memo(_ClaimAndMint) +export const ButtonClaimAndMint = memo(_ButtonClaimAndMint) diff --git a/src/containers/MintWithTicket/Panel/PanelControls.tsx b/src/containers/MintWithTicket/Panel/PanelControls.tsx index 469d02c66..8689cf1e5 100644 --- a/src/containers/MintWithTicket/Panel/PanelControls.tsx +++ b/src/containers/MintWithTicket/Panel/PanelControls.tsx @@ -18,6 +18,7 @@ import { TOnMintHandler } from "../MintWithTicketPage" import { isBefore } from "date-fns" import { useMintingState } from "hooks/useMintingState" import { ButtonClaimAndMint } from "./ButtonClaimAndMint" +import { Qu_mintTickets } from "../../../queries/mint-ticket" export type PanelSubmitMode = "with-ticket" | "free" | "none" @@ -38,28 +39,27 @@ export function PanelControls(props: PanelControlsProps) { const showMintButton = !hidden && !locked && enabled // get user mint tickets when user is available, and mode is "free" - const { data } = useQuery(Qu_userMintTickets, { + const { data } = useQuery(Qu_mintTickets, { fetchPolicy: "network-only", variables: { - id: user?.id, + take: 20, + skip: 0, + sort: { + taxationPaidUntil: "ASC", + }, + filters: { + token_eq: token.id, + owner_eq: user?.id, + underAuction_eq: false, + }, }, skip: !(mode === "free" && user), }) // extract user tickets for this project const userTickets = useMemo(() => { - if (!data) return null - if (!data.user?.mintTickets) return null - const projectTickets = (data.user.mintTickets as MintTicket[]) - .filter((ticket) => ticket.token.id === token.id) - .filter((ticket) => - isBefore(new Date(), new Date(ticket.taxationPaidUntil)) - ) - .sort((a, b) => - (a.taxationPaidUntil as any) < (b.taxationPaidUntil as any) ? 1 : -1 - ) - return projectTickets.length > 0 ? projectTickets : null - }, [data, token.id]) + return data?.mintTickets.length > 0 ? data.mintTickets : null + }, [data]) const handleClickMint = useCallback(() => { onSubmit(null) diff --git a/src/queries/generative-token.ts b/src/queries/generative-token.ts index 139d13f27..25d57ed79 100644 --- a/src/queries/generative-token.ts +++ b/src/queries/generative-token.ts @@ -260,6 +260,7 @@ export const Qu_genTokenClaimableMintTickets = gql` } ) { id + taxationPaidUntil price } } diff --git a/src/queries/mint-ticket.ts b/src/queries/mint-ticket.ts index d00e80fa9..1bbe21549 100644 --- a/src/queries/mint-ticket.ts +++ b/src/queries/mint-ticket.ts @@ -1,6 +1,6 @@ import { gql } from "@apollo/client" -export const Qu_MintTickets = gql` +export const Qu_mintTickets = gql` query MintTickets( $take: Int $skip: Int @@ -9,6 +9,8 @@ export const Qu_MintTickets = gql` ) { mintTickets(take: $take, skip: $skip, sort: $sort, filters: $filters) { id + price + taxationPaidUntil } } ` diff --git a/src/services/contract-operations/MintV3Abstraction.ts b/src/services/contract-operations/MintV3Abstraction.ts index efb390864..4d6383c76 100644 --- a/src/services/contract-operations/MintV3Abstraction.ts +++ b/src/services/contract-operations/MintV3Abstraction.ts @@ -120,12 +120,17 @@ export class MintV3AbstractionOperation extends ContractOperation