Skip to content

Commit

Permalink
Merge pull request #1309 from matiasbenary/feat/add-ft-tools
Browse files Browse the repository at this point in the history
added ft tools
  • Loading branch information
calebjacob authored Sep 20, 2024
2 parents 05204d7 + 90f0d17 commit af5d7fb
Show file tree
Hide file tree
Showing 12 changed files with 1,729 additions and 58 deletions.
2 changes: 1 addition & 1 deletion src/components/NTFImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface Nft {
}

interface NftImageProps {
nft: Nft;
nft?: Nft;
ipfs_cid?: string;
alt: string;
}
Expand Down
190 changes: 190 additions & 0 deletions src/components/tools/FungibleToken/CreateTokenForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { Button, FileInput, Flex, Form, Grid, Input, openToast, Text } from '@near-pagoda/ui';
import React, { useContext } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';

import { NearContext } from '@/components/WalletSelector';

type FormData = {
total_supply: string;
name: string;
symbol: string;
icon: FileList;
decimals: number;
};

const FACTORY_CONTRACT = 'tkn.primitives.near';

const MAX_FILE_SIZE = 10 * 1024;
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];

const CreateTokenForm: React.FC = () => {
const {
control,
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>();

const { wallet, signedAccountId } = useContext(NearContext);

const validateImage = (files: FileList) => {
if (files.length === 0) return 'Image is required';
const file = files[0];
if (file.size > MAX_FILE_SIZE) return 'Image size should be less than 10KB';
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) return 'Not a valid image format';
return true;
};

const convertToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
};

const onSubmit: SubmitHandler<FormData> = async (data) => {
let base64Image = '';
if (data.icon[0]) {
base64Image = await convertToBase64(data.icon[0]);
}

const total_supply = BigInt(data.total_supply) * BigInt(Math.pow(10, Number(data.decimals)));

const args = {
args: {
owner_id: signedAccountId,
total_supply: total_supply.toString(),
metadata: {
spec: 'ft-1.0.0',
name: data.name,
symbol: data.symbol,
icon: base64Image,
decimals: data.decimals,
},
},
account_id: signedAccountId,
};

const requiredDeposit = await wallet?.viewMethod({ contractId: FACTORY_CONTRACT, method: 'get_required', args });

try {
const result = await wallet?.signAndSendTransactions({
transactions: [
{
receiverId: FACTORY_CONTRACT,
actions: [
{
type: 'FunctionCall',
params: {
methodName: 'create_token',
args,
gas: '300000000000000',
deposit: requiredDeposit,
},
},
],
},
],
});

if (result) {
const transactionId = result[0].transaction_outcome.id;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
window.open(`https://nearblocks.io/txns/${transactionId}`, '_blank')!.focus();
}

openToast({
type: 'success',
title: 'Token Created',
description: `Token ${data.name} (${data.symbol}) created successfully`,
duration: 5000,
});
} catch (error) {
openToast({
type: 'error',
title: 'Error',
description: 'Failed to create token',
duration: 5000,
});
}
};

return (
<>
<Text size="text-l" style={{ marginBottom: '12px' }}>
Mint a Fungible Token
</Text>
<Form onSubmit={handleSubmit(onSubmit)}>
<Flex stack gap="l">
<Grid columns="1fr 1fr" columnsTablet="1fr" columnsPhone="1fr">
<Input
label="Total Supply"
placeholder="e.g., 1000"
error={errors.total_supply?.message}
{...register('total_supply', { required: 'Total supply is required' })}
/>
<Input
label="Decimals"
type="number"
placeholder="e.g., 6"
error={errors.decimals?.message}
{...register('decimals', {
required: 'Decimals is required',
valueAsNumber: true,
min: { value: 0, message: 'Decimals must be non-negative' },
max: { value: 24, message: 'Decimals must be 24 or less' },
})}
/>
</Grid>
<Grid columns="1fr 1fr" columnsTablet="1fr" columnsPhone="1fr">
<Input
label="Token Name"
placeholder="e.g., Test Token"
error={errors.name?.message}
{...register('name', { required: 'Token name is required' })}
/>
<Input
label="Token Symbol"
placeholder="e.g., TEST"
error={errors.symbol?.message}
{...register('symbol', { required: 'Token symbol is required' })}
/>
</Grid>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Controller
control={control}
name="icon"
rules={{
required: 'Image is required',
validate: validateImage,
}}
render={({ field, fieldState }) => (
<FileInput
label="Image Upload"
accept={ACCEPTED_IMAGE_TYPES.join(',')}
error={fieldState.error?.message}
{...field}
value={field.value ? Array.from(field.value) : []}
onChange={(value: File[] | null) => {
const files = value;
field.onChange(files);
}}
/>
)}
/>
<span style={{ fontSize: '0.8rem', color: 'gray' }}>
Accepted Formats: PNG, JPEG, GIF, SVG | Ideal dimension: 1:1 | Max size: 10kb
</span>
</div>

<Button label="Create Token" variant="affirmative" type="submit" loading={isSubmitting} />
</Flex>
</Form>
</>
);
};

export default CreateTokenForm;
28 changes: 28 additions & 0 deletions src/components/tools/FungibleToken/ListToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Accordion, Flex, Text } from '@near-pagoda/ui';
import Image from 'next/image';

import type { FT } from '@/pages/tools';

const ListToken = ({ tokens }: { tokens: FT[] }) => {
return (
<Accordion.Root type="multiple">
<Accordion.Item value="one">
<Accordion.Trigger>Tokens you minted</Accordion.Trigger>
<Accordion.Content>
{tokens.map((token) => {
return (
<Flex justify="space-between" align="center" key={`ft-${token.symbol}`}>
<Text>{token.name}</Text>
<Text>{token.symbol}</Text>
<Text>{BigInt(token.total_supply) / BigInt(Math.pow(10, Number(token.decimals)))}</Text>
<Image src={token.icon} alt={token.name} width={50} height={50} />
</Flex>
);
})}
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
};

export default ListToken;
15 changes: 15 additions & 0 deletions src/components/tools/FungibleToken/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { FT } from '@/pages/tools';

import CreateTokenForm from './CreateTokenForm';
import ListToken from './ListToken';

const FungibleToken = ({ tokens }: { tokens: FT[] }) => {
return (
<>
<CreateTokenForm />
<hr />
<ListToken tokens={tokens} />
</>
);
};
export default FungibleToken;
4 changes: 3 additions & 1 deletion src/components/tools/Linkdrops/CreateTokenDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ const CreateTokenDrop = () => {
};
return (
<>
<Text size="text-l">Token Drop</Text>
<Text size="text-l" style={{ marginBottom: '12px' }}>
Create a LinkDrop
</Text>
<Form onSubmit={handleSubmit(onSubmit)}>
<Flex stack gap="l">
<Input
Expand Down
57 changes: 57 additions & 0 deletions src/components/tools/NonFungibleToken/CommunityTools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Text } from '@near-pagoda/ui';
import Image from 'next/image';
import styled from 'styled-components';

import MintBase from '@/assets/images/mintbase.svg';
import Paras from '@/assets/images/paras.svg';

const StyledButton = styled.a`
background-color: #1e2030;
color: #fff;
border: none;
border-radius: 25px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s;
&:hover {
background-color: #101124;
}
`;

const MintbaseButton = styled(StyledButton)`
background-color: #1e2030;
&:hover {
background-color: #282b3b;
}
`;

const ParasButton = styled(StyledButton)`
background-color: #050330;
&:hover {
background-color: #101438;
}
`;

const CommunityTools = () => {
return (
<>
<Text size="text-l" style={{ margin: '12px 0 0 0' }}>
Community tools
</Text>
<Text>For more advanced options use community tools:</Text>
<MintbaseButton href="https://www.mintbase.xyz/" target="_blank">
<Image alt="Mintbase Logo" src={MintBase} width={85} />{' '}
</MintbaseButton>
<ParasButton href="https://paras.id/" target="_blank">
<Image alt="Paras Logo" src={Paras} width={85} />{' '}
</ParasButton>
</>
);
};

export default CommunityTools;
58 changes: 58 additions & 0 deletions src/components/tools/NonFungibleToken/ListToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Accordion, Text } from '@near-pagoda/ui';
import Image from 'next/image';
import styled from 'styled-components';

import type { NFT } from '@/pages/tools';

const RoundedImage = styled(Image)`
border-radius: 50%;
`;

const CarouselContainer = styled.div`
display: flex;
overflow-x: auto;
width: 100%;
scrollbar-width: thin;
&::-webkit-scrollbar {
height: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
`;

const ImgCard = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px;
margin: 4px;
border-radius: 6px;
cursor: pointer;
`;

const ListToken = ({ tokens }: { tokens: NFT[] }) => {
return (
<Accordion.Root type="multiple">
<Accordion.Item value="one">
<Accordion.Trigger>NFT you minted</Accordion.Trigger>
<Accordion.Content>
<CarouselContainer>
{tokens.map((token) => {
return (
<ImgCard key={`Carousel-${token.token_id}`}>
<RoundedImage width={43} height={43} src={token.media} alt={token.title} />
<Text>{token.title}</Text>
</ImgCard>
);
})}
</CarouselContainer>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
};

export default ListToken;
3 changes: 1 addition & 2 deletions src/components/tools/NonFungibleToken/MintNft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ const MintNft = () => {
return (
<>
<Text size="text-l" style={{ marginBottom: '12px' }}>
{' '}
Mint NFT{' '}
Mint a Non-Fungible Token
</Text>
<Form onSubmit={handleSubmit(onSubmit)}>
<Flex stack gap="l">
Expand Down
Loading

0 comments on commit af5d7fb

Please sign in to comment.