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(
|
assertEq(tokenURI, string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI)))));
|
||||||
tokenURI,
|
|
||||||
string(
|
|
||||||
abi.encodePacked(
|
|
||||||
"data:application/json;base64,",
|
|
||||||
Base64.encode((dataURI))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCallingTokenURIAfterChangingAllPossibleFields() public {
|
function testCallingTokenURIAfterChangingAllPossibleFields() public {
|
||||||
|
|
@ -178,18 +170,11 @@ contract FleekTest is Test {
|
||||||
assertEq(mint, 0);
|
assertEq(mint, 0);
|
||||||
|
|
||||||
fleekContract.setTokenName(mint, "Foundry Test App 2");
|
fleekContract.setTokenName(mint, "Foundry Test App 2");
|
||||||
fleekContract.setTokenDescription(
|
fleekContract.setTokenDescription(mint, "This is a test application submitted by foundry tests. 2");
|
||||||
mint,
|
|
||||||
"This is a test application submitted by foundry tests. 2"
|
|
||||||
);
|
|
||||||
fleekContract.setTokenImage(mint, "https://fleek2.xyz");
|
fleekContract.setTokenImage(mint, "https://fleek2.xyz");
|
||||||
fleekContract.setTokenExternalURL(mint, "https://fleek2.xyz");
|
fleekContract.setTokenExternalURL(mint, "https://fleek2.xyz");
|
||||||
fleekContract.setTokenENS(mint, "fleek_xyz2");
|
fleekContract.setTokenENS(mint, "fleek_xyz2");
|
||||||
fleekContract.setTokenBuild(
|
fleekContract.setTokenBuild(mint, "afff3f62", "https://github.com/fleekxyz/contracts2");
|
||||||
mint,
|
|
||||||
"afff3f62",
|
|
||||||
"https://github.com/fleekxyz/contracts2"
|
|
||||||
);
|
|
||||||
|
|
||||||
string memory tokenURI = fleekContract.tokenURI(mint);
|
string memory tokenURI = fleekContract.tokenURI(mint);
|
||||||
|
|
||||||
|
|
@ -211,20 +196,10 @@ contract FleekTest is Test {
|
||||||
"}"
|
"}"
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEq(
|
assertEq(tokenURI, string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI)))));
|
||||||
tokenURI,
|
|
||||||
string(
|
|
||||||
abi.encodePacked(
|
|
||||||
"data:application/json;base64,",
|
|
||||||
Base64.encode((dataURI))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailChangingAllPossibleFieldsOnAnotherUsersTokenWithoutAccess()
|
function testFailChangingAllPossibleFieldsOnAnotherUsersTokenWithoutAccess() public {
|
||||||
public
|
|
||||||
{
|
|
||||||
uint256 mint = fleekContract.mint(
|
uint256 mint = fleekContract.mint(
|
||||||
DEPLOYER,
|
DEPLOYER,
|
||||||
"Foundry Test App",
|
"Foundry Test App",
|
||||||
|
|
@ -241,18 +216,11 @@ contract FleekTest is Test {
|
||||||
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
|
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
|
||||||
|
|
||||||
fleekContract.setTokenName(mint, "Foundry Test App 2");
|
fleekContract.setTokenName(mint, "Foundry Test App 2");
|
||||||
fleekContract.setTokenDescription(
|
fleekContract.setTokenDescription(mint, "This is a test application submitted by foundry tests. 2");
|
||||||
mint,
|
|
||||||
"This is a test application submitted by foundry tests. 2"
|
|
||||||
);
|
|
||||||
fleekContract.setTokenImage(mint, "https://fleek2.xyz");
|
fleekContract.setTokenImage(mint, "https://fleek2.xyz");
|
||||||
fleekContract.setTokenExternalURL(mint, "https://fleek2.xyz");
|
fleekContract.setTokenExternalURL(mint, "https://fleek2.xyz");
|
||||||
fleekContract.setTokenENS(mint, "fleek_xyz2");
|
fleekContract.setTokenENS(mint, "fleek_xyz2");
|
||||||
fleekContract.setTokenBuild(
|
fleekContract.setTokenBuild(mint, "afff3f62", "https://github.com/fleekxyz/contracts2");
|
||||||
mint,
|
|
||||||
"afff3f62",
|
|
||||||
"https://github.com/fleekxyz/contracts2"
|
|
||||||
);
|
|
||||||
|
|
||||||
string memory tokenURI = fleekContract.tokenURI(mint);
|
string memory tokenURI = fleekContract.tokenURI(mint);
|
||||||
|
|
||||||
|
|
@ -274,15 +242,7 @@ contract FleekTest is Test {
|
||||||
"}"
|
"}"
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEq(
|
assertEq(tokenURI, string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI)))));
|
||||||
tokenURI,
|
|
||||||
string(
|
|
||||||
abi.encodePacked(
|
|
||||||
"data:application/json;base64,",
|
|
||||||
Base64.encode((dataURI))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailCallingTokenURIOnNonExistantToken() public {
|
function testFailCallingTokenURIOnNonExistantToken() public {
|
||||||
|
|
@ -306,12 +266,7 @@ contract FleekTest is Test {
|
||||||
|
|
||||||
assertEq(
|
assertEq(
|
||||||
fleekContract.tokenURI(0),
|
fleekContract.tokenURI(0),
|
||||||
string(
|
string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI))))
|
||||||
abi.encodePacked(
|
|
||||||
"data:application/json;base64,",
|
|
||||||
Base64.encode((dataURI))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -433,9 +388,7 @@ contract FleekTest is Test {
|
||||||
fleekContract.setTokenDescription(mint, "NEW TOKEN NAME!");
|
fleekContract.setTokenDescription(mint, "NEW TOKEN NAME!");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailSetTokenDescriptionOnAnotherUsersTokenWithoutAccess()
|
function testFailSetTokenDescriptionOnAnotherUsersTokenWithoutAccess() public {
|
||||||
public
|
|
||||||
{
|
|
||||||
uint256 mint = fleekContract.mint(
|
uint256 mint = fleekContract.mint(
|
||||||
DEPLOYER,
|
DEPLOYER,
|
||||||
"Foundry Test App",
|
"Foundry Test App",
|
||||||
|
|
@ -507,9 +460,7 @@ contract FleekTest is Test {
|
||||||
fleekContract.setTokenExternalURL(mint, "https://ethereum.org");
|
fleekContract.setTokenExternalURL(mint, "https://ethereum.org");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailSetTokenExternalURLOnAnotherUsersTokenWithoutAccess()
|
function testFailSetTokenExternalURLOnAnotherUsersTokenWithoutAccess() public {
|
||||||
public
|
|
||||||
{
|
|
||||||
uint256 mint = fleekContract.mint(
|
uint256 mint = fleekContract.mint(
|
||||||
DEPLOYER,
|
DEPLOYER,
|
||||||
"Foundry Test App",
|
"Foundry Test App",
|
||||||
|
|
@ -542,11 +493,7 @@ contract FleekTest is Test {
|
||||||
|
|
||||||
assertEq(mint, 0);
|
assertEq(mint, 0);
|
||||||
|
|
||||||
fleekContract.setTokenBuild(
|
fleekContract.setTokenBuild(mint, "aaaaaaa", "https://github.com/fleekxyz/test_contracts");
|
||||||
mint,
|
|
||||||
"aaaaaaa",
|
|
||||||
"https://github.com/fleekxyz/test_contracts"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailSetTokenBuildOnAnotherUsersTokenWithoutAccess() public {
|
function testFailSetTokenBuildOnAnotherUsersTokenWithoutAccess() public {
|
||||||
|
|
@ -565,11 +512,7 @@ contract FleekTest is Test {
|
||||||
|
|
||||||
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
|
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
|
||||||
|
|
||||||
fleekContract.setTokenBuild(
|
fleekContract.setTokenBuild(mint, "aaaaaaa", "https://github.com/fleekxyz/test_contracts");
|
||||||
mint,
|
|
||||||
"aaaaaaa",
|
|
||||||
"https://github.com/fleekxyz/test_contracts"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function testSetTokenENS() public {
|
function testSetTokenENS() public {
|
||||||
|
|
@ -629,9 +572,7 @@ contract FleekTest is Test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailAddTokenControllerOnAnotherUsersTokenWithoutAccess()
|
function testFailAddTokenControllerOnAnotherUsersTokenWithoutAccess() public {
|
||||||
public
|
|
||||||
{
|
|
||||||
uint256 mint = fleekContract.mint(
|
uint256 mint = fleekContract.mint(
|
||||||
DEPLOYER,
|
DEPLOYER,
|
||||||
"Foundry Test App",
|
"Foundry Test App",
|
||||||
|
|
@ -706,9 +647,7 @@ contract FleekTest is Test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailRemoveTokenControllerOnAnotherUsersTokenWithoutAccess()
|
function testFailRemoveTokenControllerOnAnotherUsersTokenWithoutAccess() public {
|
||||||
public
|
|
||||||
{
|
|
||||||
uint256 mint = fleekContract.mint(
|
uint256 mint = fleekContract.mint(
|
||||||
DEPLOYER,
|
DEPLOYER,
|
||||||
"Foundry Test App",
|
"Foundry Test App",
|
||||||
|
|
@ -815,11 +754,7 @@ contract FleekTest is Test {
|
||||||
0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84
|
0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84
|
||||||
);
|
);
|
||||||
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
|
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
|
||||||
fleekContract.revokeTokenRole(
|
fleekContract.revokeTokenRole(mint, FleekAccessControl.Roles.Controller, DEPLOYER);
|
||||||
mint,
|
|
||||||
FleekAccessControl.Roles.Controller,
|
|
||||||
DEPLOYER
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBalanceOfDeployerAfterAndBeforeMinting() public {
|
function testBalanceOfDeployerAfterAndBeforeMinting() public {
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,49 @@
|
||||||
{
|
{
|
||||||
"name": "sites-as-nfts",
|
"name": "sites-as-nfts",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Minimal UI for sites as NFTs",
|
"description": "Minimal UI for sites as NFTs",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"author": "Fleek",
|
"author": "Fleek",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.0.13",
|
"@chakra-ui/icons": "^2.0.13",
|
||||||
"@chakra-ui/react": "^2.4.2",
|
"@chakra-ui/react": "^2.4.2",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"formik": "^2.2.9",
|
"@ethersproject/providers": "^5.7.2",
|
||||||
"framer-motion": "^7.6.17",
|
"@reduxjs/toolkit": "^1.9.1",
|
||||||
"path": "^0.12.7",
|
"formik": "^2.2.9",
|
||||||
"react": "^18.2.0",
|
"framer-motion": "^7.6.17",
|
||||||
"react-dom": "^18.2.0",
|
"path": "^0.12.7",
|
||||||
"react-router-dom": "^6.4.4"
|
"react": "^18.2.0",
|
||||||
},
|
"react-dom": "^18.2.0",
|
||||||
"devDependencies": {
|
"react-redux": "^8.0.5",
|
||||||
"@types/jest": "^29.2.3",
|
"react-router-dom": "^6.4.4"
|
||||||
"@types/node": "^18.11.9",
|
},
|
||||||
"@types/react": "^18.0.25",
|
"devDependencies": {
|
||||||
"@types/react-dom": "^18.0.9",
|
"@types/jest": "^29.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@types/node": "^18.11.9",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@types/react": "^18.0.25",
|
||||||
"@vitejs/plugin-react": "^2.2.0",
|
"@types/react-dom": "^18.0.9",
|
||||||
"eslint": "^8.28.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"eslint-plugin-jest": "^27.1.6",
|
"@vitejs/plugin-react": "^2.2.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint": "^8.28.0",
|
||||||
"eslint-plugin-react": "^7.31.11",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"ethers": "^5.7.2",
|
"eslint-plugin-jest": "^27.1.6",
|
||||||
"prettier": "^2.8.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"react-query": "^3.39.2",
|
"eslint-plugin-react": "^7.31.11",
|
||||||
"ts-loader": "^9.4.1",
|
"ethers": "^5.7.2",
|
||||||
"typescript": "^4.9.3",
|
"prettier": "^2.8.0",
|
||||||
"vite": "^3.2.4",
|
"react-query": "^3.39.2",
|
||||||
"vite-tsconfig-paths": "^3.6.0"
|
"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 { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
|
||||||
|
import { WalletButton } from './components';
|
||||||
|
import { initializeWallet } from './store';
|
||||||
import { Home, MintSite, MintedSiteDetail } from './views';
|
import { Home, MintSite, MintedSiteDetail } from './views';
|
||||||
|
|
||||||
|
initializeWallet();
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<>
|
||||||
<Routes>
|
<WalletButton />
|
||||||
<Route path="/mint-site" element={<MintSite />} />
|
<BrowserRouter>
|
||||||
<Route path="/home" element={<Home />} />
|
<Routes>
|
||||||
<Route path="/detail" element={<MintedSiteDetail />} />
|
<Route path="/mint-site" element={<MintSite />} />
|
||||||
<Route path="*" element={<Navigate to="/home" />} />
|
<Route path="/home" element={<Home />} />
|
||||||
</Routes>
|
<Route path="/detail" element={<MintedSiteDetail />} />
|
||||||
</BrowserRouter>
|
<Route path="*" element={<Navigate to="/home" />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,11 @@ import {
|
||||||
Stack,
|
Stack,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { FleekERC721 } from '@/integrations';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
interface CardSiteProps {
|
interface CardSiteProps {
|
||||||
site: SiteNFTDetail;
|
tokenId: number;
|
||||||
tokenId?: string; // TODO add param and remove optional
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type InfoContainerProps = {
|
type InfoContainerProps = {
|
||||||
|
|
@ -35,9 +36,15 @@ const InfoContainer = ({ heading, info, width }: InfoContainerProps) => (
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SiteCard: React.FC<CardSiteProps> = ({ site, tokenId }) => {
|
export const SiteCard: React.FC<CardSiteProps> = ({ tokenId }) => {
|
||||||
const { name, owner, image, externalUrl } = site;
|
|
||||||
const navigate = useNavigate();
|
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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
borderColor="#f3f3f36b !important"
|
borderColor="#f3f3f36b !important"
|
||||||
|
|
@ -47,10 +54,7 @@ export const SiteCard: React.FC<CardSiteProps> = ({ site, tokenId }) => {
|
||||||
borderRadius="10px"
|
borderRadius="10px"
|
||||||
width="350px"
|
width="350px"
|
||||||
height="350px"
|
height="350px"
|
||||||
// TODO add token id param
|
onClick={() => navigate(`/detail?tokenId=${tokenId}`)}
|
||||||
onClick={() => {
|
|
||||||
navigate(`/detail?tokenId=${1}`);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CardBody width="350px" height="350px" paddingTop="10px">
|
<CardBody width="350px" height="350px" paddingTop="10px">
|
||||||
<Heading size="md" textAlign="center" marginBottom="10px">
|
<Heading size="md" textAlign="center" marginBottom="10px">
|
||||||
|
|
@ -78,8 +82,7 @@ export const SiteCard: React.FC<CardSiteProps> = ({ site, tokenId }) => {
|
||||||
</Link>
|
</Link>
|
||||||
<Stack mt="10px" spacing="3" overflowY="scroll">
|
<Stack mt="10px" spacing="3" overflowY="scroll">
|
||||||
<InfoContainer heading="Owner" info={owner} width="auto" />
|
<InfoContainer heading="Owner" info={owner} width="auto" />
|
||||||
{/* TODO add param */}
|
<InfoContainer heading="Token ID" info={tokenId} width="100px" />
|
||||||
<InfoContainer heading="Token ID" info="1" width="100px" />
|
|
||||||
<InfoContainer
|
<InfoContainer
|
||||||
heading="External url"
|
heading="External url"
|
||||||
width="100px"
|
width="100px"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
export * from './loading';
|
export * from './wallet-button';
|
||||||
export * from './home-button';
|
export * from './loading';
|
||||||
export * from './image-preview';
|
export * from './home-button';
|
||||||
export * from './tile-info';
|
export * from './image-preview';
|
||||||
export * from './card';
|
export * from './tile-info';
|
||||||
export * from './accordion-item';
|
export * from './card';
|
||||||
export * from './input-field-form';
|
export * from './accordion-item';
|
||||||
export * from './attributes-detail';
|
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 { App } from './app';
|
||||||
import { ChakraProvider } from '@chakra-ui/react';
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
import { theme } from './theme';
|
import { theme } from './theme';
|
||||||
|
import { Provider as ReduxProvider } from 'react-redux';
|
||||||
|
import { store } from './store';
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
@ -13,11 +15,13 @@ const root = ReactDOM.createRoot(
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ChakraProvider theme={theme} resetCSS>
|
<ReduxProvider store={store}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<ChakraProvider theme={theme} resetCSS>
|
||||||
<App />
|
<QueryClientProvider client={queryClient}>
|
||||||
</QueryClientProvider>
|
<App />
|
||||||
</ChakraProvider>
|
</QueryClientProvider>
|
||||||
|
</ChakraProvider>
|
||||||
|
</ReduxProvider>
|
||||||
</React.StrictMode>
|
</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) => {
|
export const getRepoAndCommit = (url: string) => {
|
||||||
//TODO validate is a github url
|
//TODO validate is a github url
|
||||||
url = url.replace('/commit', '');
|
url = url.replace('/commit', '');
|
||||||
const lastIndexSlash = url.lastIndexOf('/');
|
const lastIndexSlash = url.lastIndexOf('/');
|
||||||
const repo = url.substring(0, lastIndexSlash + 1).slice(0, lastIndexSlash);
|
const repo = url.substring(0, lastIndexSlash + 1).slice(0, lastIndexSlash);
|
||||||
const commit_hash = url.substring(lastIndexSlash + 1, url.length);
|
const commit_hash = url.substring(lastIndexSlash + 1, url.length);
|
||||||
return { repo, commit_hash };
|
return { repo, commit_hash };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const contractAddress = (address: string): string => {
|
||||||
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,17 @@ import {
|
||||||
AttributesDetail,
|
AttributesDetail,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||||
import { fetchSiteDetail } from '@/mocks';
|
|
||||||
import { ErrorScreen } from '@/views';
|
import { ErrorScreen } from '@/views';
|
||||||
import { SiteNFTDetail } from '@/types';
|
import { SiteNFTDetail } from '@/types';
|
||||||
|
import { FleekERC721 } from '@/integrations';
|
||||||
|
|
||||||
export const MintedSiteDetail = () => {
|
export const MintedSiteDetail = () => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const tokenIdParam = searchParams.get('tokenId');
|
const tokenIdParam = searchParams.get('tokenId');
|
||||||
//TODO handle response type
|
//TODO handle response type
|
||||||
const { data, status } = useQuery('fetchDetail', () =>
|
const { data, status } = useQuery<SiteNFTDetail>(
|
||||||
fetchSiteDetail(tokenIdParam as string)
|
`fetchDetail${tokenIdParam}`,
|
||||||
|
async () => FleekERC721.tokenMetadata(Number(tokenIdParam))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
|
|
@ -39,7 +40,8 @@ export const MintedSiteDetail = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { owner, name, description, image, externalUrl, attributes } =
|
const { owner, name, description, image, externalUrl, attributes } =
|
||||||
data.data as SiteNFTDetail;
|
data as SiteNFTDetail;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex width="full" align="center" justifyContent="center">
|
<Flex width="full" align="center" justifyContent="center">
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,22 @@
|
||||||
import React from 'react';
|
|
||||||
import { Loading } from '@/components';
|
import { Loading } from '@/components';
|
||||||
import { fetchMintedSites } from '@/mocks';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { SiteNFTDetails } from '@/types';
|
|
||||||
import { Grid, GridItem } from '@chakra-ui/react';
|
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { SiteCard } from '@/components';
|
import { SiteCard } from '@/components';
|
||||||
|
import { FleekERC721 } from '@/integrations';
|
||||||
|
|
||||||
export const ListSites = () => {
|
export const ListSites = () => {
|
||||||
const { data, isLoading } = useQuery<Array<SiteNFTDetails>, Error>(
|
const { data, isLoading } = useQuery('fetchLastTokenId', () =>
|
||||||
'fetchSites',
|
FleekERC721.lastTokenId()
|
||||||
fetchMintedSites
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) return <Loading />;
|
if (!data || isLoading) return <Loading />;
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Flex gap={10} mt="40px" flexWrap="wrap" justifyContent="center">
|
||||||
templateColumns={{ base: 'repeat(4, 1fr)', md: 'repeat(5, 1fr)' }}
|
{new Array(data).fill(0).map((_, index) => {
|
||||||
gap={10}
|
const id = data - index - 1;
|
||||||
mt="40px"
|
return <SiteCard tokenId={id} />;
|
||||||
>
|
})}
|
||||||
{data &&
|
</Flex>
|
||||||
data.listSites.map((site: SiteNFTDetails) => (
|
|
||||||
<GridItem key={site.tokenId}>
|
|
||||||
<SiteCard site={site} />
|
|
||||||
</GridItem>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,11 @@ import {
|
||||||
import { Formik, Field } from 'formik';
|
import { Formik, Field } from 'formik';
|
||||||
import { ArrowBackIcon } from '@chakra-ui/icons';
|
import { ArrowBackIcon } from '@chakra-ui/icons';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { mintSiteNFT } from '@/mocks';
|
|
||||||
import { getRepoAndCommit } from '@/utils';
|
import { getRepoAndCommit } from '@/utils';
|
||||||
import { validateFields } from './mint-site.utils';
|
import { validateFields } from './mint-site.utils';
|
||||||
import { InputFieldForm } from '@/components';
|
import { InputFieldForm } from '@/components';
|
||||||
|
import { FleekERC721 } from '@/integrations';
|
||||||
|
import { useWalletStore } from '@/store';
|
||||||
import { useToast } from '@/hooks';
|
import { useToast } from '@/hooks';
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
|
|
@ -43,46 +44,53 @@ const initialValues = {
|
||||||
|
|
||||||
export const MintSite = () => {
|
export const MintSite = () => {
|
||||||
const setToastInfo = useToast();
|
const setToastInfo = useToast();
|
||||||
|
const { provider } = useWalletStore();
|
||||||
|
|
||||||
const handleSubmitForm = useCallback(async (values: FormValues) => {
|
const handleSubmitForm = useCallback(
|
||||||
const {
|
async (values: FormValues) => {
|
||||||
name,
|
const {
|
||||||
description,
|
|
||||||
githubCommit,
|
|
||||||
ownerAddress,
|
|
||||||
externalUrl,
|
|
||||||
image,
|
|
||||||
ens,
|
|
||||||
} = values;
|
|
||||||
|
|
||||||
const { repo, commit_hash } = getRepoAndCommit(githubCommit);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await mintSiteNFT({
|
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
owner: ownerAddress,
|
githubCommit,
|
||||||
|
ownerAddress,
|
||||||
externalUrl,
|
externalUrl,
|
||||||
image,
|
image,
|
||||||
ens,
|
ens,
|
||||||
commitHash: commit_hash,
|
} = values;
|
||||||
repo,
|
|
||||||
});
|
const { repo, commit_hash } = getRepoAndCommit(githubCommit);
|
||||||
//TODO connect with the integration
|
|
||||||
setToastInfo({
|
try {
|
||||||
title: 'Success!',
|
if (!provider) throw new Error('No provider found');
|
||||||
description: 'Your site has been minted.',
|
await FleekERC721.mint(
|
||||||
status: 'success',
|
{
|
||||||
});
|
name,
|
||||||
} catch (err) {
|
description,
|
||||||
setToastInfo({
|
owner: ownerAddress,
|
||||||
title: 'Error!',
|
externalUrl,
|
||||||
description:
|
image,
|
||||||
'We had an error while minting your site. Please try again later',
|
ens,
|
||||||
status: 'error',
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -198,7 +206,8 @@ export const MintSite = () => {
|
||||||
!values.githubCommit ||
|
!values.githubCommit ||
|
||||||
!values.ownerAddress ||
|
!values.ownerAddress ||
|
||||||
!values.image ||
|
!values.image ||
|
||||||
!values.externalUrl
|
!values.externalUrl ||
|
||||||
|
!provider
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Mint
|
Mint
|
||||||
|
|
|
||||||
748
ui/yarn.lock
748
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue