feat: UI metamask integration (#41)
* feat: add redux and metamask slice * refactor: move from metamask to generic wallet using ethers * feat: add wallet button base * feat: add getContract function and mint functionality * refactor: move functions to ethereum file * feat: wallet menus and disconnect function * refactor: Ethereum object typings * feat: add FleekERC721 contract interaction abstraction * refactor: remove token detail fetch mock using * refactor: add ethereum mint function to mint site component * feat: add wallet initialize * wip: add signature for lastTokenId function * feat: integrate list of tokens * refactor: mint params construct * fix: global window.ethereum type definition * fix: remove console log * fix: remove todo comment * fix: list view items displaying
This commit is contained in:
parent
0e67867560
commit
8b88cf2881
|
|
@ -152,15 +152,7 @@ contract FleekTest is Test {
|
|||
"}"
|
||||
);
|
||||
|
||||
assertEq(
|
||||
tokenURI,
|
||||
string(
|
||||
abi.encodePacked(
|
||||
"data:application/json;base64,",
|
||||
Base64.encode((dataURI))
|
||||
)
|
||||
)
|
||||
);
|
||||
assertEq(tokenURI, string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI)))));
|
||||
}
|
||||
|
||||
function testCallingTokenURIAfterChangingAllPossibleFields() public {
|
||||
|
|
@ -178,18 +170,11 @@ contract FleekTest is Test {
|
|||
assertEq(mint, 0);
|
||||
|
||||
fleekContract.setTokenName(mint, "Foundry Test App 2");
|
||||
fleekContract.setTokenDescription(
|
||||
mint,
|
||||
"This is a test application submitted by foundry tests. 2"
|
||||
);
|
||||
fleekContract.setTokenDescription(mint, "This is a test application submitted by foundry tests. 2");
|
||||
fleekContract.setTokenImage(mint, "https://fleek2.xyz");
|
||||
fleekContract.setTokenExternalURL(mint, "https://fleek2.xyz");
|
||||
fleekContract.setTokenENS(mint, "fleek_xyz2");
|
||||
fleekContract.setTokenBuild(
|
||||
mint,
|
||||
"afff3f62",
|
||||
"https://github.com/fleekxyz/contracts2"
|
||||
);
|
||||
fleekContract.setTokenBuild(mint, "afff3f62", "https://github.com/fleekxyz/contracts2");
|
||||
|
||||
string memory tokenURI = fleekContract.tokenURI(mint);
|
||||
|
||||
|
|
@ -211,20 +196,10 @@ contract FleekTest is Test {
|
|||
"}"
|
||||
);
|
||||
|
||||
assertEq(
|
||||
tokenURI,
|
||||
string(
|
||||
abi.encodePacked(
|
||||
"data:application/json;base64,",
|
||||
Base64.encode((dataURI))
|
||||
)
|
||||
)
|
||||
);
|
||||
assertEq(tokenURI, string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI)))));
|
||||
}
|
||||
|
||||
function testFailChangingAllPossibleFieldsOnAnotherUsersTokenWithoutAccess()
|
||||
public
|
||||
{
|
||||
function testFailChangingAllPossibleFieldsOnAnotherUsersTokenWithoutAccess() public {
|
||||
uint256 mint = fleekContract.mint(
|
||||
DEPLOYER,
|
||||
"Foundry Test App",
|
||||
|
|
@ -241,18 +216,11 @@ contract FleekTest is Test {
|
|||
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
|
||||
|
||||
fleekContract.setTokenName(mint, "Foundry Test App 2");
|
||||
fleekContract.setTokenDescription(
|
||||
mint,
|
||||
"This is a test application submitted by foundry tests. 2"
|
||||
);
|
||||
fleekContract.setTokenDescription(mint, "This is a test application submitted by foundry tests. 2");
|
||||
fleekContract.setTokenImage(mint, "https://fleek2.xyz");
|
||||
fleekContract.setTokenExternalURL(mint, "https://fleek2.xyz");
|
||||
fleekContract.setTokenENS(mint, "fleek_xyz2");
|
||||
fleekContract.setTokenBuild(
|
||||
mint,
|
||||
"afff3f62",
|
||||
"https://github.com/fleekxyz/contracts2"
|
||||
);
|
||||
fleekContract.setTokenBuild(mint, "afff3f62", "https://github.com/fleekxyz/contracts2");
|
||||
|
||||
string memory tokenURI = fleekContract.tokenURI(mint);
|
||||
|
||||
|
|
@ -274,15 +242,7 @@ contract FleekTest is Test {
|
|||
"}"
|
||||
);
|
||||
|
||||
assertEq(
|
||||
tokenURI,
|
||||
string(
|
||||
abi.encodePacked(
|
||||
"data:application/json;base64,",
|
||||
Base64.encode((dataURI))
|
||||
)
|
||||
)
|
||||
);
|
||||
assertEq(tokenURI, string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI)))));
|
||||
}
|
||||
|
||||
function testFailCallingTokenURIOnNonExistantToken() public {
|
||||
|
|
@ -306,12 +266,7 @@ contract FleekTest is Test {
|
|||
|
||||
assertEq(
|
||||
fleekContract.tokenURI(0),
|
||||
string(
|
||||
abi.encodePacked(
|
||||
"data:application/json;base64,",
|
||||
Base64.encode((dataURI))
|
||||
)
|
||||
)
|
||||
string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI))))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -433,9 +388,7 @@ contract FleekTest is Test {
|
|||
fleekContract.setTokenDescription(mint, "NEW TOKEN NAME!");
|
||||
}
|
||||
|
||||
function testFailSetTokenDescriptionOnAnotherUsersTokenWithoutAccess()
|
||||
public
|
||||
{
|
||||
function testFailSetTokenDescriptionOnAnotherUsersTokenWithoutAccess() public {
|
||||
uint256 mint = fleekContract.mint(
|
||||
DEPLOYER,
|
||||
"Foundry Test App",
|
||||
|
|
@ -507,9 +460,7 @@ contract FleekTest is Test {
|
|||
fleekContract.setTokenExternalURL(mint, "https://ethereum.org");
|
||||
}
|
||||
|
||||
function testFailSetTokenExternalURLOnAnotherUsersTokenWithoutAccess()
|
||||
public
|
||||
{
|
||||
function testFailSetTokenExternalURLOnAnotherUsersTokenWithoutAccess() public {
|
||||
uint256 mint = fleekContract.mint(
|
||||
DEPLOYER,
|
||||
"Foundry Test App",
|
||||
|
|
@ -542,11 +493,7 @@ contract FleekTest is Test {
|
|||
|
||||
assertEq(mint, 0);
|
||||
|
||||
fleekContract.setTokenBuild(
|
||||
mint,
|
||||
"aaaaaaa",
|
||||
"https://github.com/fleekxyz/test_contracts"
|
||||
);
|
||||
fleekContract.setTokenBuild(mint, "aaaaaaa", "https://github.com/fleekxyz/test_contracts");
|
||||
}
|
||||
|
||||
function testFailSetTokenBuildOnAnotherUsersTokenWithoutAccess() public {
|
||||
|
|
@ -565,11 +512,7 @@ contract FleekTest is Test {
|
|||
|
||||
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
|
||||
|
||||
fleekContract.setTokenBuild(
|
||||
mint,
|
||||
"aaaaaaa",
|
||||
"https://github.com/fleekxyz/test_contracts"
|
||||
);
|
||||
fleekContract.setTokenBuild(mint, "aaaaaaa", "https://github.com/fleekxyz/test_contracts");
|
||||
}
|
||||
|
||||
function testSetTokenENS() public {
|
||||
|
|
@ -629,9 +572,7 @@ contract FleekTest is Test {
|
|||
);
|
||||
}
|
||||
|
||||
function testFailAddTokenControllerOnAnotherUsersTokenWithoutAccess()
|
||||
public
|
||||
{
|
||||
function testFailAddTokenControllerOnAnotherUsersTokenWithoutAccess() public {
|
||||
uint256 mint = fleekContract.mint(
|
||||
DEPLOYER,
|
||||
"Foundry Test App",
|
||||
|
|
@ -706,9 +647,7 @@ contract FleekTest is Test {
|
|||
);
|
||||
}
|
||||
|
||||
function testFailRemoveTokenControllerOnAnotherUsersTokenWithoutAccess()
|
||||
public
|
||||
{
|
||||
function testFailRemoveTokenControllerOnAnotherUsersTokenWithoutAccess() public {
|
||||
uint256 mint = fleekContract.mint(
|
||||
DEPLOYER,
|
||||
"Foundry Test App",
|
||||
|
|
@ -815,11 +754,7 @@ contract FleekTest is Test {
|
|||
0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84
|
||||
);
|
||||
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
|
||||
fleekContract.revokeTokenRole(
|
||||
mint,
|
||||
FleekAccessControl.Roles.Controller,
|
||||
DEPLOYER
|
||||
);
|
||||
fleekContract.revokeTokenRole(mint, FleekAccessControl.Roles.Controller, DEPLOYER);
|
||||
}
|
||||
|
||||
function testBalanceOfDeployerAfterAndBeforeMinting() public {
|
||||
|
|
|
|||
|
|
@ -1,46 +1,49 @@
|
|||
{
|
||||
"name": "sites-as-nfts",
|
||||
"version": "0.0.1",
|
||||
"description": "Minimal UI for sites as NFTs",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"author": "Fleek",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.13",
|
||||
"@chakra-ui/react": "^2.4.2",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"formik": "^2.2.9",
|
||||
"framer-motion": "^7.6.17",
|
||||
"path": "^0.12.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.2.3",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"ethers": "^5.7.2",
|
||||
"prettier": "^2.8.0",
|
||||
"react-query": "^3.39.2",
|
||||
"ts-loader": "^9.4.1",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^3.2.4",
|
||||
"vite-tsconfig-paths": "^3.6.0"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "sites-as-nfts",
|
||||
"version": "0.0.1",
|
||||
"description": "Minimal UI for sites as NFTs",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"author": "Fleek",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.13",
|
||||
"@chakra-ui/react": "^2.4.2",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@ethersproject/providers": "^5.7.2",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"formik": "^2.2.9",
|
||||
"framer-motion": "^7.6.17",
|
||||
"path": "^0.12.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.2.3",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"ethers": "^5.7.2",
|
||||
"prettier": "^2.8.0",
|
||||
"react-query": "^3.39.2",
|
||||
"ts-loader": "^9.4.1",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^3.2.4",
|
||||
"vite-tsconfig-paths": "^3.6.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
|
||||
import { WalletButton } from './components';
|
||||
import { initializeWallet } from './store';
|
||||
import { Home, MintSite, MintedSiteDetail } from './views';
|
||||
|
||||
initializeWallet();
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/mint-site" element={<MintSite />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="/detail" element={<MintedSiteDetail />} />
|
||||
<Route path="*" element={<Navigate to="/home" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<>
|
||||
<WalletButton />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/mint-site" element={<MintSite />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="/detail" element={<MintedSiteDetail />} />
|
||||
<Route path="*" element={<Navigate to="/home" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ import {
|
|||
Stack,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { FleekERC721 } from '@/integrations';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
interface CardSiteProps {
|
||||
site: SiteNFTDetail;
|
||||
tokenId?: string; // TODO add param and remove optional
|
||||
tokenId: number;
|
||||
}
|
||||
|
||||
type InfoContainerProps = {
|
||||
|
|
@ -35,9 +36,15 @@ const InfoContainer = ({ heading, info, width }: InfoContainerProps) => (
|
|||
/>
|
||||
);
|
||||
|
||||
export const SiteCard: React.FC<CardSiteProps> = ({ site, tokenId }) => {
|
||||
const { name, owner, image, externalUrl } = site;
|
||||
export const SiteCard: React.FC<CardSiteProps> = ({ tokenId }) => {
|
||||
const navigate = useNavigate();
|
||||
const { data, isLoading } = useQuery<SiteNFTDetail>(
|
||||
`fetchDetail${tokenId}`,
|
||||
() => FleekERC721.tokenMetadata(tokenId)
|
||||
);
|
||||
|
||||
if (!data || isLoading) return null;
|
||||
const { name, owner, image, external_url: externalUrl } = data as any;
|
||||
return (
|
||||
<Card
|
||||
borderColor="#f3f3f36b !important"
|
||||
|
|
@ -47,10 +54,7 @@ export const SiteCard: React.FC<CardSiteProps> = ({ site, tokenId }) => {
|
|||
borderRadius="10px"
|
||||
width="350px"
|
||||
height="350px"
|
||||
// TODO add token id param
|
||||
onClick={() => {
|
||||
navigate(`/detail?tokenId=${1}`);
|
||||
}}
|
||||
onClick={() => navigate(`/detail?tokenId=${tokenId}`)}
|
||||
>
|
||||
<CardBody width="350px" height="350px" paddingTop="10px">
|
||||
<Heading size="md" textAlign="center" marginBottom="10px">
|
||||
|
|
@ -78,8 +82,7 @@ export const SiteCard: React.FC<CardSiteProps> = ({ site, tokenId }) => {
|
|||
</Link>
|
||||
<Stack mt="10px" spacing="3" overflowY="scroll">
|
||||
<InfoContainer heading="Owner" info={owner} width="auto" />
|
||||
{/* TODO add param */}
|
||||
<InfoContainer heading="Token ID" info="1" width="100px" />
|
||||
<InfoContainer heading="Token ID" info={tokenId} width="100px" />
|
||||
<InfoContainer
|
||||
heading="External url"
|
||||
width="100px"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
export * from './loading';
|
||||
export * from './home-button';
|
||||
export * from './image-preview';
|
||||
export * from './tile-info';
|
||||
export * from './card';
|
||||
export * from './accordion-item';
|
||||
export * from './input-field-form';
|
||||
export * from './attributes-detail';
|
||||
export * from './wallet-button';
|
||||
export * from './loading';
|
||||
export * from './home-button';
|
||||
export * from './image-preview';
|
||||
export * from './tile-info';
|
||||
export * from './card';
|
||||
export * from './accordion-item';
|
||||
export * from './input-field-form';
|
||||
export * from './attributes-detail';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from './wallet-button';
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useAppDispatch, useWalletStore, walletActions } from '@/store';
|
||||
import { contractAddress } from '@/utils';
|
||||
import { Menu, MenuButton, MenuList, MenuItem, Button } from '@chakra-ui/react';
|
||||
|
||||
const WalletMenu: React.FC = () => {
|
||||
const { account = '', provider } = useWalletStore();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleCopyAccount = useCallback(() => {
|
||||
navigator.clipboard.writeText(account);
|
||||
}, [account]);
|
||||
|
||||
const handleDisconnect = useCallback(() => {
|
||||
dispatch(walletActions.disconnect());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton as={Button}>
|
||||
{`${provider} (${contractAddress(account)})`}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem onClick={handleCopyAccount}>Copy Account</MenuItem>
|
||||
<MenuItem onClick={handleDisconnect}>Disconnect</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectionMenu: React.FC = () => {
|
||||
const { state } = useWalletStore();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleConnectWallet = useCallback(() => {
|
||||
dispatch(walletActions.connect('metamask'));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
isLoading={state === 'loading'}
|
||||
disabled={state === 'loading'}
|
||||
>
|
||||
Connect Wallet
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem onClick={handleConnectWallet}>Metamask</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export const WalletButton: React.FC = () => {
|
||||
const { state } = useWalletStore();
|
||||
|
||||
if (state === 'connected') {
|
||||
return <WalletMenu />;
|
||||
}
|
||||
|
||||
return <ConnectionMenu />;
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { ExternalProvider } from '@ethersproject/providers';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ethereum: ExternalProvider;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ import ReactDOM from 'react-dom/client';
|
|||
import { App } from './app';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { theme } from './theme';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { store } from './store';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
|
@ -13,11 +15,13 @@ const root = ReactDOM.createRoot(
|
|||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ChakraProvider theme={theme} resetCSS>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</ChakraProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<ChakraProvider theme={theme} resetCSS>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</ChakraProvider>
|
||||
</ReduxProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
export { default as FleekERC721 } from './FleekERC721.json';
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { JsonRpcProvider, Networkish } from '@ethersproject/providers';
|
||||
import { ethers } from 'ethers';
|
||||
import * as Contracts from './contracts';
|
||||
|
||||
export const Ethereum: Ethereum.Core = {
|
||||
defaultNetwork: 'https://rpc-mumbai.maticvigil.com', // TODO: make it environment variable
|
||||
|
||||
provider: {
|
||||
metamask:
|
||||
window.ethereum && new ethers.providers.Web3Provider(window.ethereum),
|
||||
},
|
||||
|
||||
getContract(contractName, providerName) {
|
||||
const contract = Contracts[contractName];
|
||||
|
||||
const provider =
|
||||
providerName && providerName in this.provider
|
||||
? this.provider[providerName].getSigner()
|
||||
: ethers.getDefaultProvider(this.defaultNetwork);
|
||||
|
||||
return new ethers.Contract(contract.address, contract.abi, provider);
|
||||
},
|
||||
};
|
||||
|
||||
export namespace Ethereum {
|
||||
export type Providers = 'metamask';
|
||||
|
||||
export type Core = {
|
||||
defaultNetwork: Networkish;
|
||||
|
||||
provider: {
|
||||
[key in Providers]: JsonRpcProvider;
|
||||
};
|
||||
|
||||
getContract: (
|
||||
contractName: keyof typeof Contracts,
|
||||
providerName?: Providers
|
||||
) => ethers.Contract;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './ethereum';
|
||||
export * from './lib';
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { Ethereum } from '../ethereum';
|
||||
|
||||
export const FleekERC721 = {
|
||||
async mint(
|
||||
params: FleekERC721.MintParams,
|
||||
provider: Ethereum.Providers
|
||||
): Promise<void> {
|
||||
const contract = Ethereum.getContract('FleekERC721', provider);
|
||||
|
||||
const response = await contract.mint(
|
||||
params.owner,
|
||||
params.name,
|
||||
params.description,
|
||||
params.image,
|
||||
params.externalUrl,
|
||||
params.ens,
|
||||
params.commitHash,
|
||||
params.repo,
|
||||
'author' // TODO: remove author after contract update
|
||||
);
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
async tokenMetadata(tokenId: number): Promise<FleekERC721.Metadata> {
|
||||
const contract = Ethereum.getContract('FleekERC721');
|
||||
|
||||
const response = await contract.tokenURI(Number(tokenId));
|
||||
|
||||
const parsed = JSON.parse(
|
||||
Buffer.from(response.slice(29), 'base64').toString('utf-8')
|
||||
);
|
||||
|
||||
return parsed;
|
||||
},
|
||||
|
||||
async lastTokenId(): Promise<number> {
|
||||
// TODO: fetch last token id
|
||||
return 6;
|
||||
},
|
||||
};
|
||||
|
||||
export namespace FleekERC721 {
|
||||
export type MintParams = {
|
||||
name: string;
|
||||
description: string;
|
||||
owner: string;
|
||||
externalUrl: string;
|
||||
image: string;
|
||||
ens?: string;
|
||||
commitHash: string;
|
||||
repo: string;
|
||||
};
|
||||
|
||||
export type Metadata = Omit<MintParams, 'ens' | 'commitHash' | 'repo'> & {
|
||||
attributes: [
|
||||
{
|
||||
trait_type: string;
|
||||
value: string;
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './fleek-erc721';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './ethereum';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './wallet';
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { walletActions, WalletState } from '../wallet-slice';
|
||||
import { RootState } from '@/store/store';
|
||||
import { Ethereum } from '@/integrations';
|
||||
|
||||
export const connect = createAsyncThunk<
|
||||
void,
|
||||
Exclude<WalletState.Provider, null>
|
||||
>('wallet/connect', async (providerName, { dispatch, getState }) => {
|
||||
if ((getState() as RootState).wallet.state === 'loading') return;
|
||||
|
||||
try {
|
||||
dispatch(walletActions.setState('loading'));
|
||||
dispatch(walletActions.setProvider(providerName));
|
||||
|
||||
const response = await Ethereum.provider[providerName].send(
|
||||
'eth_requestAccounts',
|
||||
[]
|
||||
);
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
const [account] = response;
|
||||
|
||||
if (typeof account !== 'string') throw Error('Invalid account type');
|
||||
dispatch(walletActions.setAccount(account));
|
||||
return;
|
||||
}
|
||||
|
||||
throw Error('Invalid response type');
|
||||
} catch (e) {
|
||||
console.error('Could not connect to Wallet', e);
|
||||
dispatch(walletActions.setState('disconnected'));
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './connect';
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './wallet-slice';
|
||||
export * from './utils';
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Ethereum } from '@/integrations';
|
||||
import { store } from '../../store';
|
||||
import { walletActions } from './wallet-slice';
|
||||
|
||||
export const initializeWallet = async (): Promise<void> => {
|
||||
// metamask
|
||||
try {
|
||||
const metamask = Ethereum.provider.metamask;
|
||||
const accounts = await metamask.listAccounts();
|
||||
if (accounts && accounts.length > 0) {
|
||||
store.dispatch(walletActions.setAccount(accounts[0]));
|
||||
}
|
||||
metamask.on('accountsChanged', (accounts: string[]) => {
|
||||
store.dispatch(walletActions.setAccount(accounts[0]));
|
||||
});
|
||||
store.dispatch(walletActions.setProvider('metamask'));
|
||||
} catch {}
|
||||
};
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import * as asyncThunk from './async-thunk';
|
||||
import { RootState } from '@/store';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
import { Ethereum } from '@/integrations';
|
||||
|
||||
export namespace WalletState {
|
||||
export type Provider = Ethereum.Providers | null;
|
||||
|
||||
export type State = 'disconnected' | 'loading' | 'connected';
|
||||
|
||||
export type Account = string;
|
||||
}
|
||||
|
||||
export interface WalletState {
|
||||
provider: WalletState.Provider;
|
||||
state: WalletState.State;
|
||||
account?: WalletState.Account;
|
||||
}
|
||||
|
||||
const initialState: WalletState = {
|
||||
provider: null,
|
||||
state: 'disconnected',
|
||||
account: undefined,
|
||||
};
|
||||
|
||||
export const walletSlice = createSlice({
|
||||
name: 'wallet',
|
||||
initialState,
|
||||
reducers: {
|
||||
setProvider: (state, action: PayloadAction<WalletState.Provider>) => {
|
||||
state.provider = action.payload;
|
||||
},
|
||||
setAccount: (state, action: PayloadAction<string>) => {
|
||||
state.state = 'connected';
|
||||
state.account = action.payload;
|
||||
},
|
||||
setState: (
|
||||
state,
|
||||
action: PayloadAction<Exclude<WalletState.State, 'connected'>>
|
||||
) => {
|
||||
state.state = action.payload;
|
||||
state.provider = null;
|
||||
state.account = undefined;
|
||||
},
|
||||
disconnect: (state) => {
|
||||
state.state = 'disconnected';
|
||||
state.provider = null;
|
||||
state.account = undefined;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const walletActions = {
|
||||
...walletSlice.actions,
|
||||
...asyncThunk,
|
||||
};
|
||||
|
||||
const selectWalletState = (state: RootState): WalletState => state.wallet;
|
||||
|
||||
export const useWalletStore = (): WalletState =>
|
||||
useAppSelector(selectWalletState);
|
||||
|
||||
export default walletSlice.reducer;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||
import type { AppDispatch, RootState } from './store';
|
||||
|
||||
export const useAppDispatch = (): AppDispatch => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './store';
|
||||
export * from './hooks';
|
||||
export * from './features';
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import walletReducer from './features/wallet/wallet-slice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
wallet: walletReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: false,
|
||||
}),
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
export const getRepoAndCommit = (url: string) => {
|
||||
//TODO validate is a github url
|
||||
url = url.replace('/commit', '');
|
||||
const lastIndexSlash = url.lastIndexOf('/');
|
||||
const repo = url.substring(0, lastIndexSlash + 1).slice(0, lastIndexSlash);
|
||||
const commit_hash = url.substring(lastIndexSlash + 1, url.length);
|
||||
return { repo, commit_hash };
|
||||
};
|
||||
export const getRepoAndCommit = (url: string) => {
|
||||
//TODO validate is a github url
|
||||
url = url.replace('/commit', '');
|
||||
const lastIndexSlash = url.lastIndexOf('/');
|
||||
const repo = url.substring(0, lastIndexSlash + 1).slice(0, lastIndexSlash);
|
||||
const commit_hash = url.substring(lastIndexSlash + 1, url.length);
|
||||
return { repo, commit_hash };
|
||||
};
|
||||
|
||||
export const contractAddress = (address: string): string => {
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,16 +18,17 @@ import {
|
|||
AttributesDetail,
|
||||
} from '@/components';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { fetchSiteDetail } from '@/mocks';
|
||||
import { ErrorScreen } from '@/views';
|
||||
import { SiteNFTDetail } from '@/types';
|
||||
import { FleekERC721 } from '@/integrations';
|
||||
|
||||
export const MintedSiteDetail = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const tokenIdParam = searchParams.get('tokenId');
|
||||
//TODO handle response type
|
||||
const { data, status } = useQuery('fetchDetail', () =>
|
||||
fetchSiteDetail(tokenIdParam as string)
|
||||
const { data, status } = useQuery<SiteNFTDetail>(
|
||||
`fetchDetail${tokenIdParam}`,
|
||||
async () => FleekERC721.tokenMetadata(Number(tokenIdParam))
|
||||
);
|
||||
|
||||
if (status === 'loading') {
|
||||
|
|
@ -39,7 +40,8 @@ export const MintedSiteDetail = () => {
|
|||
}
|
||||
|
||||
const { owner, name, description, image, externalUrl, attributes } =
|
||||
data.data as SiteNFTDetail;
|
||||
data as SiteNFTDetail;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex width="full" align="center" justifyContent="center">
|
||||
|
|
|
|||
|
|
@ -1,31 +1,22 @@
|
|||
import React from 'react';
|
||||
import { Loading } from '@/components';
|
||||
import { fetchMintedSites } from '@/mocks';
|
||||
import { SiteNFTDetails } from '@/types';
|
||||
import { Grid, GridItem } from '@chakra-ui/react';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { SiteCard } from '@/components';
|
||||
import { FleekERC721 } from '@/integrations';
|
||||
|
||||
export const ListSites = () => {
|
||||
const { data, isLoading } = useQuery<Array<SiteNFTDetails>, Error>(
|
||||
'fetchSites',
|
||||
fetchMintedSites
|
||||
const { data, isLoading } = useQuery('fetchLastTokenId', () =>
|
||||
FleekERC721.lastTokenId()
|
||||
);
|
||||
|
||||
if (isLoading) return <Loading />;
|
||||
if (!data || isLoading) return <Loading />;
|
||||
return (
|
||||
<Grid
|
||||
templateColumns={{ base: 'repeat(4, 1fr)', md: 'repeat(5, 1fr)' }}
|
||||
gap={10}
|
||||
mt="40px"
|
||||
>
|
||||
{data &&
|
||||
data.listSites.map((site: SiteNFTDetails) => (
|
||||
<GridItem key={site.tokenId}>
|
||||
<SiteCard site={site} />
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
<Flex gap={10} mt="40px" flexWrap="wrap" justifyContent="center">
|
||||
{new Array(data).fill(0).map((_, index) => {
|
||||
const id = data - index - 1;
|
||||
return <SiteCard tokenId={id} />;
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@ import {
|
|||
import { Formik, Field } from 'formik';
|
||||
import { ArrowBackIcon } from '@chakra-ui/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { mintSiteNFT } from '@/mocks';
|
||||
import { getRepoAndCommit } from '@/utils';
|
||||
import { validateFields } from './mint-site.utils';
|
||||
import { InputFieldForm } from '@/components';
|
||||
import { FleekERC721 } from '@/integrations';
|
||||
import { useWalletStore } from '@/store';
|
||||
import { useToast } from '@/hooks';
|
||||
|
||||
interface FormValues {
|
||||
|
|
@ -43,46 +44,53 @@ const initialValues = {
|
|||
|
||||
export const MintSite = () => {
|
||||
const setToastInfo = useToast();
|
||||
const { provider } = useWalletStore();
|
||||
|
||||
const handleSubmitForm = useCallback(async (values: FormValues) => {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
githubCommit,
|
||||
ownerAddress,
|
||||
externalUrl,
|
||||
image,
|
||||
ens,
|
||||
} = values;
|
||||
|
||||
const { repo, commit_hash } = getRepoAndCommit(githubCommit);
|
||||
|
||||
try {
|
||||
await mintSiteNFT({
|
||||
const handleSubmitForm = useCallback(
|
||||
async (values: FormValues) => {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
owner: ownerAddress,
|
||||
githubCommit,
|
||||
ownerAddress,
|
||||
externalUrl,
|
||||
image,
|
||||
ens,
|
||||
commitHash: commit_hash,
|
||||
repo,
|
||||
});
|
||||
//TODO connect with the integration
|
||||
setToastInfo({
|
||||
title: 'Success!',
|
||||
description: 'Your site has been minted.',
|
||||
status: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
setToastInfo({
|
||||
title: 'Error!',
|
||||
description:
|
||||
'We had an error while minting your site. Please try again later',
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
} = values;
|
||||
|
||||
const { repo, commit_hash } = getRepoAndCommit(githubCommit);
|
||||
|
||||
try {
|
||||
if (!provider) throw new Error('No provider found');
|
||||
await FleekERC721.mint(
|
||||
{
|
||||
name,
|
||||
description,
|
||||
owner: ownerAddress,
|
||||
externalUrl,
|
||||
image,
|
||||
ens,
|
||||
commitHash: commit_hash,
|
||||
repo,
|
||||
},
|
||||
provider
|
||||
);
|
||||
setToastInfo({
|
||||
title: 'Success!',
|
||||
description: 'Your site has been minted.',
|
||||
status: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
setToastInfo({
|
||||
title: 'Error!',
|
||||
description:
|
||||
'We had an error while minting your site. Please try again later',
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
[provider]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -198,7 +206,8 @@ export const MintSite = () => {
|
|||
!values.githubCommit ||
|
||||
!values.ownerAddress ||
|
||||
!values.image ||
|
||||
!values.externalUrl
|
||||
!values.externalUrl ||
|
||||
!provider
|
||||
}
|
||||
>
|
||||
Mint
|
||||
|
|
|
|||
748
ui/yarn.lock
748
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue