Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
matiasbenary committed Sep 19, 2024
1 parent 9613eb9 commit faa49f1
Show file tree
Hide file tree
Showing 5 changed files with 1,525 additions and 1 deletion.
195 changes: 195 additions & 0 deletions src/components/tools/FungibleToken/CreateTokenForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { NearContext } from '@/components/WalletSelector';
import { Button, FileInput, Flex, Form, Input, openToast } from '@near-pagoda/ui';
import React, { useContext, useState } from 'react';
import type { SubmitHandler} from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';

type FormData = {
owner_id: string;
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 [imagePreview, setImagePreview] = useState<string | null>(null);

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 onImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setImagePreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};

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) => {
try {
let base64Image = '';
if (data.icon[0]) {
base64Image = await convertToBase64(data.icon[0]);
}
const args = {
args: {
owner_id: data.owner_id,
total_supply: data.total_supply,
metadata: {
spec: "ft-1.0.0",
name: data.name,
symbol: data.symbol,
icon: base64Image,
decimals: data.decimals,
},
},
account_id: data.owner_id,
};

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

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 (
<Form onSubmit={handleSubmit(onSubmit)}>
<Flex stack gap="l">
<Input
label="Owner ID"
placeholder="e.g., bob.near"
error={errors.owner_id?.message}
{...register('owner_id', { required: 'Owner ID is required', value: signedAccountId })}
/>
<Input
label="Total Supply"
placeholder="e.g., 1000000000"
error={errors.total_supply?.message}
{...register('total_supply', { required: 'Total supply is required' })}
/>
<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', {})}
/>
<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>
<Input
label="Decimals"
type="number"
placeholder="e.g., 18"
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' }
})}
/>
<Button
label="Create Token"
variant="affirmative"
type="submit"
loading={isSubmitting}
/>
</Flex>
</Form>
);
};


export default CreateTokenForm;
52 changes: 52 additions & 0 deletions src/components/tools/FungibleToken/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// https://dev.near.org/contractwizard.near/widget/ContractWizardUI
// curl https://api.fastnear.com/v1/account/here.tg/ft
// https://github.com/fastnear/fastnear-api-server-rs?tab=readme-ov-file#api-v1
// near call tkn.near create_token '{"args":{"owner_id": "maguila.near","total_supply": "1000000000","metadata":{"spec": "ft-1.0.0","name": "Test Token","symbol": "TTTEST","icon": "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7","decimals": 18}},"account_id": "maguila.near"}' --gas 300000000000000 --depositYocto 2234830000000000000000000 --accountId maguila.near --networkId mainnet
// https://docs.near.org/build/primitives/ft

import { Card, Flex, SvgIcon, Switch, Text, Tooltip } from '@near-pagoda/ui';
import { CheckFat, ListNumbers, PlusCircle } from '@phosphor-icons/react';
import { useState } from 'react';
import CreateTokenForm from './CreateTokenForm';


const formattedBalance = (balance: string, decimals = 24) => {
const numericBalance = Number(balance);
if (isNaN(numericBalance) || isNaN(decimals)) {
return '0';
}
const result = numericBalance / Math.pow(10, decimals);
return result % 1 === 0 ? result.toString() : result.toFixed(5).replace(/\.?0+$/, '');
};

const FungibleToken = ({tokens}) => {
const [toggle, setToggle] = useState(false);

return (
<div>

<Switch onClick={() => setToggle(!toggle)} iconOn={<PlusCircle weight="bold" />} iconOff={<ListNumbers weight="bold" />} />
{toggle && <CreateTokenForm />}
{!toggle && tokens.map((token, index) => (
<Card key={index} style={{ marginBottom: '8px' }}>
<Flex align="center" justify="space-between">
<Flex align="center" style={{ flex: "1" }} >
<Text>{token.icon && <img width={25} height={25} alt={token.symbol} src={token.icon} />}</Text>
</Flex>
<Text style={{ flex: "1" }} size="text-l">{formattedBalance(token.balance, token.decimals)}</Text>

<Flex justify="end" align='center' style={{ flex: "1" }}>
<Text>{token.symbol}</Text>
{/* {token.verified && ( */}
<Tooltip content="It is verified">
<SvgIcon icon={<CheckFat /> /*<SealCheck />*/} size="m" color="violet8" />
</Tooltip>
{/* )} */}
</Flex>
</Flex>
</Card>
))}
</div>
);
};
export default FungibleToken;
61 changes: 61 additions & 0 deletions src/hooks/useFungibleTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { NearContext } from "@/components/WalletSelector";
import { useContext, useEffect, useState, useCallback } from "react";
import whiteList from '@/utils/white-list.json';

export const accounts_ft = async (accountId: string) => {
const response = await fetch(`https://api.fastnear.com/v1/account/${accountId}/ft`);
return await response.json();
};

const useFungibleTokens = () => {
const { wallet, signedAccountId } = useContext(NearContext);
const [tokens, setTokens] = useState([]);
const [loading, setLoading] = useState(false);

const fetchTokens = useCallback(async () => {
if (!wallet || !signedAccountId) return;

setLoading(true);
try {
const res = await accounts_ft(signedAccountId);
const tokensWithMetadata = await Promise.all(
res.tokens
.filter(token => token.balance !== '0')
.map(async (token) => {
const tokenVerified = whiteList.find((item) => item.token_name === token.contract_id);
if (!tokenVerified) {
let metadata = {};
try {
metadata = await wallet.viewMethod({ contractId: token.contract_id, method: 'ft_metadata' });
} catch (error) {
console.error(`Error fetching metadata for ${token.contract_id}:`, error);
}
return {
...metadata,
balance: token.balance,
verified: false,
};
}
return {
...tokenVerified,
balance: token.balance,
verified: true,
};
}),
);
setTokens(tokensWithMetadata);
} catch (error) {
console.error("Error fetching fungible tokens:", error);
} finally {
setLoading(false);
}
}, [wallet, signedAccountId]);

useEffect(() => {
fetchTokens();
}, [fetchTokens]);

return { tokens, loading, reloadTokens: fetchTokens };
};

export default useFungibleTokens;
5 changes: 4 additions & 1 deletion src/pages/tools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import { useDefaultLayout } from '@/hooks/useLayout';
import useLinkdrops from '@/hooks/useLinkdrops';
import { useSignInRedirect } from '@/hooks/useSignInRedirect';
import type { NextPageWithLayout } from '@/utils/types';
import FungibleToken from '@/components/tools/FungibleToken';
import useFungibleTokens from '@/hooks/useFungibleTokens';

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

const { requestAuthentication } = useSignInRedirect();
return (
Expand Down Expand Up @@ -47,7 +50,7 @@ const ToolsPage: NextPageWithLayout = () => {
</Tabs.List>

<Tabs.Content value="ft">
<Text>Coming soon</Text>
<FungibleToken tokens={tokens}/>
</Tabs.Content>

<Tabs.Content value="nft">
Expand Down
Loading

0 comments on commit faa49f1

Please sign in to comment.