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

integration: friend tech leaderboard prelim implementation #437

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
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
11 changes: 9 additions & 2 deletions src/components/current/MessageTranslator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Fragment, useEffect, useMemo, useState } from 'react';
import { Message } from '@/contexts/ChatContext';
import { SharedStateContextProvider } from '@/contexts/SharedStateContext';
import { parseMessage } from '@/utils/parse-message';
import Avatar from '../shared/Avatar';
import { Widgetize } from '../legacy/legacyComponents/MessageTranslator';
import { ErrorResponse, TextResponse } from '../cactiComponents';
import { ImageVariant } from '../cactiComponents/ImageResponse';
import { TableResponse } from '../cactiComponents/TableResponse';
import { Widgetize } from '../legacy/legacyComponents/MessageTranslator';
import Avatar from '../shared/Avatar';
import { FeedbackButton } from './FeedbackButton';
import { MessageWrap } from './MessageWrap';
import ListContainer from './containers/ListContainer';
Expand All @@ -18,6 +18,8 @@ import WithdrawVault from './widgets/4626vault/WithdrawFromVault';
import DepositDSR from './widgets/dsr/DepositDSR';
import RedeemDSR from './widgets/dsr/RedeemDSR';
import StakeSfrxEth from './widgets/frax/StakeSfrxETH';
import FriendTechBuyKeys from './widgets/friend-tech/FriendTechBuyKeys';
import FriendTechLeaderboard from './widgets/friend-tech/FriendTechLeaderboard';
import LidoDeposit from './widgets/lido/LidoDeposit';
import LidoWithdraw from './widgets/lido/LidoWithdraw';
import LiquityBorrow from './widgets/liquity/borrow/LiquityBorrow';
Expand Down Expand Up @@ -310,6 +312,11 @@ export const Widget = (props: WidgetProps) => {
<WithdrawVault withdrawToken={parsedArgs[0]} amount={parsedArgs[1]} vault={parsedArgs[2]} />
);
widgets.set('wrap-eth', <WrapEth amtString={'1'} />);
widgets.set('friend-tech-leaderboard', <FriendTechLeaderboard />);
widgets.set(
'friend-tech-buy-keys',
<FriendTechBuyKeys username={parsedArgs[0]} amount={parsedArgs[1]} />
);

/* If available, return the widget in the widgets map */
if (widgets.has(fnName)) {
Expand Down
71 changes: 71 additions & 0 deletions src/components/current/widgets/friend-tech/FriendTechBuyKeys.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { BigNumber } from 'ethers';
import { useContractRead, usePrepareContractWrite } from 'wagmi';
import { ActionResponse, HeaderResponse, SingleLineResponse } from '@/components/cactiComponents';
import { ResponseGrid, ResponseTitle } from '@/components/cactiComponents/helpers/layout';
import abi from './abi';
import useLeaderboardData from './useLeaderboardData';

interface FriendTechBuyKeysProps {
username: string; // username of the user to buy keys for
amount: string; // number of keys to buy
}

// TODO update wagmi to be able to use the chain id from wagmi chains
const BASE_CHAIN_ID = 8453;

const CONTRACT_ADDRESS = '0xCF205808Ed36593aa40a44F10c7f7C2F67d4A4d4';

const FriendTechBuyKeys = ({ username, amount }: FriendTechBuyKeysProps) => {
const { data } = useLeaderboardData();
const parsedAmount = BigNumber.from(amount);
// get the specific user data from leaderboard
const user = data?.find((user) => user.username === username.replace('@', ''));

// get the keys buy price
const { data: buyPrice } = useContractRead({
address: CONTRACT_ADDRESS,
chainId: BASE_CHAIN_ID,
abi,
functionName: 'getBuyPriceAfterFee',
args: [user?.address!, parsedAmount],
});

const { config, isError } = usePrepareContractWrite({
address: CONTRACT_ADDRESS,
abi,
functionName: 'buyShares',
args: [user?.address!, parsedAmount],
overrides: {
value: buyPrice,
},
});

return (
<>
<HeaderResponse text="Buy Keys on Friend.Tech" />
<ResponseGrid className="grid gap-1">
<SingleLineResponse className="flex justify-between">
<ResponseTitle>{user?.username}</ResponseTitle>
<div className="flex">
<ResponseTitle>Amount to Buy:</ResponseTitle>
<ResponseTitle>{amount}</ResponseTitle>
</div>
</SingleLineResponse>

<ActionResponse
sendParams={config.request}
txParams={undefined}
approvalParams={undefined}
label={
isError
? `Erroring preparing: make sure you have specified the username and amount`
: `Buy ${amount} of ${username} keys`
}
disabled={isError}
/>
</ResponseGrid>
</>
);
};

export default FriendTechBuyKeys;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { HeaderResponse, SingleLineResponse } from '@/components/cactiComponents';
import { ResponseGrid, ResponseTitle } from '@/components/cactiComponents/helpers/layout';
import useLeaderboardData, { LeaderboardUserItem } from './useLeaderboardData';

const FriendTechLeaderboard = () => {
const { data } = useLeaderboardData();
// get the first 10 users from the leaderboard
const sliced = data?.slice(0, 10);

return (
<>
<HeaderResponse text="Friend.Tech Leaderboard" />
<ResponseGrid className="grid gap-1">
{sliced?.map((user) => (
<LeaderboardItem key={user.address} user={user} />
))}
</ResponseGrid>
</>
);
};

const LeaderboardItem = ({ user }: { user: LeaderboardUserItem }) => {
return (
<SingleLineResponse className="flex justify-between">
<ResponseTitle>@{user.username}</ResponseTitle>
<ResponseTitle>{user.price} ETH</ResponseTitle>
</SingleLineResponse>
);
};

export default FriendTechLeaderboard;
176 changes: 176 additions & 0 deletions src/components/current/widgets/friend-tech/abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
export default [
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
],
name: 'OwnershipTransferred',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: false, internalType: 'address', name: 'trader', type: 'address' },
{ indexed: false, internalType: 'address', name: 'subject', type: 'address' },
{ indexed: false, internalType: 'bool', name: 'isBuy', type: 'bool' },
{ indexed: false, internalType: 'uint256', name: 'shareAmount', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'ethAmount', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'protocolEthAmount', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'subjectEthAmount', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'supply', type: 'uint256' },
],
name: 'Trade',
type: 'event',
},
{
inputs: [
{ internalType: 'address', name: 'sharesSubject', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'buyShares',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'sharesSubject', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'getBuyPrice',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'sharesSubject', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'getBuyPriceAfterFee',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'uint256', name: 'supply', type: 'uint256' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'getPrice',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'pure',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'sharesSubject', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'getSellPrice',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'sharesSubject', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'getSellPriceAfterFee',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'owner',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'protocolFeeDestination',
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'protocolFeePercent',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'renounceOwnership',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'sharesSubject', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'sellShares',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: '_feeDestination', type: 'address' }],
name: 'setFeeDestination',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'uint256', name: '_feePercent', type: 'uint256' }],
name: 'setProtocolFeePercent',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'uint256', name: '_feePercent', type: 'uint256' }],
name: 'setSubjectFeePercent',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: '', type: 'address' },
{ internalType: 'address', name: '', type: 'address' },
],
name: 'sharesBalance',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: '', type: 'address' }],
name: 'sharesSupply',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'subjectFeePercent',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
name: 'transferOwnership',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
] as const;
31 changes: 31 additions & 0 deletions src/components/current/widgets/friend-tech/useLeaderboardData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useQuery } from 'react-query';
import axios from 'axios';
import { Address } from 'wagmi';

export interface LeaderboardUserItem {
address: Address;
username: string;
kosettoName: string;
price: string;
}

const useLeaderboardData = () => {
const queryFn = async () => {
const {
data: { leaderboard },
} = await axios.get<{ leaderboard: LeaderboardUserItem[] }>(
`https://prod-api.kosetto.com/leaderboard`
);
return leaderboard;
};

const { data, ...rest } = useQuery({
queryKey: ['friend-tech/leaderboard'],
queryFn,
refetchOnWindowFocus: false,
});

return { data, ...rest };
};

export default useLeaderboardData;
40 changes: 40 additions & 0 deletions src/components/current/widgets/friend-tech/useUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useQuery } from 'react-query';
import axios from 'axios';
import { Address } from 'wagmi';

interface FriendTechUser {
id: number;
address: string;
twitterUsername: string;
twitterName: string;
twitterPfpUrl: string;
twitterUserId: string;
lastOnline: number;
holderCount: number;
holdingCount: number;
shareSupply: number;
displayPrice: string;
lifetimeFeesCollectedInWei: string;
}

// friend tech user data hook
const useUser = (userAddress: Address) => {
const queryFn = async () => {
const { data } = await axios.get<FriendTechUser>(
`https://prod-api.kosetto.com/users/${userAddress}`
);
return data;
};

// use react query
const { data, ...rest } = useQuery({
queryKey: ['friend-tech/user', userAddress],
queryFn,
refetchOnWindowFocus: false,
retry: false,
});

return { data, ...rest };
};

export default useUser;