Skip to content

Commit bfcf95d

Browse files
authored
feat: redpacket history for solana (#12042)
1 parent c6fcd31 commit bfcf95d

26 files changed

+340
-103
lines changed

packages/plugins/RedPacket/src/SiteAdaptor/SolanaRedpacketDialog/RedpacketConfirm.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ export function SolanaRedPacketConfirm() {
151151
DEFAULT_DURATION,
152152
!!isRandom,
153153
claimer,
154-
message,
155154
creator,
155+
message,
156156
)
157157
: tokenMint ?
158158
createWithSplToken(
@@ -163,8 +163,9 @@ export function SolanaRedPacketConfirm() {
163163
DEFAULT_DURATION,
164164
!!isRandom,
165165
claimer,
166-
message,
166+
167167
creator,
168+
message,
168169
)
169170
: null)
170171
if (!result) return

packages/plugins/RedPacket/src/SiteAdaptor/SolanaRedpacketDialog/Routes.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ import { RoutePaths } from '../../constants.js'
33
import { CreateSolRedPacket } from './CreateRedpacket.js'
44
import { SolanaRedPacketConfirm } from './RedpacketConfirm.js'
55
import { CustomCover } from '../views/CustomCover.js'
6+
import { History } from '../views/History.js'
7+
import { HistoryDetail } from '../views/HistoryDetail.js'
8+
69
export function SolRedPacketRoutes() {
710
return (
811
<Routes>
912
<Route path={RoutePaths.Create}>
1013
<Route index path={RoutePaths.CreateSolanaRedPacket} element={<CreateSolRedPacket />} />
1114
</Route>
12-
15+
<Route path={RoutePaths.History}>
16+
<Route index element={<History />} />
17+
<Route path={RoutePaths.HistoryDetail} element={<HistoryDetail />} />
18+
</Route>
1319
<Route path={RoutePaths.CustomCover} element={<CustomCover />} />
1420
<Route path={RoutePaths.Confirm}>
1521
<Route path={RoutePaths.ConfirmSolanaRedPacket} element={<SolanaRedPacketConfirm />} />

packages/plugins/RedPacket/src/SiteAdaptor/components/ClaimRecord.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import { Trans } from '@lingui/react/macro'
22
import { Icons } from '@masknet/icons'
33
import { EmojiAvatar } from '@masknet/shared'
4-
import { NetworkPluginID } from '@masknet/shared-base'
54
import { makeStyles } from '@masknet/theme'
6-
import { useAccount } from '@masknet/web3-hooks-base'
7-
import { EVMExplorerResolver } from '@masknet/web3-providers'
5+
import { useAccount, useWeb3Utils } from '@masknet/web3-hooks-base'
86
import type { FireflyRedPacketAPI } from '@masknet/web3-providers/types'
97
import { formatBalance, isSameAddress } from '@masknet/web3-shared-base'
10-
import { formatEthereumAddress } from '@masknet/web3-shared-evm'
118
import { Typography } from '@mui/material'
129
import { memo, type HTMLProps } from 'react'
1310

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

6966
export const ClaimRecord = memo(function ClaimRecord({ className, record, chainId, ...rest }: Props) {
7067
const { classes, theme } = useStyles()
71-
const account = useAccount(NetworkPluginID.PLUGIN_EVM)
68+
const account = useAccount()
69+
const Utils = useWeb3Utils()
70+
7271
return (
7372
<div className={classes.container} {...rest}>
7473
<EmojiAvatar value={record.creator} />
@@ -84,8 +83,8 @@ export const ClaimRecord = memo(function ClaimRecord({ className, record, chainI
8483
</div>
8584
: null}
8685
<Typography className={classes.address}>
87-
<Typography component="span">{formatEthereumAddress(record.creator, 4)}</Typography>
88-
<a href={EVMExplorerResolver.addressLink(chainId, record.creator)} target="_blank">
86+
<Typography component="span">{Utils.formatAddress(record.creator, 4)}</Typography>
87+
<a href={Utils.explorerResolver.addressLink(chainId, record.creator)} target="_blank">
8988
<Icons.LinkOut size={20} color={theme.palette.maskColor.second} />
9089
</a>
9190
</Typography>

packages/plugins/RedPacket/src/SiteAdaptor/components/RedPacketActionButton.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { Trans } from '@lingui/react/macro'
2-
import { RedPacketMetaKey } from '@masknet/shared-base'
2+
import { NetworkPluginID, RedPacketMetaKey } from '@masknet/shared-base'
33
import { ActionButton, makeStyles, type ActionButtonProps } from '@masknet/theme'
4-
import { FireflyRedPacket } from '@masknet/web3-providers'
54
import { FireflyRedPacketAPI } from '@masknet/web3-providers/types'
65
import type { ChainId } from '@masknet/web3-shared-evm'
76
import { useMediaQuery, type Theme } from '@mui/material'
87
import { memo, useCallback, useContext } from 'react'
98
import { useAsyncFn } from 'react-use'
109
import { CompositionTypeContext } from '../contexts/CompositionTypeContext.js'
11-
import { useRefundCallback } from '../hooks/useRefundCallback.js'
10+
import { useRefundCallback, useSolanaRefundCallback } from '../hooks/useRefundCallback.js'
1211
import { openComposition } from '../openComposition.js'
12+
import { useEnvironmentContext } from '@masknet/web3-hooks-base'
1313

1414
const useStyles = makeStyles()((theme) => {
1515
const smallQuery = `@media (max-width: ${theme.breakpoints.values.sm}px)`
@@ -80,10 +80,13 @@ export const RedPacketActionButton = memo(function RedPacketActionButton({
8080
...rest
8181
}: Props) {
8282
const { classes, cx } = useStyles()
83+
const { pluginID } = useEnvironmentContext()
8384
const isSmall = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
8485
const compositionType = useContext(CompositionTypeContext)
8586

8687
const [{ loading: isRefunding }, refunded, refundCallback] = useRefundCallback(4, account, rpid, chainId)
88+
const [{ loading: isSolanaRefunding }, solanaRefunded, refundSolanaCallback] = useSolanaRefundCallback(rpid)
89+
8790
const statusToTransMap = {
8891
[FireflyRedPacketAPI.RedPacketStatus.Send]: <Trans>Send</Trans>,
8992
[FireflyRedPacketAPI.RedPacketStatus.Expired]: <Trans>Expired</Trans>,
@@ -96,14 +99,6 @@ export const RedPacketActionButton = memo(function RedPacketActionButton({
9699
const [{ loading: isSharing }, shareCallback] = useAsyncFn(async () => {
97100
if (!shareFrom || !themeId || !createdAt) return
98101

99-
const payloadImage = await FireflyRedPacket.getPayloadUrlByThemeId(
100-
themeId,
101-
shareFrom,
102-
tokenInfo.amount,
103-
'fungible',
104-
tokenInfo.symbol,
105-
Number(tokenInfo.decimals),
106-
)
107102
openComposition(
108103
RedPacketMetaKey,
109104
{
@@ -125,23 +120,24 @@ export const RedPacketActionButton = memo(function RedPacketActionButton({
125120
total: tokenInfo.amount,
126121
},
127122
compositionType,
128-
{ claimRequirements: claim_strategy, payloadImage },
123+
{ claimRequirements: claim_strategy },
129124
)
130125
}, [])
131126

132-
const redpacketStatus = refunded ? RedPacketStatus.Refund : propRedpacketStatus
127+
const redpacketStatus = refunded || solanaRefunded ? RedPacketStatus.Refund : propRedpacketStatus
133128

134129
const handleClick = useCallback(async () => {
135130
if (canResend) onResend?.()
136131
else if (redpacketStatus === RedPacketStatus.Send || redpacketStatus === RedPacketStatus.View)
137132
await shareCallback()
138-
else if (redpacketStatus === RedPacketStatus.Refunding) await refundCallback()
139-
}, [redpacketStatus, shareCallback, refundCallback, canResend, onResend])
133+
else if (redpacketStatus === RedPacketStatus.Refunding)
134+
pluginID === NetworkPluginID.PLUGIN_SOLANA ? await refundSolanaCallback() : await refundCallback()
135+
}, [redpacketStatus, shareCallback, refundCallback, canResend, onResend, refundSolanaCallback, pluginID])
140136

141137
return (
142138
<ActionButton
143139
{...rest}
144-
loading={isRefunding || isSharing}
140+
loading={isRefunding || isSolanaRefunding || isSharing}
145141
fullWidth={isSmall}
146142
onClick={handleClick}
147143
className={cx(classes.actionButton, rest.className)}

packages/plugins/RedPacket/src/SiteAdaptor/components/RedPacketRecord.tsx

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { t } from '@lingui/core/macro'
22
import { Trans } from '@lingui/react/macro'
33
import { TokenIcon } from '@masknet/shared'
4-
import { NetworkPluginID } from '@masknet/shared-base'
54
import { openWindow, useEverSeen } from '@masknet/shared-base-ui'
65
import { ActionButton, makeStyles, ShadowRootTooltip, TextOverflowTooltip } from '@masknet/theme'
7-
import { useChainContext, useFungibleToken, useNetworkDescriptor } from '@masknet/web3-hooks-base'
8-
import { EVMExplorerResolver } from '@masknet/web3-providers'
6+
import {
7+
useChainContext,
8+
useEnvironmentContext,
9+
useFungibleToken,
10+
useNetworkDescriptor,
11+
} from '@masknet/web3-hooks-base'
12+
import { EVMExplorerResolver, SolanaExplorerResolver } from '@masknet/web3-providers'
913
import { FireflyRedPacketAPI, type RedPacketJSONPayload } from '@masknet/web3-providers/types'
1014
import { formatBalance, TokenType } from '@masknet/web3-shared-base'
1115
import {
@@ -23,9 +27,9 @@ import { useNavigate } from 'react-router-dom'
2327
import { RoutePaths } from '../../constants.js'
2428
import { RedPacketRPC } from '../../messages.js'
2529
import { useCreateRedPacketReceipt } from '../hooks/useCreateRedPacketReceipt.js'
26-
import { useRedpacketToken } from '../hooks/useRedpacketToken.js'
2730
import { RedPacketActionButton } from './RedPacketActionButton.js'
2831
import { formatTokenAmount } from '../helpers/formatTokenAmount.js'
32+
import { NetworkPluginID } from '@masknet/shared-base'
2933

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

200-
const { account } = useChainContext<NetworkPluginID.PLUGIN_EVM>()
201-
const networkDescriptor = useNetworkDescriptor(NetworkPluginID.PLUGIN_EVM, chainId)
204+
const { account } = useChainContext()
205+
const { pluginID } = useEnvironmentContext()
206+
const networkDescriptor = useNetworkDescriptor(pluginID, chainId)
202207

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

208-
// Only concern about MATIC token which has been renamed to POL
209-
const { data: tokenAddress } = useRedpacketToken(
210-
chainId,
211-
history.trans_hash ?? '',
212-
seen && token_symbol === 'MATIC' && !!history.trans_hash,
213-
)
214-
const { data: token } = useFungibleToken(NetworkPluginID.PLUGIN_EVM, tokenAddress, undefined, { chainId })
215-
const tokenSymbol = token?.symbol ?? token_symbol
213+
const tokenSymbol = token_symbol
216214
const contractAddress = useRedPacketConstant(chainId, 'HAPPY_RED_PACKET_ADDRESS_V4')
217215
const { data: redpacketRecord } = useQuery({
218216
queryKey: ['redpacket', 'by-tx-hash', history.trans_hash],
219217
queryFn: history.trans_hash ? () => RedPacketRPC.getRedPacketRecord(history.trans_hash!) : skipToken,
220218
})
221-
const { data: createSuccessResult } = useCreateRedPacketReceipt(history.trans_hash ?? '', chainId)
219+
const { data: createSuccessResult } = useCreateRedPacketReceipt(
220+
pluginID === NetworkPluginID.PLUGIN_SOLANA ? history.redpacket_id : (history.trans_hash ?? ''),
221+
chainId,
222+
seen,
223+
)
224+
225+
// Only concern about MATIC token which has been renamed to POL
226+
const { data: token } = useFungibleToken(pluginID, createSuccessResult?.token_address, undefined, { chainId })
227+
222228
const isViewStatus = redpacket_status === FireflyRedPacketAPI.RedPacketStatus.View
223229
const canResend = isViewStatus && !!redpacketRecord && !!createSuccessResult
224230

@@ -235,15 +241,16 @@ export const RedPacketRecord = memo(function RedPacketRecord({
235241
size={36}
236242
badgeSize={16}
237243
chainId={chainId}
238-
address={token?.address ?? tokenAddress!}
244+
address={token?.address ?? createSuccessResult?.token_address}
239245
logoURL={token_logo}
240-
symbol={token?.symbol}
241-
name={token?.name}
246+
symbol={token_symbol}
247+
name={token_symbol}
248+
disableBadge={pluginID === NetworkPluginID.PLUGIN_SOLANA}
242249
/>
243250
<div className={classes.status}>
244251
<Typography className={classes.total}>
245252
{formatBalance(amount, token_decimal, { significant: 2, isPrecise: true })}{' '}
246-
{tokenSymbol ?? token?.symbol ?? '--'}
253+
{tokenSymbol ?? token_symbol ?? '--'}
247254
</Typography>
248255
<Typography className={classes.progress} component="div">
249256
{!onlyView ?
@@ -286,7 +293,11 @@ export const RedPacketRecord = memo(function RedPacketRecord({
286293
<ActionButton
287294
className={cx(classes.viewButton, classes.actionButton)}
288295
onClick={() => {
289-
openWindow(EVMExplorerResolver.transactionLink(chainId, history.trans_hash!), '_blank')
296+
const link =
297+
pluginID === NetworkPluginID.PLUGIN_SOLANA ?
298+
SolanaExplorerResolver.transactionLink(chainId, history.trans_hash)
299+
: EVMExplorerResolver.transactionLink(chainId, history.trans_hash!)
300+
openWindow(link, '_blank')
290301
}}>
291302
{t`View`}
292303
</ActionButton>

packages/plugins/RedPacket/src/SiteAdaptor/components/RouterDialog.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ export function RouterDialog(props: InjectedDialogProps & { pageMap: Record<RedP
8888
onClick={() => navigate({ pathname: RoutePaths.History, search: `tab=${HistoryTabs.Sent}` })}
8989
/>
9090
),
91+
[RoutePaths.CreateSolanaRedPacket]: (
92+
<Icons.History
93+
onClick={() => navigate({ pathname: RoutePaths.History, search: `tab=${HistoryTabs.Sent}` })}
94+
/>
95+
),
9196
[RoutePaths.CreateNftRedPacket]: (
9297
<Icons.History
9398
onClick={() => {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { getRpProgram } from './getRpProgram.js'
2+
3+
export async function getRedpacket(id: string) {
4+
const program = await getRpProgram()
5+
return program.account.redPacket.fetch(id)
6+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { web3 } from '@coral-xyz/anchor'
2+
import { getRpProgram } from './getRpProgram.js'
3+
import * as SolanaWeb3 from /* webpackDefer: true */ '@solana/web3.js'
4+
5+
export async function refundNativeToken(id: string, creator: SolanaWeb3.PublicKey) {
6+
const program = await getRpProgram()
7+
return program.methods
8+
.withdrawWithNativeToken()
9+
.accounts({
10+
// @ts-expect-error missing type
11+
redPacket: new SolanaWeb3.PublicKey(id),
12+
signer: new SolanaWeb3.PublicKey(creator),
13+
systemProgram: web3.SystemProgram.programId,
14+
})
15+
.rpc({
16+
commitment: 'confirmed',
17+
})
18+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token'
2+
import * as SolanaWeb3 from /* webpackDefer: true */ '@solana/web3.js'
3+
import { getRpProgram } from './getRpProgram.js'
4+
5+
export async function refundSplToken({
6+
id,
7+
tokenMint,
8+
tokenProgram,
9+
tokenAccount,
10+
creator,
11+
}: {
12+
id: string
13+
tokenMint: SolanaWeb3.PublicKey
14+
tokenProgram: SolanaWeb3.PublicKey | null
15+
tokenAccount: SolanaWeb3.PublicKey | null
16+
creator: SolanaWeb3.PublicKey
17+
}) {
18+
if (!tokenProgram || !tokenAccount) throw new Error('Token program or account not found')
19+
20+
const program = await getRpProgram()
21+
const vault = getAssociatedTokenAddressSync(
22+
new SolanaWeb3.PublicKey(tokenMint),
23+
new SolanaWeb3.PublicKey(id),
24+
true,
25+
new SolanaWeb3.PublicKey(tokenProgram),
26+
)
27+
28+
return program.methods
29+
.withdrawWithSplToken()
30+
.accounts({
31+
// @ts-expect-error missing type
32+
redPacket: new SolanaWeb3.PublicKey(id),
33+
signer: creator,
34+
vault,
35+
tokenAccount,
36+
tokenMint,
37+
tokenProgram,
38+
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
39+
})
40+
.rpc({
41+
commitment: 'confirmed',
42+
})
43+
}

0 commit comments

Comments
 (0)