diff --git a/packages/common/src/api/tan-query/queryKeys.ts b/packages/common/src/api/tan-query/queryKeys.ts index 6957c68c7a8..584a04b57f8 100644 --- a/packages/common/src/api/tan-query/queryKeys.ts +++ b/packages/common/src/api/tan-query/queryKeys.ts @@ -124,5 +124,6 @@ export const QUERY_KEYS = { firstBuyQuote: 'firstBuyQuote', walletSolBalance: 'walletSolBalance', launchpadConfig: 'launchpadConfig', - externalWalletBalance: 'externalWalletBalance' + externalWalletBalance: 'externalWalletBalance', + claimFee: 'claimFee' } as const diff --git a/packages/common/src/messages/coinDetailsMessages.ts b/packages/common/src/messages/coinDetailsMessages.ts index 51691ffb18b..25306f33567 100644 --- a/packages/common/src/messages/coinDetailsMessages.ts +++ b/packages/common/src/messages/coinDetailsMessages.ts @@ -66,6 +66,12 @@ export const coinDetailsMessages = { }, overflowMenu: { copyCoinAddress: 'Copy Coin Address', + unclaimedFees: 'Unclaimed Fees', + artistEarnings: 'Artist Earnings', + vestingSchedule: 'Vesting Schedule', + vestingScheduleValue: '5 years (post-graduation)', + $audio: '$AUDIO', + claim: 'Claim', openDexscreener: 'Open Dexscreener', details: 'Details', copiedToClipboard: 'Copied Coin Address To Clipboard!', @@ -115,5 +121,9 @@ export const coinDetailsMessages = { descriptionPlaceholder: 'Tell fans what makes your artist coin special — think early listens, exclusive drops, or fun perks for your biggest supporters.', pasteLink: 'Paste a link' + }, + toasts: { + feesClaimed: 'Fees claimed successfully!', + feesClaimFailed: 'Unable to claim fees. Please try again.' } } diff --git a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/index.ts b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/index.ts index 31f16b1dba2..31f983a55d8 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/index.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/index.ts @@ -18,6 +18,7 @@ import { cache } from './routes/cache' import { feePayer } from './routes/feePayer' import { health } from './routes/health/health' import { location } from './routes/instruction/location' +import { claimFees } from './routes/launchpad/claim_fees' import { firstBuyQuote, getLaunchpadConfigRoute @@ -45,6 +46,7 @@ const main = async () => { }) // launchpad endpoints don't need user/discovery validation, so register them before middleware app.post('/solana/launchpad/launch_coin', upload.single('image'), launchCoin) + app.get('/solana/launchpad/claim_fees', claimFees) app.get('/solana/launchpad/first_buy_quote', firstBuyQuote) app.get('/solana/launchpad/config', getLaunchpadConfigRoute) diff --git a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/launchpad/claim_fees.ts b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/launchpad/claim_fees.ts new file mode 100644 index 00000000000..fe9c6fdf9cb --- /dev/null +++ b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/launchpad/claim_fees.ts @@ -0,0 +1,66 @@ +import { DynamicBondingCurveClient } from '@meteora-ag/dynamic-bonding-curve-sdk' +import { PublicKey } from '@solana/web3.js' +import { Request, Response } from 'express' + +import { logger } from '../../logger' +import { getConnection } from '../../utils/connections' + +interface ClaimFeesRequestBody { + tokenMint: string + ownerWalletAddress: string + receiverWalletAddress: string +} + +export const claimFees = async ( + req: Request, + res: Response +) => { + try { + const { tokenMint, ownerWalletAddress, receiverWalletAddress } = req.query + + // Validate required parameters + if (!tokenMint || !ownerWalletAddress || !receiverWalletAddress) { + throw new Error( + 'Invalid request parameters. tokenMint, ownerWalletAddress, and receiverWalletAddress are required.' + ) + } + + const connection = getConnection() + const dbcClient = new DynamicBondingCurveClient(connection, 'confirmed') + + const tokenPool = await dbcClient.state.getPoolByBaseMint( + new PublicKey(tokenMint) + ) + if (!tokenPool) { + throw new Error(`No DBC pool found for base mint: ${tokenMint}.`) + } + + const poolAddress = tokenPool.publicKey + const poolData = tokenPool.account + const ownerWallet = new PublicKey(ownerWalletAddress) + const receiverWallet = new PublicKey(receiverWalletAddress) + + const claimFeesTx = await dbcClient.creator.claimCreatorTradingFee({ + pool: poolAddress, + payer: ownerWallet, + creator: ownerWallet, + maxBaseAmount: poolData.creatorBaseFee, // Match max amount to the claimable amount (effectively no limit) + maxQuoteAmount: poolData.creatorQuoteFee, // Match max amount to the claimable amount (effectively no limit) + receiver: receiverWallet + }) + + claimFeesTx.recentBlockhash = ( + await connection.getLatestBlockhash() + ).blockhash + claimFeesTx.feePayer = ownerWallet + + return res.status(200).send({ + claimFeesTx: claimFeesTx.serialize({ requireAllSignatures: false }) + }) + } catch (e) { + logger.error( + 'Error in claim_fees - unable to create creator claim fee transaction' + ) + res.status(500).send() + } +} diff --git a/packages/sdk/src/sdk/api/generated/default/models/CoinDynamicBondingCurve.ts b/packages/sdk/src/sdk/api/generated/default/models/CoinDynamicBondingCurve.ts index 27afc8bf235..5bf9e649260 100644 --- a/packages/sdk/src/sdk/api/generated/default/models/CoinDynamicBondingCurve.ts +++ b/packages/sdk/src/sdk/api/generated/default/models/CoinDynamicBondingCurve.ts @@ -44,6 +44,24 @@ export interface CoinDynamicBondingCurve { * @memberof CoinDynamicBondingCurve */ curveProgress: number; + /** + * Whether the bonding curve has been migrated + * @type {boolean} + * @memberof CoinDynamicBondingCurve + */ + isMigrated?: boolean; + /** + * Creator quote fee for the bonding curve + * @type {number} + * @memberof CoinDynamicBondingCurve + */ + creatorQuoteFee: number; + /** + * Total trading quote fee accumulated + * @type {number} + * @memberof CoinDynamicBondingCurve + */ + totalTradingQuoteFee: number; } /** @@ -55,6 +73,8 @@ export function instanceOfCoinDynamicBondingCurve(value: object): value is CoinD isInstance = isInstance && "price" in value && value["price"] !== undefined; isInstance = isInstance && "priceUSD" in value && value["priceUSD"] !== undefined; isInstance = isInstance && "curveProgress" in value && value["curveProgress"] !== undefined; + isInstance = isInstance && "creatorQuoteFee" in value && value["creatorQuoteFee"] !== undefined; + isInstance = isInstance && "totalTradingQuoteFee" in value && value["totalTradingQuoteFee"] !== undefined; return isInstance; } @@ -73,6 +93,9 @@ export function CoinDynamicBondingCurveFromJSONTyped(json: any, ignoreDiscrimina 'price': json['price'], 'priceUSD': json['priceUSD'], 'curveProgress': json['curveProgress'], + 'isMigrated': !exists(json, 'isMigrated') ? undefined : json['isMigrated'], + 'creatorQuoteFee': json['creatorQuoteFee'], + 'totalTradingQuoteFee': json['totalTradingQuoteFee'], }; } @@ -89,6 +112,9 @@ export function CoinDynamicBondingCurveToJSON(value?: CoinDynamicBondingCurve | 'price': value.price, 'priceUSD': value.priceUSD, 'curveProgress': value.curveProgress, + 'isMigrated': value.isMigrated, + 'creatorQuoteFee': value.creatorQuoteFee, + 'totalTradingQuoteFee': value.totalTradingQuoteFee, }; } diff --git a/packages/sdk/src/sdk/services/Solana/SolanaRelay.ts b/packages/sdk/src/sdk/services/Solana/SolanaRelay.ts index 8b1f50a9974..d5d5881fb35 100644 --- a/packages/sdk/src/sdk/services/Solana/SolanaRelay.ts +++ b/packages/sdk/src/sdk/services/Solana/SolanaRelay.ts @@ -17,7 +17,9 @@ import { LaunchCoinSchema, FirstBuyQuoteResponse, FirstBuyQuoteRequest, - LaunchpadConfigResponse + LaunchpadConfigResponse, + ClaimFeesRequest, + ClaimFeesResponse } from './types' /** @@ -302,4 +304,35 @@ export class SolanaRelay extends BaseAPI { return json as LaunchpadConfigResponse }).value() } + + /** + * Claims creator trading fees from a dynamic bonding curve pool. + */ + public async claimFees( + params: ClaimFeesRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction + ): Promise { + const headerParameters: runtime.HTTPHeaders = { + 'Content-Type': 'application/json' + } + const queryParameters: runtime.HTTPQuery = { + tokenMint: params.tokenMint, + ownerWalletAddress: params.ownerWalletAddress, + receiverWalletAddress: params.receiverWalletAddress + } + + const response = await this.request( + { + path: '/launchpad/claim_fees', + method: 'GET', + headers: headerParameters, + query: queryParameters + }, + initOverrides + ) + + return await new runtime.JSONApiResponse(response, (json) => { + return json as ClaimFeesResponse + }).value() + } } diff --git a/packages/sdk/src/sdk/services/Solana/types.ts b/packages/sdk/src/sdk/services/Solana/types.ts index 4cd960e9398..26fda06e592 100644 --- a/packages/sdk/src/sdk/services/Solana/types.ts +++ b/packages/sdk/src/sdk/services/Solana/types.ts @@ -157,3 +157,13 @@ export type LaunchpadConfigResponse = { maxTokenOutputAmount: string startingPrice: string } + +export type ClaimFeesRequest = { + tokenMint: string + ownerWalletAddress: string + receiverWalletAddress: string +} + +export type ClaimFeesResponse = { + claimFeesTx: string +} diff --git a/packages/web/src/hooks/useClaimFees.ts b/packages/web/src/hooks/useClaimFees.ts new file mode 100644 index 00000000000..a116648f6cb --- /dev/null +++ b/packages/web/src/hooks/useClaimFees.ts @@ -0,0 +1,111 @@ +import { type Coin } from '@audius/common/adapters' +import { getArtistCoinQueryKey, useQueryContext } from '@audius/common/api' +import { Feature } from '@audius/common/models' +import type { Provider as SolanaProvider } from '@reown/appkit-adapter-solana/react' +import { VersionedTransaction } from '@solana/web3.js' +import { + useMutation, + UseMutationOptions, + useQueryClient +} from '@tanstack/react-query' + +import { appkitModal } from 'app/ReownAppKitModal' +import { reportToSentry } from 'store/errors/reportToSentry' + +export type UseClaimFeesParams = { + tokenMint: string + ownerWalletAddress: string + receiverWalletAddress: string +} + +export type ClaimFeesResponse = { + claimFeesTx: string + signature: string +} + +/** + * Hook for claiming creator trading fees from a dynamic bonding curve pool. + * This gets the TX from solana relay, then signs and sends the claim fees transaction. + * NOTE: This is a web feature only because the user must sign with the same external wallet they used to launch the coin (wallet connect wallet). + */ +export const useClaimFees = ( + options?: UseMutationOptions +) => { + const { audiusSdk } = useQueryContext() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ + tokenMint, + ownerWalletAddress, + receiverWalletAddress + }: UseClaimFeesParams): Promise => { + const sdk = await audiusSdk() + const solanaProvider = appkitModal.getProvider('solana') + if (!solanaProvider) { + throw new Error('Missing SolanaProvider') + } + if (!ownerWalletAddress) { + throw new Error('Missing owner wallet address') + } + + // Get the claim fee transaction from the relay + const claimFeesResponse = await sdk.services.solanaRelay.claimFees({ + tokenMint, + ownerWalletAddress, + receiverWalletAddress + }) + + const { claimFeesTx: claimFeesTxSerialized } = claimFeesResponse + + // Transaction is sent from the backend as a serialized base64 string + const deserializedTx = VersionedTransaction.deserialize( + Buffer.from(claimFeesTxSerialized, 'base64') + ) + + // Triggers 3rd party wallet to sign and send the transaction + const signature = + await solanaProvider.signAndSendTransaction(deserializedTx) + + // Confirm the transaction + await sdk.services.solanaClient.connection.confirmTransaction( + signature, + 'confirmed' + ) + + return { + claimFeesTx: claimFeesTxSerialized, + signature + } + }, + ...options, + onError: (error, params) => { + // Call the original onError if provided + reportToSentry({ + error, + feature: Feature.ArtistCoins, + name: 'Artist coin fees claim error', + additionalInfo: { + ...params + } + }) + }, + onSuccess: (data, variables, context) => { + // Optimistically update the unclaimed fees data + const queryKey = getArtistCoinQueryKey(variables.tokenMint) + queryClient.setQueryData(queryKey, (existingCoin) => { + if (!existingCoin) return existingCoin + return { + ...existingCoin, + dynamicBondingCurve: { + ...existingCoin?.dynamicBondingCurve, + creatorQuoteFee: 0 + } + } + }) + + // Call the original onSuccess if provided + options?.onSuccess?.(data, variables, context) + } + }) +} diff --git a/packages/web/src/pages/artist-coins-launchpad-page/LaunchpadPage.tsx b/packages/web/src/pages/artist-coins-launchpad-page/LaunchpadPage.tsx index f57a8ef532f..d1b2fa045f1 100644 --- a/packages/web/src/pages/artist-coins-launchpad-page/LaunchpadPage.tsx +++ b/packages/web/src/pages/artist-coins-launchpad-page/LaunchpadPage.tsx @@ -40,7 +40,7 @@ import { } from './components/LaunchpadModals' import { LAUNCHPAD_COIN_DESCRIPTION, MIN_SOL_BALANCE, Phase } from './constants' import { BuyCoinPage, ReviewPage, SetupPage, SplashPage } from './pages' -import { getLatestConnectedWallet, useLaunchpadAnalytics } from './utils' +import { getLastConnectedSolWallet, useLaunchpadAnalytics } from './utils' import { useLaunchpadFormSchema } from './validation' const messages = { @@ -70,7 +70,7 @@ const LaunchpadPageContent = ({ const { data: connectedWallets } = useConnectedWallets() const { toast } = useContext(ToastContext) const connectedWallet = useMemo( - () => getLatestConnectedWallet(connectedWallets), + () => getLastConnectedSolWallet(connectedWallets), [connectedWallets] ) const { @@ -173,7 +173,7 @@ const LaunchpadPageContent = ({ async (error: unknown) => { // If wallet is already linked, continue with the flow if (error instanceof AlreadyAssociatedError) { - const lastConnectedWallet = getLatestConnectedWallet(connectedWallets) + const lastConnectedWallet = getLastConnectedSolWallet(connectedWallets) if (lastConnectedWallet) { const { isValid: isValidWalletBalance, walletBalanceLamports } = await getIsValidWalletBalance(lastConnectedWallet?.address) @@ -320,7 +320,7 @@ export const LaunchpadPage = () => { const navigate = useNavigate() const connectedWallet = useMemo( - () => getLatestConnectedWallet(connectedWallets), + () => getLastConnectedSolWallet(connectedWallets), [connectedWallets] ) const { @@ -474,7 +474,7 @@ export const LaunchpadPage = () => { // Get the most recent connected Solana wallet (last in the array) const connectedWallet: ConnectedWallet | undefined = - getLatestConnectedWallet(connectedWallets) + getLastConnectedSolWallet(connectedWallets) if (!currentUser || !connectedWallet) { toast(messages.errors.unknownError) diff --git a/packages/web/src/pages/artist-coins-launchpad-page/components/LaunchpadBuyModal.tsx b/packages/web/src/pages/artist-coins-launchpad-page/components/LaunchpadBuyModal.tsx index 08a45dc7015..4b251656bef 100644 --- a/packages/web/src/pages/artist-coins-launchpad-page/components/LaunchpadBuyModal.tsx +++ b/packages/web/src/pages/artist-coins-launchpad-page/components/LaunchpadBuyModal.tsx @@ -37,7 +37,7 @@ import { useExternalWalletSwap } from 'hooks/useExternalWalletSwap' import { make, track } from 'services/analytics' import zIndex from 'utils/zIndex' -import { getLatestConnectedWallet } from '../utils' +import { getLastConnectedSolWallet } from '../utils' const INPUT_TOKEN_MAP: Record = { @@ -402,7 +402,7 @@ export const LaunchpadBuyModal = ({ } const { data: connectedWallets } = useConnectedWallets() const externalWalletAddress = useMemo( - () => getLatestConnectedWallet(connectedWallets)?.address, + () => getLastConnectedSolWallet(connectedWallets)?.address, [connectedWallets] ) const { diff --git a/packages/web/src/pages/artist-coins-launchpad-page/pages/BuyCoinPage.tsx b/packages/web/src/pages/artist-coins-launchpad-page/pages/BuyCoinPage.tsx index 3648260abdd..89180d188c5 100644 --- a/packages/web/src/pages/artist-coins-launchpad-page/pages/BuyCoinPage.tsx +++ b/packages/web/src/pages/artist-coins-launchpad-page/pages/BuyCoinPage.tsx @@ -34,7 +34,7 @@ import { ArtistCoinsSubmitRow } from '../components/ArtistCoinsSubmitRow' import { LaunchpadBuyModal } from '../components/LaunchpadBuyModal' import type { PhasePageProps } from '../components/types' import { AMOUNT_OF_STEPS } from '../constants' -import { getLatestConnectedWallet, useLaunchpadAnalytics } from '../utils' +import { getLastConnectedSolWallet, useLaunchpadAnalytics } from '../utils' import { FIELDS } from '../validation' const messages = { @@ -92,7 +92,7 @@ export const BuyCoinPage = ({ const [isReceiveAmountChanging, setIsReceiveAmountChanging] = useState(false) const { data: connectedWallets } = useConnectedWallets() const connectedWallet = useMemo( - () => getLatestConnectedWallet(connectedWallets), + () => getLastConnectedSolWallet(connectedWallets), [connectedWallets] ) diff --git a/packages/web/src/pages/artist-coins-launchpad-page/utils.ts b/packages/web/src/pages/artist-coins-launchpad-page/utils.ts index adae9bf0a3a..1b37f87c8a2 100644 --- a/packages/web/src/pages/artist-coins-launchpad-page/utils.ts +++ b/packages/web/src/pages/artist-coins-launchpad-page/utils.ts @@ -10,9 +10,9 @@ import { omit } from 'lodash' import { make } from 'services/analytics' /** - * Gets the most recently added connected wallet + * Gets the last connected Solana wallet in the connected wallets array */ -export const getLatestConnectedWallet = ( +export const getLastConnectedSolWallet = ( connectedWallets: ConnectedWallet[] | undefined ) => { return connectedWallets?.filter( diff --git a/packages/web/src/pages/artist-coins-launchpad-page/validation.ts b/packages/web/src/pages/artist-coins-launchpad-page/validation.ts index 5d62d5e8fc6..1e83209ca5a 100644 --- a/packages/web/src/pages/artist-coins-launchpad-page/validation.ts +++ b/packages/web/src/pages/artist-coins-launchpad-page/validation.ts @@ -18,7 +18,7 @@ import { toFormikValidationSchema } from 'zod-formik-adapter' import { useLaunchpadConfig } from 'hooks/useLaunchpadConfig' import { reportToSentry } from 'store/errors/reportToSentry' -import { getLatestConnectedWallet } from './utils' +import { getLastConnectedSolWallet } from './utils' export const FIELDS = { coinName: 'coinName', @@ -215,7 +215,7 @@ export const useLaunchpadFormSchema = () => { }, [firstBuyQuoteData]) const connectedWallet = useMemo( - () => getLatestConnectedWallet(connectedWallets), + () => getLastConnectedSolWallet(connectedWallets), [connectedWallets] ) const { data: audioBalance } = useWalletAudioBalance({ diff --git a/packages/web/src/pages/asset-detail-page/components/AssetInfoSection.tsx b/packages/web/src/pages/asset-detail-page/components/AssetInfoSection.tsx index fdce4c6ab48..42288d60c41 100644 --- a/packages/web/src/pages/asset-detail-page/components/AssetInfoSection.tsx +++ b/packages/web/src/pages/asset-detail-page/components/AssetInfoSection.tsx @@ -3,14 +3,16 @@ import { useCallback, useContext, useMemo } from 'react' import type { Coin } from '@audius/common/adapters' import { useArtistCoin, - useCurrentUserId, useUser, - useUserCoins + useUserCoins, + useConnectedWallets, + useCurrentAccountUser } from '@audius/common/api' import { useDiscordOAuthLink } from '@audius/common/hooks' import { coinDetailsMessages } from '@audius/common/messages' -import { WidthSizes } from '@audius/common/models' +import { Feature, WidthSizes } from '@audius/common/models' import { removeNullable, route, shortenSPLAddress } from '@audius/common/utils' +import { wAUDIO } from '@audius/fixed-decimal' import { Flex, IconCopy, @@ -21,9 +23,12 @@ import { IconLink, IconTikTok, IconX, + IconInfo, + LoadingSpinner, Paper, PlainButton, Text, + TextLink, useTheme } from '@audius/harmony' import { HashId } from '@audius/sdk' @@ -34,13 +39,17 @@ import Skeleton from 'components/skeleton/Skeleton' import { ToastContext } from 'components/toast/ToastContext' import Tooltip from 'components/tooltip/Tooltip' import { UserTokenBadge } from 'components/user-token-badge/UserTokenBadge' +import { useClaimFees } from 'hooks/useClaimFees' import { useCoverPhoto } from 'hooks/useCoverPhoto' +import { getLastConnectedSolWallet } from 'pages/artist-coins-launchpad-page/utils' import { env } from 'services/env' +import { reportToSentry } from 'store/errors/reportToSentry' import { copyToClipboard } from 'utils/clipboardUtil' import { push } from 'utils/navigation' const messages = coinDetailsMessages.coinInfo const overflowMessages = coinDetailsMessages.overflowMenu +const toastMessages = coinDetailsMessages.toasts const BANNER_HEIGHT = 120 @@ -252,15 +261,54 @@ export const AssetInfoSection = ({ mint }: AssetInfoSectionProps) => { const { data: coin, isLoading } = useArtistCoin(mint) - const { data: currentUserId } = useCurrentUserId() - const { data: userCoins } = useUserCoins({ userId: currentUserId }) + const { data: currentUser } = useCurrentAccountUser() + const { data: userCoins } = useUserCoins({ userId: currentUser?.user_id }) const userToken = useMemo( () => userCoins?.find((coin) => coin.mint === mint), [userCoins, mint] ) + const isCoinCreator = coin?.ownerId === currentUser?.user_id const discordOAuthLink = useDiscordOAuthLink(userToken?.ticker) const { balance: userTokenBalance } = userToken ?? {} + // Get wallet addresses for claim fee + const { data: connectedWallets } = useConnectedWallets() + const externalSolWallet = useMemo( + () => getLastConnectedSolWallet(connectedWallets), + [connectedWallets] + ) + + // Claim fee hook + const { mutate: claimFees, isPending: isClaimFeesPending } = useClaimFees({ + onSuccess: () => { + toast(toastMessages.feesClaimed) + }, + onError: (error) => { + reportToSentry({ + error, + feature: Feature.ArtistCoins, + name: 'Failed to claim artist coin fees', + additionalInfo: { + coin, + tokenMint: mint, + unclaimedFees, + totalArtistEarnings + } + }) + toast(toastMessages.feesClaimFailed) + } + }) + + const unclaimedFees = coin?.dynamicBondingCurve?.creatorQuoteFee ?? 0 + const formattedUnclaimedFees = useMemo(() => { + return wAUDIO(BigInt(unclaimedFees)).toShorthand() + }, [unclaimedFees]) + const totalArtistEarnings = + coin?.dynamicBondingCurve?.totalTradingQuoteFee ?? 0 + const formattedTotalArtistEarnings = useMemo(() => { + // Here we divide by 2 because the artist only gets half of the fees (this value includes the AUDIO network fees) + return wAUDIO(BigInt(Math.trunc(totalArtistEarnings / 2))).toShorthand() + }, [totalArtistEarnings]) const descriptionParagraphs = coin?.description?.split('\n') ?? [] const openDiscord = () => { @@ -280,6 +328,30 @@ export const AssetInfoSection = ({ mint }: AssetInfoSectionProps) => { toast(overflowMessages.copiedToClipboard) }, [mint, toast]) + const handleClaimFees = useCallback(() => { + if (!externalSolWallet || !mint || !currentUser?.spl_wallet) { + toast(toastMessages.feesClaimFailed) + reportToSentry({ + error: new Error('Unknown error while claiming fees'), + feature: Feature.ArtistCoins, + name: 'No external Solana wallet connected', + additionalInfo: { + coin, + mint, + externalSolWallet, + currentUser + } + }) + return + } + + claimFees({ + tokenMint: mint, + ownerWalletAddress: externalSolWallet.address, + receiverWalletAddress: currentUser.spl_wallet // Using same wallet for owner and receiver + }) + }, [externalSolWallet, mint, currentUser, claimFees, toast, coin]) + if (isLoading || !coin) { return } @@ -406,6 +478,90 @@ export const AssetInfoSection = ({ mint }: AssetInfoSectionProps) => { {shortenSPLAddress(mint)} + + + + + {overflowMessages.vestingSchedule} + + + + + + + {overflowMessages.vestingScheduleValue} + + + + + + {overflowMessages.artistEarnings} + + + + + + + {formattedTotalArtistEarnings} {overflowMessages.$audio} + + + {isCoinCreator ? ( + + + + {overflowMessages.unclaimedFees} + + + + + + + {unclaimedFees > 0 ? ( + + + {overflowMessages.claim} + + {isClaimFeesPending ? ( + + ) : null} + + ) : null} + + + {formattedUnclaimedFees} {overflowMessages.$audio} + + + + ) : null} + ) }