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.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',