From f4292756d086bfb66660661e3afc4a9872bf550a Mon Sep 17 00:00:00 2001 From: sven Tan Date: Fri, 6 Dec 2024 23:07:12 +0800 Subject: [PATCH] Portal invite (#3022) * finish * fix * fix ui * fix * switch testnet * fix * fix build --- .../src/app/faucet/inviter/[address]/page.tsx | 11 + .../src/app/invitation/[address]/page.tsx | 7 + .../src/app/invitation/page..tsx | 8 + .../src/app/inviter/[address]/page.tsx | 14 +- .../rooch-portal-v2/src/app/inviter/page.tsx | 6 + .../auth/session-key-guard-button-v1.tsx | 49 ++-- .../rooch-portal-v2/src/hooks/use-networks.ts | 13 +- .../src/layouts/config-nav-dashboard.tsx | 6 + infra/rooch-portal-v2/src/middleware.ts | 1 + infra/rooch-portal-v2/src/routes/paths.ts | 1 + .../assets/components/asset-row-item.tsx | 1 - .../src/sections/faucet/inviter.tsx | 240 ++++++++++++++++++ .../src/sections/faucet/view.tsx | 5 + .../components/invitation-list.tsx | 113 +++++++++ .../invitations/components/lottery-list.tsx | 176 +++++++++++++ .../src/sections/invitations/index.tsx | 137 ++++++++++ .../src/sections/inviter/index.tsx | 19 ++ .../src/sections/settings/view.tsx | 163 ++++++++---- infra/rooch-portal-v2/src/utils/inviter.ts | 2 + .../rooch-sdk/test-e2e/case/session.test.ts | 4 + 20 files changed, 891 insertions(+), 85 deletions(-) create mode 100644 infra/rooch-portal-v2/src/app/faucet/inviter/[address]/page.tsx create mode 100644 infra/rooch-portal-v2/src/app/invitation/[address]/page.tsx create mode 100644 infra/rooch-portal-v2/src/app/invitation/page..tsx create mode 100644 infra/rooch-portal-v2/src/app/inviter/page.tsx create mode 100644 infra/rooch-portal-v2/src/sections/faucet/inviter.tsx create mode 100644 infra/rooch-portal-v2/src/sections/invitations/components/invitation-list.tsx create mode 100644 infra/rooch-portal-v2/src/sections/invitations/components/lottery-list.tsx create mode 100644 infra/rooch-portal-v2/src/sections/invitations/index.tsx create mode 100644 infra/rooch-portal-v2/src/sections/inviter/index.tsx create mode 100644 infra/rooch-portal-v2/src/utils/inviter.ts diff --git a/infra/rooch-portal-v2/src/app/faucet/inviter/[address]/page.tsx b/infra/rooch-portal-v2/src/app/faucet/inviter/[address]/page.tsx new file mode 100644 index 0000000000..38e136e217 --- /dev/null +++ b/infra/rooch-portal-v2/src/app/faucet/inviter/[address]/page.tsx @@ -0,0 +1,11 @@ +import WalletGuard from 'src/components/guard/WalletGuard'; + +import { InviterFaucetView } from 'src/sections/faucet/inviter'; + +export const metadata = { title: `Faucet` }; + +export default function Page({ params }: { params: { address: string } }) { + return + + +} diff --git a/infra/rooch-portal-v2/src/app/invitation/[address]/page.tsx b/infra/rooch-portal-v2/src/app/invitation/[address]/page.tsx new file mode 100644 index 0000000000..f9d227c635 --- /dev/null +++ b/infra/rooch-portal-v2/src/app/invitation/[address]/page.tsx @@ -0,0 +1,7 @@ +import { InvitationsView } from 'src/sections/invitations/index'; + +export const metadata = { title: `Invitation` }; + +export default function Page({ params }: { params: { address: string } }) { + return ; +} diff --git a/infra/rooch-portal-v2/src/app/invitation/page..tsx b/infra/rooch-portal-v2/src/app/invitation/page..tsx new file mode 100644 index 0000000000..a95260cfd4 --- /dev/null +++ b/infra/rooch-portal-v2/src/app/invitation/page..tsx @@ -0,0 +1,8 @@ + +import { InvitationsView } from 'src/sections/invitations/index'; + +export const metadata = { title: `Invitation` }; + +export default function Page() { + return ; +} diff --git a/infra/rooch-portal-v2/src/app/inviter/[address]/page.tsx b/infra/rooch-portal-v2/src/app/inviter/[address]/page.tsx index 5108143092..3bbdb0d0c9 100644 --- a/infra/rooch-portal-v2/src/app/inviter/[address]/page.tsx +++ b/infra/rooch-portal-v2/src/app/inviter/[address]/page.tsx @@ -1,16 +1,6 @@ -import WalletGuard from 'src/components/guard/WalletGuard'; -import { SettingsView } from 'src/sections/settings/view'; +import { InviterView } from 'src/sections/inviter/index'; export default function Page({ params }: { params: { address: string } }) { - // window.localStorage.setItem('inviter', params.address) - console.log(params) - return ( - - - - ); + return ; } - - - diff --git a/infra/rooch-portal-v2/src/app/inviter/page.tsx b/infra/rooch-portal-v2/src/app/inviter/page.tsx new file mode 100644 index 0000000000..ec31ddc548 --- /dev/null +++ b/infra/rooch-portal-v2/src/app/inviter/page.tsx @@ -0,0 +1,6 @@ + +import { InviterView } from 'src/sections/inviter/index'; + +export default function Page() { + return ; +} diff --git a/infra/rooch-portal-v2/src/components/auth/session-key-guard-button-v1.tsx b/infra/rooch-portal-v2/src/components/auth/session-key-guard-button-v1.tsx index a0b305677a..72d5581a26 100644 --- a/infra/rooch-portal-v2/src/components/auth/session-key-guard-button-v1.tsx +++ b/infra/rooch-portal-v2/src/components/auth/session-key-guard-button-v1.tsx @@ -9,7 +9,15 @@ import { isSessionExpired } from 'src/utils/common'; import { toast } from 'src/components/snackbar'; -export default function SessionKeyGuardButtonV1({ children, desc, callback }: { children?: ReactNode, desc?: string, callback?: () => Promise }) { +export default function SessionKeyGuardButtonV1({ + children, + desc, + callback, +}: { + children?: ReactNode; + desc?: string; + callback?: () => Promise; +}) { const sessionKey = useCurrentSession(); const { mutateAsync: createSessionKey } = useCreateSessionKey(); const [loading, setLoading] = useState(false); @@ -27,13 +35,14 @@ export default function SessionKeyGuardButtonV1({ children, desc, callback }: { }, [sessionKey]); const handle = async () => { - setLoading(true) - if (sessionKey && !isCurrentSessionExpired) { - if (callback) { - await callback() - } - } else { - try { + setLoading(true); + + try { + if (sessionKey && !isCurrentSessionExpired) { + if (callback) { + await callback(); + } + } else { await createSessionKey({ appName: 'rooch-portal', appUrl: 'portal.rooch.network', @@ -46,19 +55,19 @@ export default function SessionKeyGuardButtonV1({ children, desc, callback }: { maxInactiveInterval: 60 * 60 * 8, }); if (callback) { - await callback() + await callback(); } - } catch (error) { - if (error.message) { - toast.error(error.message); - return; - } - toast.error(String(error)); } + } catch (error) { + if (error.message) { + toast.error(error.message); + return; + } + toast.error(String(error)); + } finally { + setLoading(false); } - - setLoading(false) - } + }; return sessionKey && !isCurrentSessionExpired && children ? ( children @@ -70,9 +79,7 @@ export default function SessionKeyGuardButtonV1({ children, desc, callback }: { loading={loading} onClick={handle} > - { - desc || 'Create Session Key' - } + {desc || 'Create Session Key'} ); } diff --git a/infra/rooch-portal-v2/src/hooks/use-networks.ts b/infra/rooch-portal-v2/src/hooks/use-networks.ts index 92e5937fc6..d45ebd04c2 100644 --- a/infra/rooch-portal-v2/src/hooks/use-networks.ts +++ b/infra/rooch-portal-v2/src/hooks/use-networks.ts @@ -20,13 +20,14 @@ const { networkConfig, useNetworkVariable, useNetworkVariables } = createNetwork variables: { roochOperatingAddress: ROOCH_NFT_OPERATING_ADDRESS, mintAddress: ROOCH_MINT_OPERATING_ADDRESS, - btcGasAddress: 'bc1prcajaj9n7e29u4dfp33x3hcf52yqeegspdpcd79pqu4fpr6llx4sugkfjt', + btcGasAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', gasMarketAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', faucetUrl: FAUCET_MAINNET, faucetAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', faucetObject: '0xd5723eda84f691ae2623da79312c7909b1737c5b3866ecc5dbd6aa21718ff15d', BTCMemPool: 'https://mempool.space/tx/', twitterOracleAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', + inviterCA: ['0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', 'invitation', 'InvitationConf'], }, }, testnet: { @@ -34,13 +35,14 @@ const { networkConfig, useNetworkVariable, useNetworkVariables } = createNetwork variables: { roochOperatingAddress: ROOCH_NFT_OPERATING_ADDRESS, mintAddress: ROOCH_MINT_OPERATING_ADDRESS, - btcGasAddress: 'tb1prcajaj9n7e29u4dfp33x3hcf52yqeegspdpcd79pqu4fpr6llx4stqqxgy', - gasMarketAddress: '0x872502737008ac71c4c008bb3846a688bfd9fa54c6724089ea51b72f813dc71e', + btcGasAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', + gasMarketAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e 744c13f2d9998bf76cc3', faucetUrl: FAUCET_TESTNET, faucetAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', faucetObject: '0xd5723eda84f691ae2623da79312c7909b1737c5b3866ecc5dbd6aa21718ff15d', BTCMemPool: 'https://mempool.space/testnet/tx/', twitterOracleAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', + inviterCA: ['0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', 'invitation', 'InvitationConf'], }, }, localnet: { @@ -48,13 +50,14 @@ const { networkConfig, useNetworkVariable, useNetworkVariables } = createNetwork variables: { roochOperatingAddress: ROOCH_NFT_OPERATING_ADDRESS, mintAddress: ROOCH_MINT_OPERATING_ADDRESS, - btcGasAddress: 'tb1prcajaj9n7e29u4dfp33x3hcf52yqeegspdpcd79pqu4fpr6llx4stqqxgy', - gasMarketAddress: '0x872502737008ac71c4c008bb3846a688bfd9fa54c6724089ea51b72f813dc71e', + btcGasAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', + gasMarketAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', faucetUrl: FAUCET_TESTNET, faucetAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', faucetObject: '0xd5723eda84f691ae2623da79312c7909b1737c5b3866ecc5dbd6aa21718ff15d', BTCMemPool: 'https://mempool.space/testnet/tx/', twitterOracleAddress: '0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', + inviterCA: ['0x701c21bf1c8cd5af8c42983890d8ca55e7a820171b8e744c13f2d9998bf76cc3', 'invitation', 'InvitationConf'], }, }, }); diff --git a/infra/rooch-portal-v2/src/layouts/config-nav-dashboard.tsx b/infra/rooch-portal-v2/src/layouts/config-nav-dashboard.tsx index 38758b2147..72a15de5da 100644 --- a/infra/rooch-portal-v2/src/layouts/config-nav-dashboard.tsx +++ b/infra/rooch-portal-v2/src/layouts/config-nav-dashboard.tsx @@ -38,6 +38,12 @@ export const navData = [ icon: , // noAddressRequired: true, }, + { + title: 'Invitation', + path: paths.dashboard.invitation, + icon: , + // noAddressRequired: true, + }, { title: 'Settings', path: paths.dashboard.settings, diff --git a/infra/rooch-portal-v2/src/middleware.ts b/infra/rooch-portal-v2/src/middleware.ts index c38cb320ce..f42d2811c6 100644 --- a/infra/rooch-portal-v2/src/middleware.ts +++ b/infra/rooch-portal-v2/src/middleware.ts @@ -16,6 +16,7 @@ const apiDomains = [ getRoochNodeUrl('testnet'), 'https://test-faucet.rooch.network', 'https://main-faucet.rooch.network', + 'http://127.0.0.1:6868', ]; const isProduction = process.env.NODE_ENV === 'production'; diff --git a/infra/rooch-portal-v2/src/routes/paths.ts b/infra/rooch-portal-v2/src/routes/paths.ts index a1ce4de81e..808a08a97c 100644 --- a/infra/rooch-portal-v2/src/routes/paths.ts +++ b/infra/rooch-portal-v2/src/routes/paths.ts @@ -14,6 +14,7 @@ export const paths = { settings: `${ROOTS.DASHBOARD}/settings`, search: `${ROOTS.DASHBOARD}/search`, faucet: `${ROOTS.DASHBOARD}/faucet`, + invitation: `${ROOTS.DASHBOARD}/invitation`, 'gas-swap': `${ROOTS.DASHBOARD}/gas-swap`, }, }; diff --git a/infra/rooch-portal-v2/src/sections/assets/components/asset-row-item.tsx b/infra/rooch-portal-v2/src/sections/assets/components/asset-row-item.tsx index e67727e3e4..b54af61f4e 100644 --- a/infra/rooch-portal-v2/src/sections/assets/components/asset-row-item.tsx +++ b/infra/rooch-portal-v2/src/sections/assets/components/asset-row-item.tsx @@ -24,7 +24,6 @@ export default function AssetRowItem({ row, isWalletOwner, onOpenTransferModal } - {/* {row.icon_url && {row.symbol}} */} {row.icon_url ? ( = { + 1: FAUCET_NOT_OPEN, + 2: INVALID_UTXO, + 3: FAUCET_NOT_ENOUGH_RGAS, + 4: ALREADY_CLAIMED, + 5: UTXO_VALUE_IS_ZERO, +}; + +export function InviterFaucetView({ inviterAddress }: { inviterAddress: string }) { + const router = useRouter(); + + const client = useRoochClient(); + const faucetAddress = useNetworkVariable('faucetAddress'); + const faucetObject = useNetworkVariable('faucetObject'); + const [inviterCA, inviterName] = useNetworkVariable('inviterCA') + const inviterConf = `${inviterCA}::${inviterName}::InvitationConf`; + const faucetUrl = useNetworkVariable('faucetUrl'); + const wallet = useCurrentWallet(); + + const viewAddress = useCurrentAddress(); + const [faucetStatus, setFaucetStatus] = useState(false); + const [errorMsg, setErrorMsg] = useState(); + const [claimGas, setClaimGas] = useState(0); + const [UTXOs, setUTXOs] = useState | null>(null); + + const { data: inviter } = useRoochClientQuery('queryObjectStates', { + filter: { + object_type: inviterConf, + }, + queryOption: { + decode: true, + }, + }); + + useEffect(() => { + + // invite close + if (inviter && inviter.data.length > 0 && inviter.data[0].decoded_value?.value.is_open === false) { + router.push(paths.dashboard.faucet); + } + + }, [inviter, router]) + + const { data, isPending, refetch } = useRoochClientQuery( + 'getBalance', + { + owner: viewAddress?.genRoochAddress().toStr()!, + coinType: '0x3::gas_coin::RGas', + }, + { refetchInterval: 5000 } + ); + + useEffect(() => { + if (!viewAddress) { + return; + } + setFaucetStatus(true); + client + .queryUTXO({ + filter: { + owner: viewAddress.toStr(), + }, + }) + .then(async (result) => { + const utxoIds = result.data.map((item) => item.id); + if (utxoIds) { + setUTXOs(utxoIds); + const result = await client.executeViewFunction({ + target: `${faucetAddress}::gas_faucet::check_claim`, + args: [ + Args.objectId(faucetObject), + Args.address(viewAddress.genRoochAddress()!), + Args.vec('objectId', utxoIds), + ], + }); + + if (result.vm_status === 'Executed') { + const gas = Number(formatCoin(Number(result.return_values![0].decoded_value), 8, 2)); + setClaimGas(gas); + } else if ('MoveAbort' in result.vm_status) { + setErrorMsg(ERROR_MSG[Number(result.vm_status.MoveAbort.abort_code)]); + } + } else { + setErrorMsg('Not found utxo'); + } + }) + .finally(() => { + setFaucetStatus(false); + }); + }, [client, faucetAddress, faucetObject, viewAddress]); + + const fetchFaucet = async () => { + if (errorMsg === ALREADY_CLAIMED) { + router.push(paths.dashboard['gas-swap']); + return; + } + + setFaucetStatus(true); + + if ( + inviterAddress && + inviter && + inviter.data.length > 0 && + inviter.data[0].decoded_value?.value.is_open === true + ) { + let sign: Bytes | undefined + const pk = wallet.wallet!.getPublicKey().toBytes() + const signMsg = 'Welcome to use Rooch! Hold BTC Claim your Rgas.' + try { + sign = await wallet.wallet?.sign(stringToBytes('utf8', signMsg)) + } catch (e) { + toast.error(e.message) + } + + if (!sign) { + return; + } + + try { + const payload = JSON.stringify({ + claimer: viewAddress!.toStr(), + inviter: inviterAddress, + claimer_sign: toHEX(sign), + public_key: toHEX(pk), + message: signMsg, + }); + const response = await fetch(`${faucetUrl}/faucet-inviter`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: payload, + }); + + if (!response.ok) { + const data = await response.json(); + console.log(data); + if (response.status === 500 && data.error.includes('UTXO value is zero')) { + const msg = 'Claim failed, Not found UTXO'; + setErrorMsg(msg); + toast.error(msg); + return; + } + + toast.error('Network response was not ok'); + return; + } + + const d = await response.json(); + window.localStorage.setItem(INVITER_ADDRESS_KEY, '') + await refetch(); + toast.success( + `Faucet Success! RGas: ${formatCoin(Number(d.gas || 0), data?.decimals || 0, 2)}` + ); + } catch (error) { + console.error('Error:', error); + toast.error(`faucet error: ${error}`); + } finally { + setFaucetStatus(false); + } + } + }; + + return ( + + + + + + + + ({viewAddress?.toStr()}) + + + + + ({viewAddress?.genRoochAddress().toStr()}) + + + + + + {formatCoin(Number(data?.balance || 0), data?.decimals || 0, 2)} + + + {errorMsg + ? errorMsg === ALREADY_CLAIMED + ? 'You Already Claimed RGAS' + : 'You cannot claim gas, Please make sure the current address has a valid utxo and try again' + : ''} + + {errorMsg === ALREADY_CLAIMED + ? 'Purchase RGas' + : errorMsg || `Claim: ${claimGas} RGas`} + + + + + + ); +} diff --git a/infra/rooch-portal-v2/src/sections/faucet/view.tsx b/infra/rooch-portal-v2/src/sections/faucet/view.tsx index 11ccd89ca9..81e9627b0b 100644 --- a/infra/rooch-portal-v2/src/sections/faucet/view.tsx +++ b/infra/rooch-portal-v2/src/sections/faucet/view.tsx @@ -20,6 +20,7 @@ import { DashboardContent } from 'src/layouts/dashboard'; import { toast } from 'src/components/snackbar'; import { paths } from '../../routes/paths' +import { INVITER_ADDRESS_KEY } from "../../utils/inviter"; const FAUCET_NOT_OPEN= 'Faucet Not Open' const INVALID_UTXO = 'Invalid UTXO' @@ -50,6 +51,10 @@ export function FaucetView({ address }: { address: string }) { useAddressChanged({ address, path: 'faucet' }); useEffect(() => { + const inviterAddress = window.localStorage.getItem(INVITER_ADDRESS_KEY) + if (inviterAddress && inviterAddress.length > 0) { + router.push(`/faucet/inviter/${inviterAddress}`) + } if (isValidBitcoinAddress(address)) { setViewAddress(address); try { diff --git a/infra/rooch-portal-v2/src/sections/invitations/components/invitation-list.tsx b/infra/rooch-portal-v2/src/sections/invitations/components/invitation-list.tsx new file mode 100644 index 0000000000..0d1683082f --- /dev/null +++ b/infra/rooch-portal-v2/src/sections/invitations/components/invitation-list.tsx @@ -0,0 +1,113 @@ +import dayjs from 'dayjs'; +import { useState, useEffect } from "react"; +import { useRoochClient } from "@roochnetwork/rooch-sdk-kit"; + +import { + Box, + Card, + Table, + Tooltip, + TableRow, + TableBody, + TableCell, + Typography, +} from '@mui/material'; + +import { shortAddress } from '../../../utils/address'; +import { Scrollbar } from '../../../components/scrollbar'; +import { getUTCOffset } from '../../../utils/format-time'; +import { formatCoin } from '../../../utils/format-number'; +import { ROOCH_GAS_COIN_DECIMALS } from '../../../config/constant'; +import TableSkeleton from '../../../components/skeleton/table-skeleton'; +import { TableNoData, TableHeadCustom } from '../../../components/table'; + +type ListType = { + address: string + reward: number + timestamp: number +} + +export function InvitationList({ table }: { table?: string }) { + const client = useRoochClient() + const [loading, setLoading] = useState(false) + const [data, setData] = useState>() + + useEffect(() => { + if (!table) { + return + } + + setLoading(true) + client.listStates({ + accessPath: `/table/${table}`, + stateOption: { + decode: true + } + }).then((result) => { + setData(result.data.map((item) => { + const view = ((item.state.decoded_value!.value) as any).value.value + return { + address: view.address, + reward: view.reward_amount, + timestamp: view.timestamp, + } + })) + }).catch((e) => { + console.log(e) + }).finally(() => setLoading(false)) + }, [table, client]); + + return ( + + + Activity History + + + + + Timestamp ({getUTCOffset()}) + + ), + }, + { id: 'coin', label: 'RGAS' }, + ]} + /> + + {loading ? ( + + ) : ( + <> + {data?.map((item) => ( + + + + + {shortAddress(item.address, 8, 6)} + + + + + {dayjs(Number(item.timestamp * 1000)).format('MMMM DD, YYYY HH:mm:ss')} + + {item.reward && ( + + {formatCoin(Number(item.reward), ROOCH_GAS_COIN_DECIMALS, 6)} + + )} + + ))} + + + )} + +
+
+
+ ); +} diff --git a/infra/rooch-portal-v2/src/sections/invitations/components/lottery-list.tsx b/infra/rooch-portal-v2/src/sections/invitations/components/lottery-list.tsx new file mode 100644 index 0000000000..083b0b80ef --- /dev/null +++ b/infra/rooch-portal-v2/src/sections/invitations/components/lottery-list.tsx @@ -0,0 +1,176 @@ +import dayjs from 'dayjs'; +import { useState, useEffect, useCallback } from "react"; +import { Args, Transaction } from '@roochnetwork/rooch-sdk'; +import { useRoochClient, useCurrentSession } from '@roochnetwork/rooch-sdk-kit'; + +import { LoadingButton } from '@mui/lab'; +import { + Box, + Card, + Table, + Button, + TableRow, + TableBody, + TableCell, + Typography, +} from '@mui/material'; + +import { Scrollbar } from '../../../components/scrollbar'; +import { getUTCOffset } from '../../../utils/format-time'; +import { formatCoin } from '../../../utils/format-number'; +import { useNetworkVariable } from '../../../hooks/use-networks'; +import { ROOCH_GAS_COIN_DECIMALS } from '../../../config/constant'; +import TableSkeleton from '../../../components/skeleton/table-skeleton'; +import { TableNoData, TableHeadCustom } from '../../../components/table'; +import SessionKeyGuardButtonV1 from "../../../components/auth/session-key-guard-button-v1"; + +const options = [1, 5, 10, 0]; + +type ListType = { + reward: number; + timestamp: number; +}; + +export function InvitationLotteryList({ table, ticket = 0, openCallback }: { table?: string, ticket: number, openCallback: () => void }) { + const client = useRoochClient(); + const [data, setData] = useState>(); + const [loading, setLoading] = useState(false); + const [opening, setOpening] = useState(false); + const [ticketOption, setTicketOption] = useState(1); + const session = useCurrentSession(); + const [inviterCA, inviterModule, inviterObj] = useNetworkVariable('inviterCA'); + + const fetch = useCallback(() => { + setLoading(true); + client + .listStates({ + accessPath: `/table/${table}`, + stateOption: { + decode: true, + }, + }) + .then((result) => { + setData( + result.data.map((item) => { + console.log(result) + const view = (item.state.decoded_value!.value as any).value.value; + return { + address: view.address, + reward: view.reward_amount, + timestamp: view.timestamp, + }; + }) + ); + }) + .catch((e) => { + console.log(e); + }) + .finally(() => setLoading(false)); + }, [client, table]) + + useEffect(() => { + if (!table) { + return; + } + + fetch() + + }, [fetch, table]); + + const openTicket = async () => { + setOpening(true) + const tx = new Transaction(); + tx.callFunction({ + target: `${inviterCA}::${inviterModule}::lottery`, + args: [Args.object({ + address: inviterCA, + module: inviterModule, + name: inviterObj, + }), Args.u64(BigInt(ticketOption === 0 ? ticket : ticketOption))], + }); + + const result = await client.signAndExecuteTransaction({ + transaction: tx, + signer: session!, + }); + + if (result.execution_info.status.type === 'executed') { + openCallback() + fetch() + } + setOpening(false) + console.log(result); + }; + + return ( + + + Activity History + {ticket === 0 || ( + + {options.map((item) => ( + + ))} + + + )} + + + + + Timestamp ({getUTCOffset()}) + + ), + }, + { id: 'coin', label: 'RGAS' }, + ]} + /> + + {loading ? ( + + ) : ( + <> + {data?.map((item) => ( + + + {dayjs(Number(item.timestamp * 1000)).format('MMMM DD, YYYY HH:mm:ss')} + + {item.reward && ( + + {formatCoin(Number(item.reward), ROOCH_GAS_COIN_DECIMALS, 6)} + + )} + + ))} + + + )} + +
+
+
+ ); +} diff --git a/infra/rooch-portal-v2/src/sections/invitations/index.tsx b/infra/rooch-portal-v2/src/sections/invitations/index.tsx new file mode 100644 index 0000000000..33775c3b8e --- /dev/null +++ b/infra/rooch-portal-v2/src/sections/invitations/index.tsx @@ -0,0 +1,137 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Args } from '@roochnetwork/rooch-sdk'; +import { + useCurrentAddress, + useRoochClientQuery, +} from '@roochnetwork/rooch-sdk-kit'; + +import Typography from '@mui/material/Typography'; +import { Tab, Box, Tabs, Card, Stack, CardHeader, CardContent } from '@mui/material'; + +import { useTabs } from '../../hooks/use-tabs'; +import { fromDustToPrecision } from '../../utils/number'; +import { AnimateCountUp } from '../../components/animate'; +import { DashboardContent } from '../../layouts/dashboard'; +import { useNetworkVariable } from '../../hooks/use-networks'; +import { InvitationList } from './components/invitation-list'; +import { InvitationLotteryList } from './components/lottery-list'; + +const TABS = [ + { label: 'Lottery Tickets', value: 'lottery_tickets' }, + { label: 'Invitation List', value: 'invitation_records' }, +]; + +type inviterDataType = { + invitationCount: number; + invitationReward: number; + lotteryTable: string; + invitationTable: string; + lotteryReward: number; + remainingLotteryTicket: number; +}; + +export function InvitationsView() { + const [inviterCA, inviterModule, inviterObj] = useNetworkVariable('inviterCA'); + const currentAddress = useCurrentAddress(); + const tabs = useTabs('lottery_tickets'); + const [inviterData, setInviterData] = useState(); + + const { data, refetch } = useRoochClientQuery('executeViewFunction', { + target: `${inviterCA}::${inviterModule}::invitation_user_record`, + args: [ + Args.object({ + address: inviterCA, + module: inviterModule, + name: inviterObj, + }), + Args.address(currentAddress?.genRoochAddress().toHexAddress() || ''), + ], + }); + + useEffect(() => { + if (!data || data.vm_status !== 'Executed') { + return; + } + + const dataView = (data.return_values![0].decoded_value as any).value; + + setInviterData({ + invitationCount: Number(dataView.total_invitations), + invitationReward: Number(dataView.invitation_reward_amount), + invitationTable: dataView.invitation_records.value.contents.value.handle.value.id as string, + lotteryTable: dataView.lottery_records.value.contents.value.handle.value.id as string, + remainingLotteryTicket: Number(dataView.remaining_luckey_ticket), + lotteryReward: Number(dataView.lottery_reward_amount), + }); + }, [data]); + + const renderTabs = ( + + {TABS.map((tab) => ( + + ))} + + ); + + return ( + + + Invitation Overview + + + + + + + {inviterData && ( + + )} + + + + + + {inviterData && ( + + )} + + + + + + {inviterData && ( + + )} + + + + + {renderTabs} + + {tabs.value === 'lottery_tickets' && ( + refetch()} + /> + )} + + {tabs.value === 'invitation_records' && ( + + )} + + ); +} diff --git a/infra/rooch-portal-v2/src/sections/inviter/index.tsx b/infra/rooch-portal-v2/src/sections/inviter/index.tsx new file mode 100644 index 0000000000..3c389e966e --- /dev/null +++ b/infra/rooch-portal-v2/src/sections/inviter/index.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { useEffect } from 'react'; + +import { useRouter } from '../../routes/hooks'; +import { INVITER_ADDRESS_KEY } from '../../utils/inviter'; + +export function InviterView({ inviterAddress }: { inviterAddress?: string }) { + const router = useRouter(); + + useEffect(() => { + if (inviterAddress) { + window.localStorage.setItem(INVITER_ADDRESS_KEY, inviterAddress); + router.push(`/settings`); + } + }, [inviterAddress, router]); + + return <>; +} diff --git a/infra/rooch-portal-v2/src/sections/settings/view.tsx b/infra/rooch-portal-v2/src/sections/settings/view.tsx index 3116ee6027..00e5414da0 100644 --- a/infra/rooch-portal-v2/src/sections/settings/view.tsx +++ b/infra/rooch-portal-v2/src/sections/settings/view.tsx @@ -3,13 +3,14 @@ import axios from 'axios' import { useState, useEffect, useCallback } from 'react' import { CopyToClipboard } from 'react-copy-to-clipboard' -import { Args, Transaction, stringToBytes } from '@roochnetwork/rooch-sdk' +import { Args, Transaction, stringToBytes, toHEX } from '@roochnetwork/rooch-sdk' import { useRoochClient, useCurrentAddress, useCurrentNetwork, useCurrentSession, - useRoochClientQuery + useRoochClientQuery, + useCurrentWallet } from '@roochnetwork/rooch-sdk-kit' import { LoadingButton } from '@mui/lab' @@ -25,6 +26,7 @@ import { Iconify } from 'src/components/iconify' import { useNetworkVariable } from '../../hooks/use-networks' import SessionKeysTableCard from './components/session-keys-table-card' import SessionKeyGuardButtonV1 from '../../components/auth/session-key-guard-button-v1' +import { INVITER_ADDRESS_KEY } from "../../utils/inviter"; export function SettingsView() { const address = useCurrentAddress() @@ -34,6 +36,8 @@ export function SettingsView() { const network = useCurrentNetwork() const faucetUrl = useNetworkVariable('faucetUrl') const twitterOracleAddress = useNetworkVariable('twitterOracleAddress') + const [inviterCA, inviterModule, inviterConf] = useNetworkVariable('inviterCA'); + const wallet = useCurrentWallet() const [tweetStatus, setTweetStatus] = useState('') const [twitterId, setTwitterId] = useState() const [verifying, setVerifying] = useState(false) @@ -104,6 +108,116 @@ export function SettingsView() { } } + const bindTwitter = async (pureTweetId: string) => { + await axios.post( + `${faucetUrl}/verify-and-binding-twitter-account`, + { + tweet_id: pureTweetId, + }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + } + + const bindWithInviter = async (inviterAddr: string, pureTweetId: string) => { + const signMsg = 'Welcome to use Rooch! Connect with Twitter and claim your Rgas.' + const sign = await wallet.wallet?.sign(stringToBytes('utf8', signMsg)) + const pk = wallet.wallet!.getPublicKey().toBytes() + + const payload = JSON.stringify({ + inviter: inviterAddr, + tweet_id: pureTweetId, + claimer_sign: toHEX(sign!), + public_key: toHEX(pk), + message: signMsg, + }); + await axios.post( + `${faucetUrl}/binding-twitter-with-inviter`, + payload, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + + window.localStorage.setItem(INVITER_ADDRESS_KEY, '') + } + + const handleBindTwitter = async () => { + + // setp 1, check twitter + const match = tweetStatus.match(/status\/(\d+)/) + + if (!match) { + toast.error('twitter invald') + return + } + setVerifying(true) + const pureTweetId = match[1] + + try { + const pureTweetId = match[1] + const res = await axios.post( + `${faucetUrl}/fetch-tweet`, + { + tweet_id: pureTweetId, + }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + + if (!res.data.ok) { + toast.error('fetch twitter failed') + return + } + + // step 2, check inviter + const inviterAddr = window.localStorage.getItem(INVITER_ADDRESS_KEY) + if (inviterAddr && inviterAddr !== '') { + // check invite is open + const result = await client.queryObjectStates({ + filter: { + object_type: `${inviterCA}::${inviterModule}::${inviterConf}`, + }, + queryOption: { + decode: true, + }, + }); + + if (result && result.data.length > 0 && result.data[0].decoded_value?.value.is_open === true) { + await bindWithInviter(inviterAddr, pureTweetId) + } else { + await bindTwitter(pureTweetId) + } + + await sleep(3000) + const checkRes = await fetchTwitterId() + if (checkRes) { + toast.success('Binding success') + } + } + } catch(error) { + if ('response' in error) { + if ('error' in error.response.data) { + toast.error(error.response.data.error) + } else { + toast.error(error.response.data) + } + } else { + toast.error(error.message) + } + } finally { + setVerifying(false) + } + } + const networkText = network === 'mainnet' ? 'Pre-mainnet' : 'Testnet' const XText = `BTC:${address?.toStr()} @@ -216,50 +330,7 @@ https://${network === 'mainnet' ? '':'test-'}portal.rooch.network/inviter/${addr loading={verifying} className="mt-2 w-fit" variant="contained" - onClick={async () => { - try { - setVerifying(true) - const match = tweetStatus.match(/status\/(\d+)/) - if (match) { - const pureTweetId = match[1] - const res = await axios.post( - `${faucetUrl}/fetch-tweet`, - { - tweet_id: pureTweetId, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - console.log('🚀 ~ file: view.tsx:190 ~ onClick={ ~ res:', res) - if (res?.data?.ok) { - await axios.post( - `${faucetUrl}/verify-and-binding-twitter-account`, - { - tweet_id: pureTweetId, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - } - await sleep(3000) - const checkRes = await fetchTwitterId() - if (checkRes) { - toast.success('Binding success') - } - } - } catch (error) { - console.log('🚀 ~ file: view.tsx:211 ~ onClick={ ~ error:', error) - toast.error(error.response.data.error) - } finally { - setVerifying(false) - } - }} + onClick={handleBindTwitter} > Verify and bind Twitter account diff --git a/infra/rooch-portal-v2/src/utils/inviter.ts b/infra/rooch-portal-v2/src/utils/inviter.ts new file mode 100644 index 0000000000..eb78cec841 --- /dev/null +++ b/infra/rooch-portal-v2/src/utils/inviter.ts @@ -0,0 +1,2 @@ +export const INVITER_ADDRESS_KEY = 'inviter-address' + diff --git a/sdk/typescript/rooch-sdk/test-e2e/case/session.test.ts b/sdk/typescript/rooch-sdk/test-e2e/case/session.test.ts index a9ad6cd71d..b4bf31b1e8 100644 --- a/sdk/typescript/rooch-sdk/test-e2e/case/session.test.ts +++ b/sdk/typescript/rooch-sdk/test-e2e/case/session.test.ts @@ -4,6 +4,7 @@ import { beforeAll, describe, expect, it, afterAll } from 'vitest' import { TestBox } from '../setup.js' import { Transaction } from '../../src/transactions/index.js' +import { BitcoinAddress } from "../../src"; describe('Checkpoints Session API', () => { let testBox: TestBox @@ -17,6 +18,9 @@ describe('Checkpoints Session API', () => { }) it('Create session should be success', async () => { + + const s = new BitcoinAddress('bc1q04uaa0mveqtt4y0sltuxtauhlyl8ctstr5x3hu').genRoochAddress().toHexAddress() + console.log(s) const session = await testBox.getClient().createSession({ sessionArgs: { appName: 'sdk-e2e-test',