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

Feat/add linkdrop #1307

Merged
merged 4 commits into from
Sep 9, 2024
Merged
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
172 changes: 172 additions & 0 deletions src/components/tools/Linkdrops/CreateTokenDrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { Button, Flex, Form, Input, openToast, Text } from '@near-pagoda/ui';
import { parseNearAmount } from 'near-api-js/lib/utils/format';
import { useContext, useEffect, useState } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';

import generateAndStore from '@/utils/linkdrops';

import { NearContext } from '../../WalletSelector';

type FormData = {
dropName: string;
numberLinks: number;
amountPerLink: number;
};

function displayBalance(balance: number) {
let display = Math.floor(balance * 100) / 100;

if (balance < 1) {
display = Math.floor(balance * 100000) / 100000;
if (balance && !display) return '< 0.00001';
return display;
}

return display;
}

const getDeposit = (amountPerLink: number, numberLinks: number) =>
parseNearAmount(((0.0426 + amountPerLink) * numberLinks).toString());

const CreateTokenDrop = () => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
defaultValues: {
numberLinks: 1,
amountPerLink: 0,
},
});

const { wallet, signedAccountId } = useContext(NearContext);
const [currentNearAmount, setCurrentNearAmount] = useState(0);

useEffect(() => {
if (!wallet || !signedAccountId) return;

const loadBalance = async () => {
try {
const balance = await wallet.getBalance(signedAccountId);
const requiredGas = 0.00005;
const cost = 0.0426;
setCurrentNearAmount(balance - requiredGas - cost);
} catch (error) {
console.error(error);
}
};

loadBalance();
}, [wallet, signedAccountId]);

const onSubmit: SubmitHandler<FormData> = async (data) => {
if (!wallet) throw new Error('Wallet has not initialized yet');

try {
const args = {
deposit_per_use: parseNearAmount(data.amountPerLink.toString()),
metadata: JSON.stringify({
dropName: data.dropName,
}),
public_keys: generateAndStore(data.dropName, data.numberLinks),
};

await wallet.signAndSendTransactions({
transactions: [
{
receiverId: 'v2.keypom.near',
actions: [
{
type: 'FunctionCall',
params: {
methodName: 'create_drop',
args,
gas: '300000000000000',
deposit: getDeposit(data.amountPerLink, data.numberLinks),
},
},
],
},
],
});

openToast({
type: 'success',
title: 'Form Submitted',
description: 'Your form has been submitted successfully',
duration: 5000,
});
} catch (error) {
console.log(error);

openToast({
type: 'error',
title: 'Error',
description: 'Failed to submit form',
duration: 5000,
});
}
};
return (
<>
<Text size="text-l" style={{ marginBottom: '12px' }}>
Token Drop
</Text>
<Form onSubmit={handleSubmit(onSubmit)}>
<Flex stack gap="l">
<Input
label="Token Drop name"
placeholder="NEARCon Token Giveaway"
error={errors.dropName?.message}
{...register('dropName', { required: 'Token Drop name is required' })}
/>
<Input
label="Number of links"
number={{ allowDecimal: false, allowNegative: false }}
placeholder="1 - 50"
error={errors.numberLinks?.message}
{...register('numberLinks', {
min: {
message: 'Must be greater than 0',
value: 1,
},
max: {
message: `Must be equal to or less than 50`,
value: 50,
},
valueAsNumber: true,
required: 'Number of links is required',
})}
/>
<Input
label="Amount per link"
number={{
allowNegative: false,
allowDecimal: true,
}}
assistive={`${displayBalance(currentNearAmount)} available`}
placeholder="Enter an amount"
error={errors.amountPerLink?.message}
{...register('amountPerLink', {
min: {
message: 'Must be greater than 0',
value: 0.0000000001,
},
max: {
message: `Must be equal to or less than ${currentNearAmount}`,
value: currentNearAmount,
},
valueAsNumber: true,
required: 'Amount per link is required',
})}
/>
<Button label="Create links" variant="affirmative" type="submit" loading={isSubmitting} />
</Flex>
</Form>
</>
);
};

export default CreateTokenDrop;
55 changes: 55 additions & 0 deletions src/components/tools/Linkdrops/ListTokenDrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Accordion, Badge, Button, copyTextToClipboard, Flex, Text, Tooltip } from '@near-pagoda/ui';
import { Copy } from '@phosphor-icons/react';

import type { Drops } from '@/utils/types';

const getDropName = (drop: Drops) => {
return drop ? JSON.parse(drop.metadata).dropName : '';
};

const ListTokenDrop = ({ drops }: { drops: Drops[] }) => {
return (
<Accordion.Root type="multiple">
{drops.map((drop) => {
return (
<Accordion.Item key={drop.drop_id} value={drop.drop_id}>
<Accordion.Trigger>
{getDropName(drop)} - Claimed {drop.next_key_id - drop.registered_uses}/{drop.next_key_id}
</Accordion.Trigger>
<Accordion.Content>
{drop.keys &&
drop.keys.map((key) => {
const wasClaimed = !drop.information.some((info) => info.pk == key.public);
return (
<Flex align="center" justify="space-between" key={key.private}>
<Text style={{ maxWidth: '10rem' }} clampLines={1}>
{key.private}
</Text>
<Badge
label={wasClaimed ? 'Claimed' : 'Unclaimed'}
variant={wasClaimed ? 'success' : 'neutral'}
/>
<Tooltip content="Copy Account ID">
<Button
label="copy"
onClick={() => {
const url = 'https://app.mynearwallet.com' + '/linkdrop/v2.keypom.near/' + key.private;
copyTextToClipboard(url, url);
}}
size="small"
fill="outline"
icon={<Copy />}
/>
</Tooltip>
</Flex>
);
})}
</Accordion.Content>
</Accordion.Item>
);
})}
</Accordion.Root>
);
};

export default ListTokenDrop;
15 changes: 15 additions & 0 deletions src/components/tools/Linkdrops/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Drops } from '@/utils/types';

import CreateTokenDrop from './CreateTokenDrop';
import ListTokenDrop from './ListTokenDrop';

const Linkdrops = ({ drops }: { drops: Drops[] }) => {
return (
<>
<CreateTokenDrop />
<ListTokenDrop drops={drops} />
</>
);
};

export default Linkdrops;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useContext } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';

import { NearContext } from '../WalletSelector';
import { NearContext } from '../../WalletSelector';

type FormData = {
title: string;
Expand Down
54 changes: 54 additions & 0 deletions src/hooks/useLinkdrops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useContext, useEffect, useState } from 'react';

import { NearContext } from '@/components/WalletSelector';
import { getKeypomKeys } from '@/utils/linkdrops';
import type { Drops } from '@/utils/types';

const useLinkdrops = () => {
const { signedAccountId } = useContext(NearContext);
const [drops, setDrops] = useState<Drops[]>([]);

const { wallet } = useContext(NearContext);

useEffect(() => {
const fetchDropData = async () => {
if (!wallet || !signedAccountId) return;
const fetchedDrops: Drops[] = await wallet.viewMethod({
contractId: 'v2.keypom.near',
method: 'get_drops_for_owner',
args: { account_id: signedAccountId },
});

const filteredDrops = fetchedDrops.filter(
(drop) =>
drop.metadata &&
JSON.parse(drop.metadata).dropName &&
getKeypomKeys(JSON.parse(drop.metadata).dropName).length,
);

const fetchedInformationDrops = await Promise.all(
filteredDrops.map(async (drop) => {
const information = await wallet.viewMethod({
contractId: 'v2.keypom.near',
method: 'get_keys_for_drop',
args: { drop_id: drop.drop_id },
});
return { ...drop, information };
}),
);

const localDataDrops = fetchedInformationDrops.map((drop) => ({
...drop,
keys: getKeypomKeys(JSON.parse(drop.metadata).dropName),
}));

setDrops(localDataDrops);
};

fetchDropData();
}, [wallet, signedAccountId]);

return drops;
};

export default useLinkdrops;
7 changes: 5 additions & 2 deletions src/pages/tools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ import { Coin, Gift, ImagesSquare } from '@phosphor-icons/react';
import { useRouter } from 'next/router';
import { useContext } from 'react';

import Linkdrops from '@/components/tools/Linkdrops';
import NonFungibleToken from '@/components/tools/NonFungibleToken';
import { NearContext } from '@/components/WalletSelector';
import { useDefaultLayout } from '@/hooks/useLayout';
import useLinkdrops from '@/hooks/useLinkdrops';
import { useSignInRedirect } from '@/hooks/useSignInRedirect';
import type { NextPageWithLayout } from '@/utils/types';

const ToolsPage: NextPageWithLayout = () => {
const router = useRouter();
const selectedTab = (router.query.tab as string) || 'ft';
const { signedAccountId } = useContext(NearContext);
const drops = useLinkdrops();

const { requestAuthentication } = useSignInRedirect();
return (
Expand Down Expand Up @@ -52,13 +55,13 @@ const ToolsPage: NextPageWithLayout = () => {
</Tabs.Content>

<Tabs.Content value="linkdrops">
<Text>Coming soon</Text>
<Linkdrops drops={drops} />
</Tabs.Content>
</Tabs.Root>
</Card>
) : (
<Card>
<Text>Please sign in to use wallet utilities.</Text>
<Text>Please sign in to use wallet utilities</Text>
<Button label="Sign In" fill="outline" onClick={() => requestAuthentication()} />
</Card>
)}
Expand Down
40 changes: 40 additions & 0 deletions src/utils/linkdrops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { KeyPair } from 'near-api-js';

export interface Keys {
publicKey: PublicKey;
secretKey: string;
extendedSecretKey: string;
}

export interface PublicKey {
keyType: number;
data: { [key: string]: number };
}

export const getKeypomKeys = (dropName: string) => {
const keys = localStorage.getItem(`keysPom:${dropName}`);
if (keys) {
return JSON.parse(keys);
}
return [];
};

const setKeypomKeys = (dropName: string, keys: Keys[]) => {
localStorage.setItem(`keysPom:${dropName}`, JSON.stringify(keys));
};

const generateAndStore = (dropName: string, dropsNumber: number) => {
const keys = [];
const keysLocalStorage = getKeypomKeys(dropName);
for (let index = 0; index < dropsNumber; index++) {
const newKeyPair = KeyPair.fromRandom('ed25519');
const publicKey = newKeyPair.getPublicKey().toString();
keys.push(publicKey);
keysLocalStorage.push({ private: newKeyPair.toString(), public: publicKey });
}
setKeypomKeys(dropName, keysLocalStorage);

return keys;
};

export default generateAndStore;
Loading