Add disabled chain list

This commit is contained in:
HeesungB 2022-12-15 19:06:44 +09:00
parent 7de3767d03
commit bb58fb54ea
13 changed files with 221 additions and 200 deletions

View File

@ -1,20 +1,16 @@
import { ChainItemType } from "../../types"; import { ChainItemType } from "../../types";
import { Dispatch, FunctionComponent, SetStateAction } from "react";
import { ChainImage } from "./chain-image";
import { Flex1 } from "../../styles/flex-1";
import { import {
ChangeEvent, ChainCheckBox,
Dispatch,
FunctionComponent,
SetStateAction,
useEffect,
useState,
} from "react";
import {
ChainImageContainer, ChainImageContainer,
ChainInfoContainer, ChainInfoContainer,
ChainItemContainer, ChainItemContainer,
} from "./chain-list"; ChainName,
import { ChainImage } from "./chain-image"; WalletAddress,
import { Flex1 } from "../../styles/flex-1"; } from "./chain-item";
import { ChainCheckBox, ChainName, WalletAddress } from "./chain-item";
import color from "../../styles/color"; import color from "../../styles/color";
import styled from "styled-components"; import styled from "styled-components";

View File

@ -1,17 +1,5 @@
import { ChainItemType } from "../../types"; import { ChainItemType, WidthHeightProps } from "../../types";
import { import { FunctionComponent, useEffect, useState } from "react";
ChangeEvent,
Dispatch,
FunctionComponent,
SetStateAction,
useEffect,
useState,
} from "react";
import {
ChainImageContainer,
ChainInfoContainer,
ChainItemContainer,
} from "./chain-list";
import color from "../../styles/color"; import color from "../../styles/color";
import { Flex1 } from "../../styles/flex-1"; import { Flex1 } from "../../styles/flex-1";
@ -22,26 +10,31 @@ interface Props {
chainItem: ChainItemType; chainItem: ChainItemType;
checkedItemHandler: (chainItem: ChainItemType, isChecked: boolean) => void; checkedItemHandler: (chainItem: ChainItemType, isChecked: boolean) => void;
checkedItems: Set<unknown>; checkedItems: Set<unknown>;
disabled?: boolean;
} }
export const ChainItem: FunctionComponent<Props> = (props) => { export const ChainItem: FunctionComponent<Props> = (props) => {
const { chainItem, checkedItemHandler, checkedItems } = props; const { chainItem, checkedItemHandler, checkedItems, disabled } = props;
const [checked, setChecked] = useState(false); const [checked, setChecked] = useState(disabled);
const checkHandler = () => { const checkHandler = () => {
setChecked(!checked); if (!disabled) {
checkedItemHandler(chainItem, !checked); setChecked(!checked);
checkedItemHandler(chainItem, !checked);
}
}; };
useEffect(() => { useEffect(() => {
setChecked(checkedItems.has(chainItem)); if (!disabled) {
setChecked(checkedItems.has(chainItem));
}
}, [checkedItems]); }, [checkedItems]);
return ( return (
<ChainItemContainer <ChainItemContainer
key={chainItem.prefix} key={chainItem.prefix}
isLoading={false} isLoading={false}
checked={checked} disabled={disabled}
onClick={checkHandler} onClick={checkHandler}
> >
<ChainImageContainer width="3rem" height="3rem"> <ChainImageContainer width="3rem" height="3rem">
@ -63,6 +56,41 @@ export const ChainItem: FunctionComponent<Props> = (props) => {
); );
}; };
export const ChainItemContainer = styled.div<{
isLoading: boolean;
checked?: boolean;
disabled?: boolean;
}>`
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
padding: 1.5rem;
cursor: pointer;
opacity: ${(props) => (props.disabled ? "0.3" : "1")};
&:hover {
background: ${(props) => (props.isLoading ? null : color.grey["600"])};
}
`;
export const ChainImageContainer = styled.div<WidthHeightProps>`
width: ${(props) => props.width};
height: ${(props) => props.height};
position: relative;
`;
export const ChainInfoContainer = styled.div`
display: flex;
flex-direction: column;
gap: 0.5rem;
`;
export const ChainName = styled.div` export const ChainName = styled.div`
font-weight: 600; font-weight: 600;
font-size: 0.8rem; font-size: 0.8rem;

View File

@ -1,5 +1,5 @@
import { Dispatch, FunctionComponent, SetStateAction, useEffect } from "react"; import { Dispatch, FunctionComponent, SetStateAction, useEffect } from "react";
import { ChainItemType, WidthHeightProps } from "../../types"; import { ChainItemType } from "../../types";
import color from "../../styles/color"; import color from "../../styles/color";
import styled from "styled-components"; import styled from "styled-components";
import { ChainItem } from "./chain-item"; import { ChainItem } from "./chain-item";
@ -8,6 +8,7 @@ interface Props {
allChecked: boolean; allChecked: boolean;
setAllChecked: Dispatch<SetStateAction<boolean>>; setAllChecked: Dispatch<SetStateAction<boolean>>;
chainList: ChainItemType[]; chainList: ChainItemType[];
disabledChainList: ChainItemType[];
checkedItems: Set<unknown>; checkedItems: Set<unknown>;
setCheckedItems: Dispatch<SetStateAction<Set<unknown>>>; setCheckedItems: Dispatch<SetStateAction<Set<unknown>>>;
} }
@ -17,6 +18,7 @@ export const ChainList: FunctionComponent<Props> = (props) => {
allChecked, allChecked,
setAllChecked, setAllChecked,
chainList, chainList,
disabledChainList,
checkedItems, checkedItems,
setCheckedItems, setCheckedItems,
} = props; } = props;
@ -59,6 +61,15 @@ export const ChainList: FunctionComponent<Props> = (props) => {
checkedItems={checkedItems} checkedItems={checkedItems}
/> />
))} ))}
{disabledChainList.map((chainItem) => (
<ChainItem
key={chainItem.address}
chainItem={chainItem}
checkedItemHandler={checkedItemHandler}
checkedItems={checkedItems}
disabled={true}
/>
))}
</ChainContainer> </ChainContainer>
); );
}; };
@ -72,35 +83,3 @@ export const ChainContainer = styled.div`
background-color: ${(props) => props.color}; background-color: ${(props) => props.color};
`; `;
export const ChainItemContainer = styled.div<{
isLoading: boolean;
checked?: boolean;
}>`
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
padding: 1.5rem;
cursor: pointer;
&:hover {
background: ${(props) => (props.isLoading ? null : color.grey["700"])};
}
`;
export const ChainImageContainer = styled.div<WidthHeightProps>`
width: ${(props) => props.width};
height: ${(props) => props.height};
position: relative;
`;
export const ChainInfoContainer = styled.div`
display: flex;
flex-direction: column;
gap: 0.5rem;
`;

View File

@ -10,12 +10,13 @@ import {
WalletType, WalletType,
} from "../../constants/wallet"; } from "../../constants/wallet";
import { getKeplrFromWindow, KeplrWallet } from "../../wallets"; import { getKeplrFromWindow, KeplrWallet } from "../../wallets";
import { loginWithTwitter } from "../../repository"; import { loginWithTwitter } from "../../queries";
interface Props { interface Props {
wallet: WalletType; wallet: WalletType;
} }
// Todo: Wallet 관련된 부분을 Context로 빼는 부분
export const WalletItem: FunctionComponent<Props> = (props: Props) => { export const WalletItem: FunctionComponent<Props> = (props: Props) => {
const { wallet } = props; const { wallet } = props;

View File

@ -3,6 +3,7 @@ export const MainChainId = "osmo-test-4";
export const RPC_URL = "https://rpc.testnet.osmosis.zone"; export const RPC_URL = "https://rpc.testnet.osmosis.zone";
export const REST_URL = "https://lcd.testnet.osmosis.zone"; export const REST_URL = "https://lcd.testnet.osmosis.zone";
// TODO: .evn에 없으면 디폴트값 설정
export const REGISTRAR_ADDRESS = export const REGISTRAR_ADDRESS =
"osmo1npn97g7hsgqlp70rw8nhd7c7vyvkukv9x0n25sn4fk5mgcjlz4gq9zlgf3"; "osmo1npn97g7hsgqlp70rw8nhd7c7vyvkukv9x0n25sn4fk5mgcjlz4gq9zlgf3";
export const RESOLVER_ADDRESS = export const RESOLVER_ADDRESS =

View File

@ -1,51 +1,8 @@
import { request } from "../utils/url"; import { CosmwasmExecuteMessageResult } from "../types";
import {
REGISTRAR_ADDRESS,
RESOLVER_ADDRESS,
REST_URL,
} from "../constants/icns";
import { Buffer } from "buffer/";
import {
AddressesQueryResponse,
CosmwasmExecuteMessageResult,
NameByTwitterIdQueryResponse,
QueryError,
} from "../types";
import { makeCosmwasmExecMsg } from "../wallets"; import { makeCosmwasmExecMsg } from "../wallets";
import { REGISTRAR_ADDRESS, RESOLVER_ADDRESS } from "../constants/icns";
import { ContractFee } from "../constants/wallet"; import { ContractFee } from "../constants/wallet";
const getCosmwasmQueryUrl = (contractAddress: string, queryMsg: string) =>
`${REST_URL}/cosmwasm/wasm/v1/contract/${contractAddress}/smart/${queryMsg}`;
export const queryRegisteredTwitterId = async (
twitterId: string,
): Promise<NameByTwitterIdQueryResponse | QueryError> => {
const msg = {
name_by_twitter_id: { twitter_id: twitterId },
};
return request<NameByTwitterIdQueryResponse>(
getCosmwasmQueryUrl(
REGISTRAR_ADDRESS,
Buffer.from(JSON.stringify(msg)).toString("base64"),
),
);
};
export const queryAddressesFromTwitterName = async (
twitterUsername: string,
): Promise<AddressesQueryResponse> => {
const msg = {
addresses: { name: twitterUsername },
};
return request<AddressesQueryResponse>(
getCosmwasmQueryUrl(
RESOLVER_ADDRESS,
Buffer.from(JSON.stringify(msg)).toString("base64"),
),
);
};
export const makeClaimMessage = ( export const makeClaimMessage = (
senderAddress: string, senderAddress: string,
twitterUserName: string, twitterUserName: string,

1
messages/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./icns";

View File

@ -32,6 +32,8 @@ export default function CompletePage() {
const result = await txTracer.traceTx(Buffer.from(txHash, "hex")); const result = await txTracer.traceTx(Buffer.from(txHash, "hex"));
console.log(result); console.log(result);
// Todo rsult => 확인 후에 확인
}; };
return ( return (

View File

@ -2,8 +2,12 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
// Types // Types
import { ChainItemType, TwitterProfileType } from "../../types"; import {
import { checkTwitterAuthQueryParameter, request } from "../../utils/url"; ChainItemType,
RegisteredAddresses,
TwitterProfileType,
} from "../../types";
import { checkTwitterAuthQueryParameter } from "../../utils/url";
// Styles // Styles
import styled from "styled-components"; import styled from "styled-components";
@ -20,7 +24,6 @@ import { useRouter } from "next/router";
import { import {
getKeplrFromWindow, getKeplrFromWindow,
KeplrWallet, KeplrWallet,
makeCosmwasmExecMsg,
sendMsgs, sendMsgs,
simulateMsgs, simulateMsgs,
} from "../../wallets"; } from "../../wallets";
@ -29,26 +32,20 @@ import { ChainIdHelper } from "@keplr-wallet/cosmos";
import AllChainsIcon from "../../public/images/svg/all-chains-icon.svg"; import AllChainsIcon from "../../public/images/svg/all-chains-icon.svg";
import { AllChainsItem } from "../../components/chain-list/all-chains-item"; import { AllChainsItem } from "../../components/chain-list/all-chains-item";
import { SearchInput } from "../../components/search-input"; import { SearchInput } from "../../components/search-input";
import { import { MainChainId, RESOLVER_ADDRESS, REST_URL } from "../../constants/icns";
MainChainId,
REGISTRAR_ADDRESS,
RESOLVER_ADDRESS,
REST_URL,
} from "../../constants/icns";
import { import {
fetchTwitterInfo, fetchTwitterInfo,
makeClaimMessage,
makeSetRecordMessage,
queryAddressesFromTwitterName, queryAddressesFromTwitterName,
queryRegisteredTwitterId, queryRegisteredTwitterId,
verifyTwitterAccount, verifyTwitterAccount,
} from "../../repository"; } from "../../queries";
import { ErrorHandler } from "../../utils/error"; import { ErrorHandler } from "../../utils/error";
import { import {
KEPLR_NOT_FOUND_ERROR, KEPLR_NOT_FOUND_ERROR,
TWITTER_LOGIN_ERROR, TWITTER_LOGIN_ERROR,
} from "../../constants/error-message"; } from "../../constants/error-message";
import { makeClaimMessage, makeSetRecordMessage } from "../../messages";
export default function VerificationPage() { export default function VerificationPage() {
const router = useRouter(); const router = useRouter();
@ -57,13 +54,19 @@ export default function VerificationPage() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [wallet, setWallet] = useState<KeplrWallet>(); const [wallet, setWallet] = useState<KeplrWallet>();
const [allChains, setAllChains] = useState<ChainItemType>();
const [chainList, setChainList] = useState<ChainItemType[]>([]); const [chainList, setChainList] = useState<ChainItemType[]>([]);
const [registeredAddressList, setRegisteredAddressList] = useState<string[]>( const [disabledChainList, setDisabledChainList] = useState<ChainItemType[]>(
[], [],
); );
const [checkedItems, setCheckedItems] = useState(new Set()); const [registeredChainList, setRegisteredChainList] = useState<
RegisteredAddresses[]
>([]);
const [allChains, setAllChains] = useState<ChainItemType>();
const [allChecked, setAllChecked] = useState(false); const [allChecked, setAllChecked] = useState(false);
const [checkedItems, setCheckedItems] = useState(new Set());
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
useEffect(() => { useEffect(() => {
@ -80,7 +83,7 @@ export default function VerificationPage() {
// Fetch Twitter Profile // Fetch Twitter Profile
const twitterInfo = await fetchTwitterInfo(state, code); const twitterInfo = await fetchTwitterInfo(state, code);
// check registered // contract check registered
const registeredQueryResponse = await queryRegisteredTwitterId( const registeredQueryResponse = await queryRegisteredTwitterId(
twitterInfo.id, twitterInfo.id,
); );
@ -95,11 +98,7 @@ export default function VerificationPage() {
registeredQueryResponse.data.name, registeredQueryResponse.data.name,
); );
setRegisteredAddressList( setRegisteredChainList(addressesQueryResponse.data.addresses);
addressesQueryResponse.data.addresses.map(
(address) => address.address,
),
);
} }
} catch (error) { } catch (error) {
if (error instanceof Error && error.message === TWITTER_LOGIN_ERROR) { if (error instanceof Error && error.message === TWITTER_LOGIN_ERROR) {
@ -117,89 +116,96 @@ export default function VerificationPage() {
}, []); }, []);
useEffect(() => { useEffect(() => {
// After Wallet Initialize const disabledChainList = chainList.filter((chain) => {
if (wallet) { for (const registeredChain of registeredChainList) {
fetchChainList(); if (
} chain.prefix === registeredChain.bech32_prefix &&
}, [wallet]); chain.address === registeredChain.address
) {
return true;
}
}
useEffect(() => { return false;
// To check registered chain
const filteredChainList = chainList.filter((chain) => {
return registeredAddressList.includes(chain.address);
}); });
setCheckedItems(new Set(filteredChainList)); const filteredChainList = chainList.filter(
}, [registeredAddressList]); (chain) => !disabledChainList.includes(chain),
);
setAllChains({
chainId: "all chains",
chainName: "all chains",
prefix: `all chains(${filteredChainList.length})`,
address: filteredChainList.map((chain) => chain.chainName).join(", "),
chainImageUrl: AllChainsIcon,
});
setChainList(filteredChainList);
setDisabledChainList(disabledChainList);
}, [registeredChainList]);
const initWallet = async () => { const initWallet = async () => {
const keplr = await getKeplrFromWindow(); const keplr = await getKeplrFromWindow();
if (keplr) { if (keplr) {
const keplrWallet = new KeplrWallet(keplr); const keplrWallet = new KeplrWallet(keplr);
await fetchChainList(keplrWallet);
setWallet(keplrWallet); setWallet(keplrWallet);
} else { } else {
ErrorHandler(KEPLR_NOT_FOUND_ERROR); ErrorHandler(KEPLR_NOT_FOUND_ERROR);
} }
}; };
const fetchChainList = async () => { const fetchChainList = async (wallet: KeplrWallet) => {
if (wallet) { const chainIds = (await wallet.getChainInfosWithoutEndpoints()).map(
const chainIds = (await wallet.getChainInfosWithoutEndpoints()).map( (c) => c.chainId,
(c) => c.chainId, );
); const chainKeys = await Promise.all(
const chainKeys = await Promise.all( chainIds.map((chainId) => wallet.getKey(chainId)),
chainIds.map((chainId) => wallet.getKey(chainId)), );
);
const chainInfos = (await wallet.getChainInfosWithoutEndpoints()).map( const chainInfos = (await wallet.getChainInfosWithoutEndpoints()).map(
(chainInfo) => { (chainInfo) => {
return { return {
chainId: chainInfo.chainId, chainId: chainInfo.chainId,
chainName: chainInfo.chainName, chainName: chainInfo.chainName,
prefix: chainInfo.bech32Config.bech32PrefixAccAddr, prefix: chainInfo.bech32Config.bech32PrefixAccAddr,
chainImageUrl: `https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/${ chainImageUrl: `https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/${
ChainIdHelper.parse(chainInfo.chainId).identifier ChainIdHelper.parse(chainInfo.chainId).identifier
}/chain.png`, }/chain.png`,
}; };
}, },
); );
const chainArray = []; const chainArray = [];
for (let i = 0; i < chainKeys.length; i++) { for (let i = 0; i < chainKeys.length; i++) {
chainArray.push({ chainArray.push({
address: chainKeys[i].bech32Address, address: chainKeys[i].bech32Address,
...chainInfos[i], ...chainInfos[i],
});
}
// remove duplicated item
const filteredChainList = chainArray.filter((nextChain, index, self) => {
return (
index ===
self.findIndex((prevChain) => {
const isDuplicated = prevChain.prefix === nextChain.prefix;
if (isDuplicated && prevChain.chainName !== nextChain.chainName) {
console.log(
`${nextChain.chainName} has been deleted due to a duplicate name with ${prevChain.chainName}`,
);
}
return isDuplicated;
})
);
}); });
setAllChains({
chainId: "all chains",
prefix: `all chains(${filteredChainList.length})`,
address: chainInfos.map((chainInfo) => chainInfo.chainName).join(", "),
chainImageUrl: AllChainsIcon,
});
setChainList(filteredChainList);
} }
// remove duplicated item
const filteredChainList = chainArray.filter((nextChain, index, self) => {
return (
index ===
self.findIndex((prevChain) => {
const isDuplicated = prevChain.prefix === nextChain.prefix;
if (isDuplicated && prevChain.chainName !== nextChain.chainName) {
console.log(
`${nextChain.chainName} has been deleted due to a duplicate name with ${prevChain.chainName}`,
);
}
return isDuplicated;
})
);
});
setChainList(filteredChainList);
}; };
const checkAdr36 = async () => { const checkAdr36 = async () => {
@ -224,7 +230,6 @@ export default function VerificationPage() {
const { state, code } = checkTwitterAuthQueryParameter( const { state, code } = checkTwitterAuthQueryParameter(
window.location.search, window.location.search,
); );
const twitterInfo = await fetchTwitterInfo(state, code); const twitterInfo = await fetchTwitterInfo(state, code);
const adr36Infos = await checkAdr36(); const adr36Infos = await checkAdr36();
@ -336,6 +341,12 @@ export default function VerificationPage() {
chain.address.includes(searchValue) || chain.address.includes(searchValue) ||
chain.prefix.includes(searchValue), chain.prefix.includes(searchValue),
)} )}
disabledChainList={disabledChainList.filter(
(chain) =>
chain.chainId.includes(searchValue) ||
chain.address.includes(searchValue) ||
chain.prefix.includes(searchValue),
)}
checkedItems={checkedItems} checkedItems={checkedItems}
setCheckedItems={setCheckedItems} setCheckedItems={setCheckedItems}
/> />

44
queries/icns.ts Normal file
View File

@ -0,0 +1,44 @@
import { request } from "../utils/url";
import {
REGISTRAR_ADDRESS,
RESOLVER_ADDRESS,
REST_URL,
} from "../constants/icns";
import { Buffer } from "buffer/";
import {
AddressesQueryResponse,
NameByTwitterIdQueryResponse,
QueryError,
} from "../types";
const getCosmwasmQueryUrl = (contractAddress: string, queryMsg: string) =>
`${REST_URL}/cosmwasm/wasm/v1/contract/${contractAddress}/smart/${queryMsg}`;
export const queryRegisteredTwitterId = async (
twitterId: string,
): Promise<NameByTwitterIdQueryResponse | QueryError> => {
const msg = {
name_by_twitter_id: { twitter_id: twitterId },
};
return request<NameByTwitterIdQueryResponse>(
getCosmwasmQueryUrl(
REGISTRAR_ADDRESS,
Buffer.from(JSON.stringify(msg)).toString("base64"),
),
);
};
export const queryAddressesFromTwitterName = async (
twitterUsername: string,
): Promise<AddressesQueryResponse> => {
const msg = {
addresses: { name: twitterUsername },
};
return request<AddressesQueryResponse>(
getCosmwasmQueryUrl(
RESOLVER_ADDRESS,
Buffer.from(JSON.stringify(msg)).toString("base64"),
),
);
};

View File

@ -6,9 +6,9 @@ import {
import { request } from "../utils/url"; import { request } from "../utils/url";
export const loginWithTwitter = async () => { export const loginWithTwitter = async () => {
const { authUrl }: TwitterAuthUrlResponse = await ( const { authUrl } = await request<TwitterAuthUrlResponse>(
await fetch("/api/twitter-auth-url") "/api/twitter-auth-url",
).json(); );
window.location.href = authUrl; window.location.href = authUrl;
}; };

View File

@ -1,5 +1,6 @@
export interface ChainItemType { export interface ChainItemType {
chainId: string; chainId: string;
chainName: string;
prefix: string; prefix: string;
chainImageUrl: string; chainImageUrl: string;
address: string; address: string;