Skip to content

Commit

Permalink
Merge pull request #1310 from matiasbenary/feat/add-linkdrop-nft
Browse files Browse the repository at this point in the history
feat: add NFTs for linkdrops
  • Loading branch information
calebjacob authored Sep 19, 2024
2 parents 9613eb9 + 868321b commit 05204d7
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 7 deletions.
2 changes: 1 addition & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const nextConfig = {
compiler: { styledComponents: true },
reactStrictMode: true,
images: {
domains: ['ipfs.near.social'],
domains: ['ipfs.near.social','ipfs.io'],
},
experimental: {
optimizePackageImports: ['@phosphor-icons/react'],
Expand Down
22 changes: 20 additions & 2 deletions src/components/NTFImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,33 @@ interface NftImageProps {

const DEFAULT_IMAGE = 'https://ipfs.near.social/ipfs/bafkreibmiy4ozblcgv3fm3gc6q62s55em33vconbavfd2ekkuliznaq3zm';

const getImage = (key: string) => {
const imgUrl = localStorage.getItem(`keysImage:${key}`);
return imgUrl || null;
};

const setImage = (key: string, url: string) => {
localStorage.setItem(`keysImage:${key}`, url);
};

export const NftImage: React.FC<NftImageProps> = ({ nft, ipfs_cid, alt }) => {
const { wallet } = useContext(NearContext);
const [imageUrl, setImageUrl] = useState<string>(DEFAULT_IMAGE);

const fetchNftData = useCallback(async () => {
if (!wallet || !nft || !nft.contractId || !nft.tokenId || ipfs_cid) return;

const imgCache = getImage(nft.tokenId);
if (imgCache) {
setImageUrl(imgCache);
return;
}
const [nftMetadata, tokenData] = await Promise.all([
wallet.viewMethod({ contractId: nft.contractId, method: 'nft_metadata' }),
wallet.viewMethod({ contractId: nft.contractId, method: 'nft_token', args: { token_id: nft.tokenId } }),
]);

const tokenMetadata = tokenData.metadata;
const tokenMedia = tokenMetadata?.media || '';
const tokenMedia = tokenData?.metadata?.media || '';

if (tokenMedia.startsWith('https://') || tokenMedia.startsWith('http://') || tokenMedia.startsWith('data:image')) {
setImageUrl(tokenMedia);
Expand All @@ -54,5 +67,10 @@ export const NftImage: React.FC<NftImageProps> = ({ nft, ipfs_cid, alt }) => {
}
}, [ipfs_cid, fetchNftData]);

useEffect(() => {
if (!wallet || !nft || !nft.contractId || !nft.tokenId || ipfs_cid || DEFAULT_IMAGE === imageUrl) return;
setImage(nft.tokenId, imageUrl);
}, [imageUrl, wallet, nft, ipfs_cid]);

return <RoundedImage width={43} height={43} src={imageUrl} alt={alt} />;
};
208 changes: 208 additions & 0 deletions src/components/tools/Linkdrops/CreateNFTDrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { Accordion, Button, Flex, Form, Input, openToast, Text } from '@near-pagoda/ui';
import { parseNearAmount } from 'near-api-js/lib/utils/format';
import { useContext, useState } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import styled from 'styled-components';

import { NftImage } from '@/components/NTFImage';
import type { NFT } from '@/hooks/useNFT';
import useNFT from '@/hooks/useNFT';
import generateAndStore from '@/utils/linkdrops';

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

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<{
selected: boolean;
}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px;
margin: 4px;
border-radius: 6px;
cursor: pointer;
border: ${(p) => (p.selected ? 'solid 1px #878782' : '')};
`;

const KEYPOM_CONTRACT_ADDRESS = 'v2.keypom.near';

type FormData = {
dropName: string;
amountPerLink: number;
tokenId: string;
senderId: string;
contractId: string;
};

const parseToNFTimage = (nft: NFT, origin: string) => {
return {
contractId: origin,
tokenId: nft.token_id,
};
};

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

const CreateNFTDrop = () => {
const { wallet, signedAccountId } = useContext(NearContext);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
setValue,
} = useForm<FormData>({
defaultValues: {
senderId: signedAccountId,
},
});

const [nftSelected, setNftSelected] = useState('');

const { tokens } = useNFT();

const fillForm = (origin: string, nft: NFT) => () => {
setNftSelected(nft.token_id);
setValue('tokenId', nft.token_id);
setValue('contractId', origin);
};
const onSubmit: SubmitHandler<FormData> = async (data) => {
if (!wallet) throw new Error('Wallet has not initialized yet');
const dropId = Date.now().toString();
const args = {
deposit_per_use: '0',
drop_id: dropId,
metadata: JSON.stringify({
dropName: data.dropName,
}),
public_keys: generateAndStore(data.dropName, 1),
nft: {
sender_id: data.senderId,
contract_id: data.contractId,
},
};

await wallet.signAndSendTransactions({
transactions: [
{
receiverId: KEYPOM_CONTRACT_ADDRESS,
actions: [
{
type: 'FunctionCall',
params: {
methodName: 'create_drop',
args,
gas: '300000000000000',
deposit: getDeposit(1, 1),
},
},
],
},
{
receiverId: data.contractId,
actions: [
{
type: 'FunctionCall',
params: {
methodName: 'nft_transfer_call',
args: {
receiver_id: KEYPOM_CONTRACT_ADDRESS,
token_id: data.tokenId,
msg: dropId,
},
gas: '300000000000000',
deposit: 1,
},
},
],
},
],
});

openToast({
type: 'success',
title: 'Form Submitted',
description: 'Your form has been submitted successfully',
duration: 5000,
});
};
return (
<>
<Text size="text-l">NFT 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' })}
/>
<Accordion.Root type="multiple">
{tokens.map((token, index) => {
return (
<Accordion.Item value={index.toString()} key={`accordion-${token.origin}`}>
<Accordion.Trigger>{token.origin}</Accordion.Trigger>

<Accordion.Content>
<CarouselContainer>
{token.nfts.map((nft) => {
return (
<ImgCard
key={`Carousel-${nft.token_id}`}
onClick={fillForm(token.origin, nft)}
selected={nftSelected === nft.token_id}
>
<NftImage nft={parseToNFTimage(nft, token.origin)} alt={nft.metadata.title} />
<Text>{nft.metadata.title}</Text>
</ImgCard>
);
})}
</CarouselContainer>
</Accordion.Content>
</Accordion.Item>
);
})}
</Accordion.Root>
<Input
label="NFT contract address"
placeholder="Enter a NFT contract address"
disabled
error={errors.contractId?.message}
{...register('contractId', {
required: 'NFT contract address per link is required',
})}
/>
<Input
label="Token ID"
placeholder="Enter a Token ID"
disabled
error={errors.tokenId?.message}
{...register('tokenId', {
required: 'Token ID per link is required',
})}
/>

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

export default CreateNFTDrop;
4 changes: 1 addition & 3 deletions src/components/tools/Linkdrops/CreateTokenDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,7 @@ const CreateTokenDrop = () => {
};
return (
<>
<Text size="text-l" style={{ marginBottom: '12px' }}>
Token Drop
</Text>
<Text size="text-l">Token Drop</Text>
<Form onSubmit={handleSubmit(onSubmit)}>
<Flex stack gap="l">
<Input
Expand Down
19 changes: 18 additions & 1 deletion src/components/tools/Linkdrops/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import { Flex, Switch } from '@near-pagoda/ui';
import { Coins, ImageSquare } from '@phosphor-icons/react';
import { useState } from 'react';

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

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

const Linkdrops = ({ drops }: { drops: Drops[] }) => {
const [selector, setSelector] = useState(false);

return (
<>
<CreateTokenDrop />
<Flex as="label" align="center">
Token
<Switch
onCheckedChange={() => setSelector(!selector)}
iconOn={<ImageSquare weight="bold" />}
iconOff={<Coins weight="bold" />}
/>
NFT
</Flex>
{!selector && <CreateTokenDrop />}
{selector && <CreateNFTDrop />}
<ListTokenDrop drops={drops} />
</>
);
Expand Down
85 changes: 85 additions & 0 deletions src/hooks/useNFT.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useCallback, useContext, useEffect, useState } from 'react';

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

export interface Fastnear {
account_id: string;
tokens: Token[];
}

export interface Token {
contract_id: string;
last_update_block_height: number;
}

export interface NFT {
token_id: string;
owner_id: string;
metadata: Metadata;
approved_account_ids: string[];
}

export interface Metadata {
title: string;
description: string | null;
media: string | null;
media_hash: string | null;
copies: string | null;
issued_at: string | null;
expires_at: string | null;
starts_at: string | null;
updated_at: string | null;
extra: string | null;
reference: string | null;
reference_hash: string | null;
}

export interface NFTInfo {
origin: string;
nfts: NFT[];
}

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

const useNFT = () => {
const { wallet, signedAccountId } = useContext(NearContext);
const [tokens, setTokens] = useState<NFTInfo[]>([]);
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.map(async (token) => {
return {
origin: token.contract_id,
nfts: await wallet.viewMethod({
contractId: token.contract_id,
method: 'nft_tokens_for_owner',
args: { account_id: signedAccountId },
}),
};
}),
);
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 useNFT;

0 comments on commit 05204d7

Please sign in to comment.