Skip to content

Commit

Permalink
Merge pull request #1307 from matiasbenary/feat/add-linkdrop
Browse files Browse the repository at this point in the history
Feat/add linkdrop
  • Loading branch information
calebjacob authored Sep 9, 2024
2 parents 4a64c38 + e46b609 commit 9613eb9
Show file tree
Hide file tree
Showing 9 changed files with 374 additions and 3 deletions.
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
File renamed without changes.
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

0 comments on commit 9613eb9

Please sign in to comment.