icns-frontend/pages/verification/index.tsx

642 lines
17 KiB
TypeScript
Raw Normal View History

2022-12-17 14:45:50 +00:00
import * as amplitude from "@amplitude/analytics-browser";
2022-12-07 09:17:59 +00:00
// React
2022-12-01 08:33:51 +00:00
import { useEffect, useState } from "react";
2022-12-07 09:17:59 +00:00
// Types
2022-12-15 10:06:44 +00:00
import {
ChainItemType,
DisabledChainItemType,
2022-12-18 14:58:48 +00:00
ErrorMessage,
2022-12-15 15:43:18 +00:00
QueryError,
2022-12-15 10:06:44 +00:00
RegisteredAddresses,
TwitterProfileType,
} from "../../types";
import { checkTwitterAuthQueryParameter } from "../../utils/url";
2022-12-01 08:33:51 +00:00
2022-12-07 09:17:59 +00:00
// Styles
2022-12-07 13:55:22 +00:00
import styled from "styled-components";
2022-12-06 14:53:31 +00:00
import color from "../../styles/color";
2022-12-01 08:33:51 +00:00
2022-12-07 09:17:59 +00:00
// Components
import { Logo } from "../../components/logo";
2022-12-09 11:59:52 +00:00
import { SkeletonChainList } from "../../components/skeleton";
2022-12-07 09:17:59 +00:00
import { PrimaryButton } from "../../components/primary-button";
2022-12-12 07:10:11 +00:00
import { TwitterProfile } from "../../components/twitter-profile";
2022-12-09 11:59:52 +00:00
import { ChainList } from "../../components/chain-list";
import { useRouter } from "next/router";
2022-12-13 14:46:32 +00:00
import {
getKeplrFromWindow,
KeplrWallet,
2022-12-14 07:52:05 +00:00
sendMsgs,
2022-12-13 14:46:32 +00:00
simulateMsgs,
} from "../../wallets";
import { Bech32Address, ChainIdHelper } from "@keplr-wallet/cosmos";
2022-12-07 09:17:59 +00:00
import { AllChainsItem } from "../../components/chain-list/all-chains-item";
2022-12-13 13:10:31 +00:00
import { SearchInput } from "../../components/search-input";
2022-12-15 15:43:18 +00:00
import {
2022-12-20 07:25:16 +00:00
CHAIN_ALLOWLIST,
2022-12-19 13:53:13 +00:00
MAIN_CHAIN_ID,
2022-12-15 15:43:18 +00:00
REFERRAL_KEY,
RESOLVER_ADDRESS,
REST_URL,
} from "../../constants/icns";
2022-12-14 11:36:08 +00:00
import {
fetchTwitterInfo,
queryAddressesFromTwitterName,
2022-12-15 15:43:18 +00:00
queryOwnerOfTwitterName,
2022-12-14 11:36:08 +00:00
queryRegisteredTwitterId,
2022-12-15 06:02:50 +00:00
verifyTwitterAccount,
2022-12-15 10:06:44 +00:00
} from "../../queries";
2022-12-15 06:02:50 +00:00
import {
ACCOUNT_NOT_EXIST_ERROR,
ACCOUNT_NOT_EXIST_MESSAGE,
INSUFFICIENT_GAS_ERROR,
INSUFFICIENT_GAS_MESSAGE,
2022-12-15 06:02:50 +00:00
KEPLR_NOT_FOUND_ERROR,
TWITTER_LOGIN_ERROR,
VERIFICATION_THRESHOLD_ERROR,
VERIFICATION_THRESHOLD_MESSAGE,
2022-12-15 06:02:50 +00:00
} from "../../constants/error-message";
2022-12-15 10:06:44 +00:00
import { makeClaimMessage, makeSetRecordMessage } from "../../messages";
import Axios from "axios";
2022-12-15 15:43:18 +00:00
import { BackButton } from "../../components/back-button";
2022-12-17 16:51:29 +00:00
import { FinalCheckModal } from "../../components/final-check-modal";
2022-12-18 14:58:48 +00:00
import { ErrorModal } from "../../components/error-modal";
2022-12-14 07:52:05 +00:00
export default function VerificationPage() {
const router = useRouter();
2022-12-14 11:36:08 +00:00
const [twitterAuthInfo, setTwitterAuthInfo] = useState<TwitterProfileType>();
2022-12-01 08:33:51 +00:00
2022-12-18 09:38:56 +00:00
const [isLoadingInit, setIsLoadingInit] = useState(true);
const [isLoadingRegistration, setIsLoadingRegistration] = useState(false);
2022-12-07 09:17:59 +00:00
const [wallet, setWallet] = useState<KeplrWallet>();
2022-12-17 16:51:29 +00:00
const [walletKey, setWalletKey] = useState<{
name: string;
pubKey: Uint8Array;
bech32Address: string;
isLedgerNano?: boolean;
2022-12-17 16:51:29 +00:00
}>();
2022-12-15 10:06:44 +00:00
const [chainList, setChainList] = useState<
(ChainItemType & {
isEthermintLike?: boolean;
})[]
>([]);
const [disabledChainList, setDisabledChainList] = useState<
DisabledChainItemType[]
>([]);
2022-12-15 10:06:44 +00:00
const [registeredChainList, setRegisteredChainList] = useState<
RegisteredAddresses[]
>([]);
const [checkedItems, setCheckedItems] = useState(new Set());
2022-12-13 13:10:31 +00:00
const [searchValue, setSearchValue] = useState("");
const [nftOwnerAddress, setNFTOwnerAddress] = useState("");
2022-12-15 15:43:18 +00:00
const [isOwner, setIsOwner] = useState(false);
2022-12-17 16:51:29 +00:00
const [isModalOpen, setModalOpen] = useState(false);
2022-12-18 14:58:48 +00:00
const [isErrorModalOpen, setErrorModalOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState<ErrorMessage>();
2022-12-15 15:43:18 +00:00
useEffect(() => {
2022-12-14 07:52:05 +00:00
init();
2022-12-01 08:33:51 +00:00
}, []);
2022-12-16 15:52:37 +00:00
useEffect(() => {
if (wallet) {
window.addEventListener("keplr_keystorechange", async () => {
2022-12-17 13:10:38 +00:00
await init();
});
}
}, [wallet]);
2022-12-16 15:52:37 +00:00
useEffect(() => {
const disabledChainList = chainList
.filter((chain) => {
if (!chain.address) {
// Address can be "" if `getKey` failed.
2022-12-15 10:06:44 +00:00
return true;
}
for (const registeredChain of registeredChainList) {
if (
chain.prefix === registeredChain.bech32_prefix &&
chain.address === registeredChain.address
) {
return true;
}
}
2022-12-14 11:36:08 +00:00
return false;
})
.map<DisabledChainItemType>((chain) => {
if (walletKey) {
if (walletKey.isLedgerNano && chain.isEthermintLike) {
return {
...chain,
disabled: true,
reason: new Error(
"Support for Ethereum address on Ledger is coming soon.",
),
};
}
}
return {
...chain,
disabled: true,
};
});
const filteredChainList = chainList.filter((chain) => {
return (
disabledChainList.find(
(disabled) => disabled.chainId === chain.chainId,
) == null
);
});
2022-12-15 10:06:44 +00:00
setChainList(filteredChainList);
setDisabledChainList(disabledChainList);
2022-12-17 13:10:38 +00:00
setCheckedItems(new Set(filteredChainList));
2022-12-15 10:06:44 +00:00
}, [registeredChainList]);
2022-12-14 11:36:08 +00:00
2022-12-17 13:10:38 +00:00
useEffect(() => {
setCheckedItems(new Set(chainList));
}, [chainList]);
const init = async () => {
if (window.location.search) {
try {
const { state, code } = checkTwitterAuthQueryParameter(
window.location.search,
);
// Initialize Wallet
const keplrWallet = await initWallet();
// Fetch Twitter Profile
const twitterInfo = await fetchTwitterInfo(state, code);
// contract check registered
const registeredQueryResponse = await queryRegisteredTwitterId(
twitterInfo.id,
);
setTwitterAuthInfo({
...twitterInfo,
isRegistered: "data" in registeredQueryResponse,
});
if ("data" in registeredQueryResponse) {
const ownerOfQueryResponse = await queryOwnerOfTwitterName(
registeredQueryResponse.data.name,
);
if (keplrWallet) {
2022-12-19 13:53:13 +00:00
const key = await keplrWallet.getKey(MAIN_CHAIN_ID);
setIsOwner(ownerOfQueryResponse.data.owner === key.bech32Address);
setNFTOwnerAddress(ownerOfQueryResponse.data.owner);
}
2022-12-17 16:51:29 +00:00
const addressesQueryResponse = await queryAddressesFromTwitterName(
registeredQueryResponse.data.name,
);
setRegisteredChainList(addressesQueryResponse.data.addresses);
}
} catch (error) {
2022-12-18 14:58:48 +00:00
if (error instanceof Error) {
if (error.message === TWITTER_LOGIN_ERROR) {
setErrorMessage({ message: TWITTER_LOGIN_ERROR, path: "/" });
setErrorModalOpen(true);
}
}
console.error(error);
} finally {
2022-12-18 09:38:56 +00:00
setIsLoadingInit(false);
}
}
};
2022-12-15 06:02:50 +00:00
const initWallet = async () => {
const keplr = await getKeplrFromWindow();
if (keplr) {
const keplrWallet = new KeplrWallet(keplr);
2022-12-19 13:53:13 +00:00
const key = await keplrWallet.getKey(MAIN_CHAIN_ID);
2022-12-15 10:06:44 +00:00
await fetchChainList(keplrWallet);
2022-12-15 06:02:50 +00:00
setWallet(keplrWallet);
2022-12-17 16:51:29 +00:00
setWalletKey(key);
2022-12-15 15:43:18 +00:00
return keplrWallet;
2022-12-15 06:02:50 +00:00
} else {
2022-12-18 14:58:48 +00:00
setErrorMessage({ message: KEPLR_NOT_FOUND_ERROR, path: "/" });
setErrorModalOpen(true);
2022-12-15 06:02:50 +00:00
}
};
2022-12-15 10:06:44 +00:00
const fetchChainList = async (wallet: KeplrWallet) => {
const needAllowList =
2022-12-20 07:25:16 +00:00
CHAIN_ALLOWLIST != null && CHAIN_ALLOWLIST.trim().length !== 0;
const chainAllowList = (CHAIN_ALLOWLIST || "")
.split(",")
.map((str) => str.trim())
.filter((str) => str.length > 0)
.map((str) => ChainIdHelper.parse(str).identifier);
const chainAllowListMap = new Map<string, true | undefined>();
for (const allow of chainAllowList) {
chainAllowListMap.set(allow, true);
}
const chainInfos = (await wallet.getChainInfosWithoutEndpoints())
.filter((chainInfo) => {
if (!needAllowList) {
return true;
}
const chainIdentifier = ChainIdHelper.parse(
chainInfo.chainId,
).identifier;
return chainAllowListMap.get(chainIdentifier) === true;
})
.map((chainInfo) => {
2022-12-15 10:06:44 +00:00
return {
chainId: chainInfo.chainId,
chainName: chainInfo.chainName,
prefix: chainInfo.bech32Config.bech32PrefixAccAddr,
chainImageUrl: `https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/${
ChainIdHelper.parse(chainInfo.chainId).identifier
}/chain.png`,
isEthermintLike: chainInfo.isEthermintLike,
2022-12-15 10:06:44 +00:00
};
});
const chainIds = chainInfos.map((c) => c.chainId);
const chainKeys = await Promise.allSettled(
chainIds.map((chainId) => wallet.getKey(chainId)),
2022-12-15 10:06:44 +00:00
);
2022-12-15 10:06:44 +00:00
const chainArray = [];
for (let i = 0; i < chainKeys.length; i++) {
const chainKey = chainKeys[i];
if (chainKey.status !== "fulfilled") {
console.log("Failed to get key from wallet", chainKey);
}
2022-12-15 10:06:44 +00:00
chainArray.push({
address:
chainKey.status === "fulfilled" ? chainKey.value.bech32Address : "",
2022-12-15 10:06:44 +00:00
...chainInfos[i],
2022-12-14 07:52:05 +00:00
});
2022-12-15 10:06:44 +00:00
}
2022-12-15 10:06:44 +00:00
// remove duplicated item
const filteredChainList = chainArray.filter((nextChain, index, self) => {
return (
index ===
self.findIndex((prevChain) => {
const isDuplicated = prevChain.prefix === nextChain.prefix;
2022-12-15 10:06:44 +00:00
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);
};
2022-12-13 14:46:32 +00:00
const checkAdr36 = async () => {
if (twitterAuthInfo && wallet) {
2022-12-19 13:53:13 +00:00
const key = await wallet.getKey(MAIN_CHAIN_ID);
2022-12-14 15:39:51 +00:00
2022-12-14 11:36:08 +00:00
const chainIds = Array.from(checkedItems).map((chain) => {
return (chain as ChainItemType).chainId;
});
2022-12-14 15:39:51 +00:00
2022-12-15 06:02:50 +00:00
return wallet.signICNSAdr36(
2022-12-19 13:53:13 +00:00
MAIN_CHAIN_ID,
2022-12-13 14:46:32 +00:00
RESOLVER_ADDRESS,
key.bech32Address,
twitterAuthInfo.username,
chainIds,
);
}
};
2022-12-17 16:51:29 +00:00
const onClickRegistration = () => {
2022-12-17 14:45:50 +00:00
amplitude.track("click register button");
2022-12-17 16:51:29 +00:00
if (isOwner) {
handleRegistration();
} else {
setModalOpen(true);
}
};
const handleRegistration = async () => {
2022-12-15 15:43:18 +00:00
try {
2022-12-18 09:38:56 +00:00
setIsLoadingRegistration(true);
2022-12-15 15:43:18 +00:00
const { state, code } = checkTwitterAuthQueryParameter(
window.location.search,
);
const twitterInfo = await fetchTwitterInfo(state, code);
2022-12-15 15:43:18 +00:00
const adr36Infos = await checkAdr36();
2022-12-13 14:46:32 +00:00
2022-12-17 16:51:29 +00:00
if (wallet && walletKey && adr36Infos) {
2022-12-15 15:43:18 +00:00
const icnsVerificationList = await verifyTwitterAccount(
2022-12-17 16:51:29 +00:00
walletKey.bech32Address,
2022-12-15 15:43:18 +00:00
twitterInfo.accessToken,
);
2022-12-13 14:46:32 +00:00
2022-12-15 15:43:18 +00:00
const registerMsg = makeClaimMessage(
2022-12-17 16:51:29 +00:00
walletKey.bech32Address,
2022-12-15 06:02:50 +00:00
twitterInfo.username,
2022-12-15 15:43:18 +00:00
icnsVerificationList,
localStorage.getItem(REFERRAL_KEY) ?? undefined,
2022-12-13 14:46:32 +00:00
);
2022-12-14 11:36:08 +00:00
2022-12-15 15:43:18 +00:00
const addressMsgs = adr36Infos.map((adr36Info) => {
return makeSetRecordMessage(
2022-12-17 16:51:29 +00:00
walletKey.bech32Address,
2022-12-15 15:43:18 +00:00
twitterInfo.username,
adr36Info,
);
});
const aminoMsgs = twitterAuthInfo?.isRegistered
? []
: [registerMsg.amino];
const protoMsgs = twitterAuthInfo?.isRegistered
? []
: [registerMsg.proto];
for (const addressMsg of addressMsgs) {
aminoMsgs.push(addressMsg.amino);
protoMsgs.push(addressMsg.proto);
}
2022-12-13 14:46:32 +00:00
2022-12-15 15:43:18 +00:00
const chainInfo = {
2022-12-19 13:53:13 +00:00
chainId: MAIN_CHAIN_ID,
2022-12-15 15:43:18 +00:00
rest: REST_URL,
};
2022-12-13 14:46:32 +00:00
2022-12-15 15:43:18 +00:00
const simulated = await simulateMsgs(
chainInfo,
2022-12-17 16:51:29 +00:00
walletKey.bech32Address,
2022-12-15 15:43:18 +00:00
{
proto: protoMsgs,
},
{
amount: [],
},
);
2022-12-13 14:46:32 +00:00
2022-12-15 15:43:18 +00:00
const txHash = await sendMsgs(
wallet,
chainInfo,
2022-12-17 16:51:29 +00:00
walletKey.bech32Address,
2022-12-15 15:43:18 +00:00
{
amino: aminoMsgs,
proto: protoMsgs,
},
{
amount: [],
gas: Math.floor(simulated.gasUsed * 1.5).toString(),
},
);
2022-12-14 07:52:05 +00:00
2022-12-15 15:43:18 +00:00
await router.push({
pathname: "complete",
query: {
txHash: Buffer.from(txHash).toString("hex"),
twitterUsername: twitterInfo.username,
},
});
}
} catch (error) {
if (Axios.isAxiosError(error)) {
const message = (error?.response?.data as QueryError).message;
if (message.includes(INSUFFICIENT_GAS_ERROR)) {
setErrorMessage({ message: INSUFFICIENT_GAS_MESSAGE });
setErrorModalOpen(true);
return;
}
if (message.includes(ACCOUNT_NOT_EXIST_ERROR)) {
setErrorMessage({ message: ACCOUNT_NOT_EXIST_MESSAGE });
setErrorModalOpen(true);
return;
}
if (message.includes(VERIFICATION_THRESHOLD_ERROR)) {
setErrorMessage({ message: VERIFICATION_THRESHOLD_MESSAGE });
setErrorModalOpen(true);
return;
}
2022-12-18 14:58:48 +00:00
setErrorMessage({
message: (error?.response?.data as QueryError).message,
});
setErrorModalOpen(true);
return;
}
if (error instanceof Error) {
console.log(error.message);
2022-12-18 14:58:48 +00:00
setErrorMessage({ message: error.message });
setErrorModalOpen(true);
2022-12-15 15:43:18 +00:00
}
2022-12-18 09:38:56 +00:00
} finally {
setIsLoadingRegistration(false);
2022-12-13 14:46:32 +00:00
}
};
2022-12-17 13:23:22 +00:00
const isRegisterButtonDisable = (() => {
if (!isOwner && nftOwnerAddress) {
return true;
}
2022-12-17 13:23:22 +00:00
const hasCheckedItem = checkedItems.size > 0;
2022-12-17 16:51:29 +00:00
return !hasCheckedItem;
2022-12-17 13:23:22 +00:00
})();
2022-12-15 15:43:18 +00:00
2022-12-01 08:33:51 +00:00
return (
2022-12-06 14:53:31 +00:00
<Container>
<Logo />
2022-12-01 08:33:51 +00:00
2022-12-06 14:53:31 +00:00
<MainContainer>
2022-12-18 09:38:56 +00:00
{isLoadingInit ? (
2022-12-09 11:59:52 +00:00
<SkeletonChainList />
2022-12-07 09:17:59 +00:00
) : (
<ContentContainer>
2022-12-15 15:43:18 +00:00
<BackButton />
2022-12-09 11:59:52 +00:00
<TwitterProfile twitterProfileInformation={twitterAuthInfo} />
2022-12-07 09:17:59 +00:00
<ChainListTitleContainer>
<ChainListTitle>Chain List</ChainListTitle>
2022-12-13 13:10:31 +00:00
<SearchInput
searchValue={searchValue}
setSearchValue={setSearchValue}
2022-12-14 15:39:51 +00:00
/>
2022-12-07 09:17:59 +00:00
</ChainListTitleContainer>
2022-12-17 13:10:38 +00:00
{!searchValue ? (
<AllChainsItem
2022-12-17 13:10:38 +00:00
chainList={chainList}
checkedItems={checkedItems}
setCheckedItems={setCheckedItems}
/>
) : null}
<ChainList
2022-12-13 13:10:31 +00:00
chainList={chainList.filter(
(chain) =>
chain.chainId.includes(searchValue) ||
chain.address.includes(searchValue) ||
chain.prefix.includes(searchValue),
)}
2022-12-15 10:06:44 +00:00
disabledChainList={disabledChainList.filter(
(chain) =>
chain.chainId.includes(searchValue) ||
chain.address.includes(searchValue) ||
chain.prefix.includes(searchValue),
)}
checkedItems={checkedItems}
setCheckedItems={setCheckedItems}
/>
2022-12-07 09:17:59 +00:00
{!isOwner && nftOwnerAddress ? (
<OwnerAlert>
You are not owner of this name.
<br />
Please select the account (
{Bech32Address.shortenAddress(nftOwnerAddress, 28)})
</OwnerAlert>
) : null}
2022-12-17 13:23:22 +00:00
{chainList.length > 0 && (
<ButtonContainer disabled={isRegisterButtonDisable}>
<PrimaryButton
disabled={isRegisterButtonDisable}
onClick={onClickRegistration}
2022-12-18 09:38:56 +00:00
isLoading={isLoadingRegistration}
2022-12-17 13:23:22 +00:00
>
Register
</PrimaryButton>
</ButtonContainer>
)}
2022-12-07 09:17:59 +00:00
</ContentContainer>
)}
2022-12-06 14:53:31 +00:00
</MainContainer>
2022-12-17 16:51:29 +00:00
<FinalCheckModal
twitterUserName={twitterAuthInfo?.username}
walletInfo={walletKey}
isModalOpen={isModalOpen}
onCloseModal={() => setModalOpen(false)}
onClickRegisterButton={handleRegistration}
isLoadingRegistration={isLoadingRegistration}
2022-12-17 16:51:29 +00:00
/>
2022-12-18 14:58:48 +00:00
<ErrorModal
isModalOpen={isErrorModalOpen}
onCloseModal={() => setErrorModalOpen(false)}
errorMessage={errorMessage}
/>
2022-12-06 14:53:31 +00:00
</Container>
2022-12-01 08:33:51 +00:00
);
}
2022-12-07 13:55:22 +00:00
const Container = styled.div`
width: 100vw;
height: 100vh;
`;
const MainContainer = styled.div`
display: flex;
justify-content: center;
2022-12-15 15:43:18 +00:00
height: 100vh;
padding: 2.7rem 0;
2022-12-07 13:55:22 +00:00
color: white;
2022-12-17 09:21:36 +00:00
background-image: url("/images/svg/bg-asset-3.svg"),
url("/images/svg/bg-asset-3.svg"), url("/images/svg/bg-asset-3.svg"),
url("/images/svg/bg-asset-3.svg");
background-size: 3.125rem 3.125rem;
background-position: 296px 536px, 1256px 216px, 376px 776px, 1176px 856px;
background-repeat: no-repeat;
2022-12-07 13:55:22 +00:00
`;
2022-12-09 11:59:52 +00:00
export const ContentContainer = styled.div`
2022-12-07 13:55:22 +00:00
display: flex;
flex-direction: column;
align-items: center;
width: 40rem;
`;
2022-12-15 15:43:18 +00:00
export const ButtonContainer = styled.div<{ disabled?: boolean }>`
2022-12-16 06:55:31 +00:00
width: 11rem;
height: 3.5rem;
2022-12-07 13:55:22 +00:00
2022-12-17 16:51:29 +00:00
margin-top: 1.5rem;
2022-12-15 15:43:18 +00:00
background-color: ${(props) =>
props.disabled ? color.orange["300"] : color.orange["100"]};
2022-12-07 13:55:22 +00:00
`;
2022-12-09 11:59:52 +00:00
export const ChainListTitleContainer = styled.div`
2022-12-07 13:55:22 +00:00
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
margin-top: 2rem;
margin-bottom: 1rem;
`;
const ChainListTitle = styled.div`
font-weight: 700;
font-size: 1.5rem;
line-height: 1.9rem;
color: ${color.white};
`;
2022-12-15 15:43:18 +00:00
const OwnerAlert = styled.div`
font-family: "Inter", serif;
font-style: normal;
font-weight: 500;
font-size: 0.9rem;
line-height: 1.1rem;
text-align: center;
color: ${color.grey["400"]};
padding-top: 1.25rem;
`;