Skip to content

Commit

Permalink
feat: redpacket history for solana (#12042)
Browse files Browse the repository at this point in the history
  • Loading branch information
nuanyang233 authored Jan 17, 2025
1 parent c6fcd31 commit bfcf95d
Show file tree
Hide file tree
Showing 26 changed files with 340 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ export function SolanaRedPacketConfirm() {
DEFAULT_DURATION,
!!isRandom,
claimer,
message,
creator,
message,
)
: tokenMint ?
createWithSplToken(
Expand All @@ -163,8 +163,9 @@ export function SolanaRedPacketConfirm() {
DEFAULT_DURATION,
!!isRandom,
claimer,
message,

creator,
message,
)
: null)
if (!result) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ import { RoutePaths } from '../../constants.js'
import { CreateSolRedPacket } from './CreateRedpacket.js'
import { SolanaRedPacketConfirm } from './RedpacketConfirm.js'
import { CustomCover } from '../views/CustomCover.js'
import { History } from '../views/History.js'
import { HistoryDetail } from '../views/HistoryDetail.js'

export function SolRedPacketRoutes() {
return (
<Routes>
<Route path={RoutePaths.Create}>
<Route index path={RoutePaths.CreateSolanaRedPacket} element={<CreateSolRedPacket />} />
</Route>

<Route path={RoutePaths.History}>
<Route index element={<History />} />
<Route path={RoutePaths.HistoryDetail} element={<HistoryDetail />} />
</Route>
<Route path={RoutePaths.CustomCover} element={<CustomCover />} />
<Route path={RoutePaths.Confirm}>
<Route path={RoutePaths.ConfirmSolanaRedPacket} element={<SolanaRedPacketConfirm />} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { Trans } from '@lingui/react/macro'
import { Icons } from '@masknet/icons'
import { EmojiAvatar } from '@masknet/shared'
import { NetworkPluginID } from '@masknet/shared-base'
import { makeStyles } from '@masknet/theme'
import { useAccount } from '@masknet/web3-hooks-base'
import { EVMExplorerResolver } from '@masknet/web3-providers'
import { useAccount, useWeb3Utils } from '@masknet/web3-hooks-base'
import type { FireflyRedPacketAPI } from '@masknet/web3-providers/types'
import { formatBalance, isSameAddress } from '@masknet/web3-shared-base'
import { formatEthereumAddress } from '@masknet/web3-shared-evm'
import { Typography } from '@mui/material'
import { memo, type HTMLProps } from 'react'

Expand Down Expand Up @@ -68,7 +65,9 @@ interface Props extends HTMLProps<HTMLDivElement> {

export const ClaimRecord = memo(function ClaimRecord({ className, record, chainId, ...rest }: Props) {
const { classes, theme } = useStyles()
const account = useAccount(NetworkPluginID.PLUGIN_EVM)
const account = useAccount()
const Utils = useWeb3Utils()

return (
<div className={classes.container} {...rest}>
<EmojiAvatar value={record.creator} />
Expand All @@ -84,8 +83,8 @@ export const ClaimRecord = memo(function ClaimRecord({ className, record, chainI
</div>
: null}
<Typography className={classes.address}>
<Typography component="span">{formatEthereumAddress(record.creator, 4)}</Typography>
<a href={EVMExplorerResolver.addressLink(chainId, record.creator)} target="_blank">
<Typography component="span">{Utils.formatAddress(record.creator, 4)}</Typography>
<a href={Utils.explorerResolver.addressLink(chainId, record.creator)} target="_blank">
<Icons.LinkOut size={20} color={theme.palette.maskColor.second} />
</a>
</Typography>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Trans } from '@lingui/react/macro'
import { RedPacketMetaKey } from '@masknet/shared-base'
import { NetworkPluginID, RedPacketMetaKey } from '@masknet/shared-base'
import { ActionButton, makeStyles, type ActionButtonProps } from '@masknet/theme'
import { FireflyRedPacket } from '@masknet/web3-providers'
import { FireflyRedPacketAPI } from '@masknet/web3-providers/types'
import type { ChainId } from '@masknet/web3-shared-evm'
import { useMediaQuery, type Theme } from '@mui/material'
import { memo, useCallback, useContext } from 'react'
import { useAsyncFn } from 'react-use'
import { CompositionTypeContext } from '../contexts/CompositionTypeContext.js'
import { useRefundCallback } from '../hooks/useRefundCallback.js'
import { useRefundCallback, useSolanaRefundCallback } from '../hooks/useRefundCallback.js'
import { openComposition } from '../openComposition.js'
import { useEnvironmentContext } from '@masknet/web3-hooks-base'

const useStyles = makeStyles()((theme) => {
const smallQuery = `@media (max-width: ${theme.breakpoints.values.sm}px)`
Expand Down Expand Up @@ -80,10 +80,13 @@ export const RedPacketActionButton = memo(function RedPacketActionButton({
...rest
}: Props) {
const { classes, cx } = useStyles()
const { pluginID } = useEnvironmentContext()
const isSmall = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
const compositionType = useContext(CompositionTypeContext)

const [{ loading: isRefunding }, refunded, refundCallback] = useRefundCallback(4, account, rpid, chainId)
const [{ loading: isSolanaRefunding }, solanaRefunded, refundSolanaCallback] = useSolanaRefundCallback(rpid)

const statusToTransMap = {
[FireflyRedPacketAPI.RedPacketStatus.Send]: <Trans>Send</Trans>,
[FireflyRedPacketAPI.RedPacketStatus.Expired]: <Trans>Expired</Trans>,
Expand All @@ -96,14 +99,6 @@ export const RedPacketActionButton = memo(function RedPacketActionButton({
const [{ loading: isSharing }, shareCallback] = useAsyncFn(async () => {
if (!shareFrom || !themeId || !createdAt) return

const payloadImage = await FireflyRedPacket.getPayloadUrlByThemeId(
themeId,
shareFrom,
tokenInfo.amount,
'fungible',
tokenInfo.symbol,
Number(tokenInfo.decimals),
)
openComposition(
RedPacketMetaKey,
{
Expand All @@ -125,23 +120,24 @@ export const RedPacketActionButton = memo(function RedPacketActionButton({
total: tokenInfo.amount,
},
compositionType,
{ claimRequirements: claim_strategy, payloadImage },
{ claimRequirements: claim_strategy },
)
}, [])

const redpacketStatus = refunded ? RedPacketStatus.Refund : propRedpacketStatus
const redpacketStatus = refunded || solanaRefunded ? RedPacketStatus.Refund : propRedpacketStatus

const handleClick = useCallback(async () => {
if (canResend) onResend?.()
else if (redpacketStatus === RedPacketStatus.Send || redpacketStatus === RedPacketStatus.View)
await shareCallback()
else if (redpacketStatus === RedPacketStatus.Refunding) await refundCallback()
}, [redpacketStatus, shareCallback, refundCallback, canResend, onResend])
else if (redpacketStatus === RedPacketStatus.Refunding)
pluginID === NetworkPluginID.PLUGIN_SOLANA ? await refundSolanaCallback() : await refundCallback()
}, [redpacketStatus, shareCallback, refundCallback, canResend, onResend, refundSolanaCallback, pluginID])

return (
<ActionButton
{...rest}
loading={isRefunding || isSharing}
loading={isRefunding || isSolanaRefunding || isSharing}
fullWidth={isSmall}
onClick={handleClick}
className={cx(classes.actionButton, rest.className)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { t } from '@lingui/core/macro'
import { Trans } from '@lingui/react/macro'
import { TokenIcon } from '@masknet/shared'
import { NetworkPluginID } from '@masknet/shared-base'
import { openWindow, useEverSeen } from '@masknet/shared-base-ui'
import { ActionButton, makeStyles, ShadowRootTooltip, TextOverflowTooltip } from '@masknet/theme'
import { useChainContext, useFungibleToken, useNetworkDescriptor } from '@masknet/web3-hooks-base'
import { EVMExplorerResolver } from '@masknet/web3-providers'
import {
useChainContext,
useEnvironmentContext,
useFungibleToken,
useNetworkDescriptor,
} from '@masknet/web3-hooks-base'
import { EVMExplorerResolver, SolanaExplorerResolver } from '@masknet/web3-providers'
import { FireflyRedPacketAPI, type RedPacketJSONPayload } from '@masknet/web3-providers/types'
import { formatBalance, TokenType } from '@masknet/web3-shared-base'
import {
Expand All @@ -23,9 +27,9 @@ import { useNavigate } from 'react-router-dom'
import { RoutePaths } from '../../constants.js'
import { RedPacketRPC } from '../../messages.js'
import { useCreateRedPacketReceipt } from '../hooks/useCreateRedPacketReceipt.js'
import { useRedpacketToken } from '../hooks/useRedpacketToken.js'
import { RedPacketActionButton } from './RedPacketActionButton.js'
import { formatTokenAmount } from '../helpers/formatTokenAmount.js'
import { NetworkPluginID } from '@masknet/shared-base'

const DEFAULT_BACKGROUND = NETWORK_DESCRIPTORS.find((x) => x.chainId === ChainId.Mainnet)!.backgroundGradient!
const useStyles = makeStyles<{ background?: string; backgroundIcon?: string }>()((
Expand Down Expand Up @@ -197,28 +201,30 @@ export const RedPacketRecord = memo(function RedPacketRecord({
const [seen, redpacketRef] = useEverSeen()
const chainId = history.chain_id

const { account } = useChainContext<NetworkPluginID.PLUGIN_EVM>()
const networkDescriptor = useNetworkDescriptor(NetworkPluginID.PLUGIN_EVM, chainId)
const { account } = useChainContext()
const { pluginID } = useEnvironmentContext()
const networkDescriptor = useNetworkDescriptor(pluginID, chainId)

const { classes, cx } = useStyles({
background: networkDescriptor?.backgroundGradient,
backgroundIcon: networkDescriptor ? `url("${networkDescriptor.icon}")` : undefined,
})

// Only concern about MATIC token which has been renamed to POL
const { data: tokenAddress } = useRedpacketToken(
chainId,
history.trans_hash ?? '',
seen && token_symbol === 'MATIC' && !!history.trans_hash,
)
const { data: token } = useFungibleToken(NetworkPluginID.PLUGIN_EVM, tokenAddress, undefined, { chainId })
const tokenSymbol = token?.symbol ?? token_symbol
const tokenSymbol = token_symbol
const contractAddress = useRedPacketConstant(chainId, 'HAPPY_RED_PACKET_ADDRESS_V4')
const { data: redpacketRecord } = useQuery({
queryKey: ['redpacket', 'by-tx-hash', history.trans_hash],
queryFn: history.trans_hash ? () => RedPacketRPC.getRedPacketRecord(history.trans_hash!) : skipToken,
})
const { data: createSuccessResult } = useCreateRedPacketReceipt(history.trans_hash ?? '', chainId)
const { data: createSuccessResult } = useCreateRedPacketReceipt(
pluginID === NetworkPluginID.PLUGIN_SOLANA ? history.redpacket_id : (history.trans_hash ?? ''),
chainId,
seen,
)

// Only concern about MATIC token which has been renamed to POL
const { data: token } = useFungibleToken(pluginID, createSuccessResult?.token_address, undefined, { chainId })

const isViewStatus = redpacket_status === FireflyRedPacketAPI.RedPacketStatus.View
const canResend = isViewStatus && !!redpacketRecord && !!createSuccessResult

Expand All @@ -235,15 +241,16 @@ export const RedPacketRecord = memo(function RedPacketRecord({
size={36}
badgeSize={16}
chainId={chainId}
address={token?.address ?? tokenAddress!}
address={token?.address ?? createSuccessResult?.token_address}
logoURL={token_logo}
symbol={token?.symbol}
name={token?.name}
symbol={token_symbol}
name={token_symbol}
disableBadge={pluginID === NetworkPluginID.PLUGIN_SOLANA}
/>
<div className={classes.status}>
<Typography className={classes.total}>
{formatBalance(amount, token_decimal, { significant: 2, isPrecise: true })}{' '}
{tokenSymbol ?? token?.symbol ?? '--'}
{tokenSymbol ?? token_symbol ?? '--'}
</Typography>
<Typography className={classes.progress} component="div">
{!onlyView ?
Expand Down Expand Up @@ -286,7 +293,11 @@ export const RedPacketRecord = memo(function RedPacketRecord({
<ActionButton
className={cx(classes.viewButton, classes.actionButton)}
onClick={() => {
openWindow(EVMExplorerResolver.transactionLink(chainId, history.trans_hash!), '_blank')
const link =
pluginID === NetworkPluginID.PLUGIN_SOLANA ?
SolanaExplorerResolver.transactionLink(chainId, history.trans_hash)
: EVMExplorerResolver.transactionLink(chainId, history.trans_hash!)
openWindow(link, '_blank')
}}>
{t`View`}
</ActionButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export function RouterDialog(props: InjectedDialogProps & { pageMap: Record<RedP
onClick={() => navigate({ pathname: RoutePaths.History, search: `tab=${HistoryTabs.Sent}` })}
/>
),
[RoutePaths.CreateSolanaRedPacket]: (
<Icons.History
onClick={() => navigate({ pathname: RoutePaths.History, search: `tab=${HistoryTabs.Sent}` })}
/>
),
[RoutePaths.CreateNftRedPacket]: (
<Icons.History
onClick={() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { getRpProgram } from './getRpProgram.js'

export async function getRedpacket(id: string) {
const program = await getRpProgram()
return program.account.redPacket.fetch(id)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { web3 } from '@coral-xyz/anchor'
import { getRpProgram } from './getRpProgram.js'
import * as SolanaWeb3 from /* webpackDefer: true */ '@solana/web3.js'

export async function refundNativeToken(id: string, creator: SolanaWeb3.PublicKey) {
const program = await getRpProgram()
return program.methods
.withdrawWithNativeToken()
.accounts({
// @ts-expect-error missing type
redPacket: new SolanaWeb3.PublicKey(id),
signer: new SolanaWeb3.PublicKey(creator),
systemProgram: web3.SystemProgram.programId,
})
.rpc({
commitment: 'confirmed',
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token'
import * as SolanaWeb3 from /* webpackDefer: true */ '@solana/web3.js'
import { getRpProgram } from './getRpProgram.js'

export async function refundSplToken({
id,
tokenMint,
tokenProgram,
tokenAccount,
creator,
}: {
id: string
tokenMint: SolanaWeb3.PublicKey
tokenProgram: SolanaWeb3.PublicKey | null
tokenAccount: SolanaWeb3.PublicKey | null
creator: SolanaWeb3.PublicKey
}) {
if (!tokenProgram || !tokenAccount) throw new Error('Token program or account not found')

const program = await getRpProgram()
const vault = getAssociatedTokenAddressSync(
new SolanaWeb3.PublicKey(tokenMint),
new SolanaWeb3.PublicKey(id),
true,
new SolanaWeb3.PublicKey(tokenProgram),
)

return program.methods
.withdrawWithSplToken()
.accounts({
// @ts-expect-error missing type
redPacket: new SolanaWeb3.PublicKey(id),
signer: creator,
vault,
tokenAccount,
tokenMint,
tokenProgram,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
})
.rpc({
commitment: 'confirmed',
})
}
Loading

0 comments on commit bfcf95d

Please sign in to comment.