Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement merkle trees for claiming #4932

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .ebstalk.apps.env/scoutgameadmin.env
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ REACT_APP_DECENT_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/decent:SecretStri
ALCHEMY_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/alchemy:SecretString:alchemy_api_key}}"
AIRSTACK_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/farcaster:SecretString:airstack_api_key}}"

REACT_APP_BUILDER_NFT_CONTRACT_ADDRESS="{{pull:secretsmanager:/io.cv.app/prd/buildernft:SecretString:builder_smart_contract_address}}"
REACT_APP_BUILDER_NFT_CONTRACT_ADDRESS="{{pull:secretsmanager:/io.cv.app/prd/buildernft:SecretString:builder_smart_contract_address}}"

REACT_APP_SCOUTPROTOCOL_CONTRACT_ADDRESS="{{pull:secretsmanager:/io.cv.app/prd/buildernft:SecretString:scoutprotocol_contract_address}}"
SCOUTPROTOCOL_CLAIMS_MANAGER_KEY="{{pull:secretsmanager:/io.cv.app/prd/buildernft:SecretString:scoutprotocol_claims_manager_key}}"
8 changes: 5 additions & 3 deletions apps/scoutgameadmin/app/(general)/contract/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ContractDashboard } from 'components/contract/ContractDashboard';
import { ContractHome } from 'components/contract/ContractsHome';
import { aggregateProtocolData } from 'lib/contract/aggregateProtocolData';
import { getContractData } from 'lib/contract/getContractData';

export const dynamic = 'force-dynamic';

export default async function Dashboard() {
const data = await getContractData();
return <ContractDashboard {...data} />;
const seasonOneData = await getContractData();
const protocolData = await aggregateProtocolData();
return <ContractHome seasonOne={seasonOneData} protocol={protocolData} />;
}
31 changes: 31 additions & 0 deletions apps/scoutgameadmin/components/contract/ContractsHome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';

import { Tabs, Tab, Box } from '@mui/material';
import { useState } from 'react';

import type { ProtocolData } from 'lib/contract/aggregateProtocolData';
import type { BuilderNFTContractData } from 'lib/contract/getContractData';

import { ProtocolContractDashboard } from './ProtocolContractDashboard';
import { SeasonOneDashboard } from './SeasonOneDashboard';

export function ContractHome({ seasonOne, protocol }: { seasonOne: BuilderNFTContractData; protocol: ProtocolData }) {
const [selectedTab, setSelectedTab] = useState(0);

const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setSelectedTab(newValue);
};

return (
<Box px={6}>
<Tabs value={selectedTab} onChange={handleChange}>
<Tab label='Season One' />
<Tab label='Protocol (Testnet)' />
</Tabs>
<Box mt={2}>
{selectedTab === 0 && <SeasonOneDashboard {...seasonOne} />}
{selectedTab === 1 && <ProtocolContractDashboard {...protocol} />}
</Box>
</Box>
);
}
117 changes: 117 additions & 0 deletions apps/scoutgameadmin/components/contract/ProtocolContractDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Box, Button, Divider, Grid2, IconButton, Typography } from '@mui/material';
import Link from 'next/link';
import { MdLaunch } from 'react-icons/md';

import type { ProtocolData } from 'lib/contract/aggregateProtocolData';
import type { BuilderNFTContractData } from 'lib/contract/getContractData';

function ContractLink({
address,
linkType = 'address',
title,
subtitle
}: {
address: string;
linkType?: 'address' | 'token' | 'contract';
title: string;
subtitle?: string;
}) {
return (
<Box gap={1} display='flex' flexDirection='column'>
<Typography variant='h6'>{title}</Typography>
<Box sx={{ minHeight: '40px' }}>{subtitle && <Typography variant='body2'>{subtitle}</Typography>}</Box>
<Link href={`https://optimism.blockscout.com/${linkType}/${address}`} target='_blank'>
{address}
<IconButton size='small' color='primary'>
<MdLaunch size='16px' />
</IconButton>
</Link>
</Box>
);
}

function SectionTitle({ title }: { title: string }) {
return (
<Typography variant='h5' fontWeight='bold'>
{title}
</Typography>
);
}

function GridDivider() {
return (
<Grid2 size={12}>
<Divider />
</Grid2>
);
}

export function ProtocolContractDashboard(data: ProtocolData) {
const itemSizeTwoColumnMd = { xs: 12, md: 6 };
const itemSizeThreeColumnMd = { xs: 12, md: 4 };

return (
<Grid2 container spacing={2}>
<Grid2 size={12}>
<SectionTitle title='Protocol Contract Addresses' />
</Grid2>
<Grid2 size={itemSizeTwoColumnMd}>
<ContractLink
address={data.proxy}
title='Proxy address'
linkType='token'
subtitle='Long term contract for interacting with the protocol'
/>
</Grid2>
<Grid2 size={itemSizeTwoColumnMd}>
<ContractLink
address={data.implementation}
title='Current Implementation'
subtitle='This contract is called by the proxy and contains the main protocol logic'
/>
</Grid2>
<GridDivider />
<Grid2 size={12}>
<SectionTitle title='Data' />
</Grid2>
{data.merkleRoots.map((root) => (
<Grid2 size={itemSizeThreeColumnMd} key={root.week}>
<Typography variant='h6'>Merkle Root for week {root.week}</Typography>
{!root.root && <Typography>Week not processed</Typography>}
{root.root &&
(root.publishedOnchain ? (
<Typography>Published onchain</Typography>
) : (
<Typography>Awaiting publication</Typography>
))}
</Grid2>
))}
<GridDivider />
<Grid2 size={12}>
<SectionTitle title='Governance' />
</Grid2>
<Grid2 size={itemSizeTwoColumnMd}>
<Typography variant='h6'>Upgrade contract</Typography>
<Button>Upgrade contract</Button>
</Grid2>
<GridDivider />
<Grid2 size={12}>
<SectionTitle title='Roles & Permissions' />
</Grid2>
<Grid2 size={itemSizeTwoColumnMd}>
<ContractLink
address={data.admin}
title='Admin'
subtitle='Admin wallet can upgrade the contract, update the wallet that receives proceeds from NFT sales, modify pricing, register builders and mint tokens.'
/>
</Grid2>
<Grid2 size={itemSizeTwoColumnMd}>
<ContractLink
address={data.claimsManager}
title='Claims Manager'
subtitle='The wallet that can register weekly merkle roots'
/>
</Grid2>
</Grid2>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ function GridDivider() {
);
}

export function ContractDashboard(data: BuilderNFTContractData) {
export function SeasonOneDashboard(data: BuilderNFTContractData) {
const itemSizeTwoColumnMd = { xs: 12, md: 6 };
const itemSizeThreeColumnMd = { xs: 12, md: 4 };

return (
<Grid2 container spacing={2} px={6}>
<Grid2 container spacing={2}>
<Grid2 size={12}>
<SectionTitle title='Contract Addresses' />
</Grid2>
Expand Down
131 changes: 131 additions & 0 deletions apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// useProposeSetImplementation.ts
import type {} from '@safe-global/api-kit';
import { ProposeTransactionProps } from '@safe-global/api-kit';
import { ethers } from 'ethers';
import { useState, useCallback } from 'react';
import { encodeFunctionData } from 'viem';
import { use } from 'wagmi';

/**
* Hook to propose a transaction to call setImplementation on a contract using Gnosis Safe SDK.
*
* @param safeAddress - The address of the Gnosis Safe.
* @param contractAddress - The address of the contract with the setImplementation method.
* @returns An object containing the proposeTransaction function, loading state, and any error.
*/
const useProposeSetImplementation = (safeAddress: string, contractAddress: string) => {
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const { data: signer } = useSigner();
const { chain } = useNetwork();

const proposeTransaction = useCallback(
async (newImplementationAddress: string) => {
setLoading(true);
setError(null);

try {
if (!signer) {
throw new Error('No signer available');
}

if (!chain) {
throw new Error('No network information available');
}

// Initialize ethers provider and signer
const provider = new ethers.providers.Web3Provider((window as any).ethereum);
const signerAddress = await signer.getAddress();

// Initialize Safe API Kit
const apiKit = new SafeApiKit({
txServiceUrl: getTxServiceUrl(chain.id),

Check failure on line 42 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

'getTxServiceUrl' was used before it was defined

Check failure on line 42 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

'getTxServiceUrl' was used before it was defined

Check failure on line 42 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

'getTxServiceUrl' was used before it was defined

Check failure on line 42 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test app

'getTxServiceUrl' was used before it was defined

Check failure on line 42 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test

'getTxServiceUrl' was used before it was defined

Check failure on line 42 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

'getTxServiceUrl' was used before it was defined

Check failure on line 42 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Validate code

'getTxServiceUrl' was used before it was defined
ethAdapter: new ethers.providers.Web3Provider((window as any).ethereum),
safeAddress
});

// Encode the setImplementation call using viem
const data = encodeFunctionData({
abi: [
{
name: 'setImplementation',
type: 'function',
inputs: [
{
type: 'address',
name: 'newImplementation'
}
]
}
],
functionName: 'setImplementation',
args: [newImplementationAddress]
});

// Prepare transaction data
const safeTx: SafeTransactionData = {
to: contractAddress,
value: '0',
data,
operation: 0 // 0 for CALL, 1 for DELEGATECALL
};

// Create the Safe transaction
const safeTransaction = await apiKit.createTransaction({
safeTransactionData: safeTx
});

// Get the transaction hash for signing
const txHash = await apiKit.getTransactionHash(safeTransaction);

// Sign the transaction hash using the signer
const signature = await signer.signMessage(ethers.utils.arrayify(txHash));

// Add the signature to the transaction
safeTransaction.signatures[safeAddress] = {
signer: signerAddress,
data: signature
};

// Propose the transaction via Safe API Kit
const response = await apiKit.proposeTransaction(safeTransaction);

if (!response.success) {
throw new Error(response.message || 'Failed to propose transaction');
}

console.log('Transaction proposed successfully:', response);

Check failure on line 97 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

Unexpected console statement

Check failure on line 97 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

Unexpected console statement

Check failure on line 97 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

Unexpected console statement

Check failure on line 97 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test app

Unexpected console statement

Check failure on line 97 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test

Unexpected console statement

Check failure on line 97 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

Unexpected console statement

Check failure on line 97 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Validate code

Unexpected console statement
} catch (err: any) {
console.error('Error proposing transaction:', err);

Check failure on line 99 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

Unexpected console statement

Check failure on line 99 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

Unexpected console statement

Check failure on line 99 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

Unexpected console statement

Check failure on line 99 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test app

Unexpected console statement

Check failure on line 99 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test

Unexpected console statement

Check failure on line 99 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Test apps

Unexpected console statement

Check failure on line 99 in apps/scoutgameadmin/hooks/useProposeContractUpgrade.tsx

View workflow job for this annotation

GitHub Actions / Validate code

Unexpected console statement
setError(err.message || 'An unknown error occurred');
} finally {
setLoading(false);
}
},
[signer, chain, safeAddress, contractAddress]
);

return { proposeTransaction, loading, error };
};

/**
* Helper function to get the Safe Transaction Service URL based on chain ID.
*
* @param chainId - The ID of the Ethereum chain.
* @returns The URL of the Safe Transaction Service.
*/
const getTxServiceUrl = (chainId: number): string => {
switch (chainId) {
case 1:
return 'https://safe-transaction.gnosis.io/';
case 5:
return 'https://safe-transaction-goerli.safe.global/';
case 137:
return 'https://safe-transaction-mainnet.safe.global/';
// Add other chains as needed
default:
throw new Error(`Unsupported chain ID: ${chainId}`);
}
};

export default useProposeSetImplementation;
63 changes: 63 additions & 0 deletions apps/scoutgameadmin/lib/contract/aggregateProtocolData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { prisma } from '@charmverse/core/prisma-client';
import { getAllISOWeeksFromSeasonStart } from '@packages/scoutgame/dates';
import {
protocolImplementationReadonlyApiClient,
protocolProxyReadonlyApiClient
} from '@packages/scoutgame/protocol/clients/protocolReadClients';
import { getScoutProtocolAddress } from '@packages/scoutgame/protocol/constants';
import type { Address } from 'viem';

type MerkleRoot = {
week: string;
publishedOnchain: boolean;
root: string | null;
};

export type ProtocolData = {
admin: Address;
proxy: Address;
implementation: Address;
claimsManager: Address;
merkleRoots: MerkleRoot[];
};

export async function aggregateProtocolData(): Promise<ProtocolData> {
const [implementation, admin, claimsManager] = await Promise.all([
protocolProxyReadonlyApiClient.implementation(),
protocolProxyReadonlyApiClient.admin(),
protocolProxyReadonlyApiClient.claimsManager()
]);

const weeks = getAllISOWeeksFromSeasonStart();

const weeklyClaims = await prisma.weeklyClaims.findMany({
where: {
week: {
in: weeks
}
}
});

const merkleRoots = await Promise.all<MerkleRoot>(
weeks.map((week) =>
protocolImplementationReadonlyApiClient
.getMerkleRoot({ args: { week } })
.then((root) => ({ week, root, publishedOnchain: true }) as MerkleRoot)
.catch(() => {
return {
week,
root: weeklyClaims.find((claim) => claim.week === week)?.merkleTreeRoot || null,
publishedOnchain: false
} as MerkleRoot;
})
)
);

return {
merkleRoots,
admin: admin as Address,
proxy: getScoutProtocolAddress(),
implementation: implementation as Address,
claimsManager: claimsManager as Address
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DateTime } from 'luxon';
import { BuilderInfo } from './generateSeedData';
import { randomTimeOfDay } from './generator';

export async function generateNftPurchaseEvents(scoutId: string, assignedBuilders: BuilderInfo[], date: DateTime) {
export async function generateNftPurchaseEvents(scoutId: string, assignedBuilders: Pick<BuilderInfo, 'builderNftId' | 'nftPrice'>[], date: DateTime) {
const week = getWeekFromDate(date.toJSDate());
let totalNftsPurchasedToday = 0;
for (let nftCount = 0; nftCount < faker.number.int({ min: 0, max: 3 }); nftCount++) {
Expand Down
Loading
Loading