From 8a9cb3b75c4c670169fe375d10eb23ae34a89501 Mon Sep 17 00:00:00 2001 From: Rita Date: Mon, 25 Nov 2024 13:03:17 +0900 Subject: [PATCH] Update logics for linking mobile --- .../general/link-keplr-mobile/index.tsx | 303 ++++++------------ .../general/link-keplr-mobile/upload-to-s3.ts | 31 ++ 2 files changed, 133 insertions(+), 201 deletions(-) create mode 100644 apps/extension/src/pages/setting/general/link-keplr-mobile/upload-to-s3.ts diff --git a/apps/extension/src/pages/setting/general/link-keplr-mobile/index.tsx b/apps/extension/src/pages/setting/general/link-keplr-mobile/index.tsx index 98129b5d34..f474329c2e 100644 --- a/apps/extension/src/pages/setting/general/link-keplr-mobile/index.tsx +++ b/apps/extension/src/pages/setting/general/link-keplr-mobile/index.tsx @@ -23,48 +23,13 @@ import { BACKGROUND_PORT } from "@keplr-wallet/router"; // @ts-ignore import { QRCodeSVG } from "qrcode.react"; import { useStore } from "../../../../stores"; -import SignClient from "@walletconnect/sign-client"; -import { useNavigate } from "react-router"; import AES, { Counter } from "aes-js"; import { AddressBookData } from "../../../../stores/ui-config/address-book"; import { toJS } from "mobx"; import { Box } from "../../../../components/box"; import { Gutter } from "../../../../components/gutter"; import { FormattedMessage, useIntl } from "react-intl"; - -class MemoryKeyValueStorage { - protected readonly store: Map = new Map(); - - getEntries(): Promise<[string, T][]> { - const entries: [string, T][] = []; - for (const [key, value] of this.store.entries()) { - entries.push([key, JSON.parse(value)]); - } - return Promise.resolve(entries); - } - - getItem(key: string): Promise { - const value = this.store.get(key); - if (value != null) { - return Promise.resolve(JSON.parse(value)); - } - return Promise.resolve(undefined); - } - - getKeys(): Promise { - return Promise.resolve(Array.from(this.store.keys())); - } - - removeItem(key: string): Promise { - this.store.delete(key); - return Promise.resolve(undefined); - } - - setItem(key: string, value: T): Promise { - this.store.set(key, JSON.stringify(value)); - return Promise.resolve(undefined); - } -} +import { uploadToS3 } from "./upload-to-s3"; const Styles = { Container: styled(Stack)` @@ -264,11 +229,9 @@ const QRCodeView: FunctionComponent<{ }> = observer(({ keyRingVaults, cancel }) => { const { chainStore, uiConfigStore } = useStore(); - const navigate = useNavigate(); const confirm = useConfirm(); const intl = useIntl(); - const [signClient, setSignClient] = useState(); const [qrCodeData, setQRCodeData] = useState(); const cancelRef = useRef(cancel); @@ -304,156 +267,75 @@ const QRCodeView: FunctionComponent<{ }, [confirm, intl]); useEffect(() => { - (async () => { - const signClient = await SignClient.init({ - projectId: process.env["WC_PROJECT_ID"], - storage: new MemoryKeyValueStorage(), - }); + let intervalId: NodeJS.Timer | undefined; - setSignClient(signClient); - })(); - }, []); + try { + (async () => { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + const password = Buffer.from(bytes); - const topic = useRef(""); - useEffect(() => { - let intervalId: NodeJS.Timer | undefined; + const ivBytes = new Uint8Array(16); + crypto.getRandomValues(ivBytes); + const iv = Buffer.from(ivBytes); - if (signClient) { - try { - (async () => { - const { uri, approval } = await signClient.connect({ - requiredNamespaces: { - cosmos: { - methods: ["__keplr_export_keyring_vaults"], - chains: ["cosmos:cosmoshub-4"], - events: [], - }, - }, - }); - - if (uri) { - const bytes = new Uint8Array(32); - crypto.getRandomValues(bytes); - const password = Buffer.from(bytes); - - const ivBytes = new Uint8Array(16); - crypto.getRandomValues(ivBytes); - const iv = Buffer.from(ivBytes); - - const n = Math.floor(Math.random() * 10000); - const len = 5; - const data = JSON.stringify({ - wcURI: uri, - password: password.toString("hex"), - iv: iv.toString("hex"), - }); - const chunks: string[] = []; - for (let i = 0; i < len; i += 1) { - const chunk = - i === len - 1 - ? data.slice((data.length / len) * i) - : data.slice( - (data.length / len) * i, - (data.length / len) * (i + 1) - ); - chunks.push(chunk); - } - let i = 0; - const setQR = () => { - const _i = i % len; - const payload = { - t: "export", - n, - len, - v: "v2", - i: _i, - d: chunks[_i], - }; - - setQRCodeData(JSON.stringify(payload)); - - i++; - }; - - setQR(); - intervalId = setInterval(() => { - setQR(); - }, 1000); - - const counter = new Counter(0); - counter.setBytes(iv); - const aesCtr = new AES.ModeOfOperation.ctr(password, counter); - - // Await session approval from the wallet. - const session = await approval(); - topic.current = session.topic; - - await (async () => { - const addressBooks: { - [chainId: string]: AddressBookData[] | undefined; - } = {}; - - for (const chainInfo of chainStore.chainInfos) { - const addressBookData = - uiConfigStore.addressBookConfig.getAddressBook( - chainInfo.chainId - ); - - addressBooks[chainInfo.chainIdentifier] = toJS(addressBookData); - } + const counter = new Counter(0); + counter.setBytes(iv); + const aesCtr = new AES.ModeOfOperation.ctr(password, counter); - const enabledChainIdentifiers: Record< - string, - string[] | undefined - > = {}; - for (const vault of keyRingVaults) { - enabledChainIdentifiers[vault.id] = - await new InExtensionMessageRequester().sendMessage( - BACKGROUND_PORT, - new GetEnabledChainIdentifiersMsg(vault.id) - ); - } + const addressBooks: { + [chainId: string]: AddressBookData[] | undefined; + } = {}; + + for (const chainInfo of chainStore.chainInfos) { + const addressBookData = + uiConfigStore.addressBookConfig.getAddressBook(chainInfo.chainId); + + addressBooks[chainInfo.chainIdentifier] = toJS(addressBookData); + } + + const enabledChainIdentifiers: Record = + {}; + for (const vault of keyRingVaults) { + enabledChainIdentifiers[vault.id] = + await new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + new GetEnabledChainIdentifiersMsg(vault.id) + ); + } + + const buf = Buffer.from(JSON.stringify(keyRingVaults)); - const buf = Buffer.from(JSON.stringify(keyRingVaults)); - - const response: WCExportKeyRingDatasResponse = { - encrypted: { - ciphertext: Buffer.from(aesCtr.encrypt(buf)).toString("hex"), - }, - addressBooks, - enabledChainIdentifiers, - }; - - await signClient.request({ - topic: session.topic, - chainId: "cosmos:cosmoshub-4", - request: { - method: "__keplr_export_keyring_vaults", - params: [ - `0x${Buffer.from(JSON.stringify(response)).toString( - "hex" - )}`, - ], - }, - }); - - navigate("/"); - })(); + const keyringData: WCExportKeyRingDatasResponse = { + encrypted: { + ciphertext: Buffer.from(aesCtr.encrypt(buf)).toString("hex"), + }, + addressBooks, + enabledChainIdentifiers, + }; + + const uploadResult = await uploadToS3(JSON.stringify(keyringData)); + + const data = JSON.stringify({ + otp: uploadResult?.otp, + encryptionKey: password.toString("hex"), + iv: iv.toString("hex"), + }); + + intervalId = generateQRCodeDataByInterval(data, setQRCodeData); + })(); + } catch (e) { + confirm + .confirm( + "", + `Failed to create qr code data: ${e.message || e.toString()}`, + { + forceYes: true, } - })(); - } catch (e) { - confirm - .confirm( - "", - `Failed to create qr code data: ${e.message || e.toString()}`, - { - forceYes: true, - } - ) - .then(() => { - cancelRef.current(); - }); - } + ) + .then(() => { + cancelRef.current(); + }); } return () => { @@ -462,25 +344,7 @@ const QRCodeView: FunctionComponent<{ } }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [signClient]); - - useEffect(() => { - return () => { - if (signClient && topic.current) { - signClient - .disconnect({ - topic: topic.current, - reason: { - code: 1000, - message: "Unmounted", - }, - }) - .catch((e) => { - console.log(e); - }); - } - }; - }, [signClient]); + }, []); return ( ); }); + +function generateQRCodeDataByInterval( + data: string, + setQRCodeData: (data: string) => void +): NodeJS.Timer { + const n = Math.floor(Math.random() * 10000); + const len = 5; + const chunks: string[] = []; + for (let i = 0; i < len; i += 1) { + const chunk = + i === len - 1 + ? data.slice((data.length / len) * i) + : data.slice((data.length / len) * i, (data.length / len) * (i + 1)); + chunks.push(chunk); + } + let i = 0; + const setQR = () => { + const _i = i % len; + const payload = { + t: "export", + n, + len, + v: "v2", + i: _i, + d: chunks[_i], + }; + + setQRCodeData(JSON.stringify(payload)); + + i++; + }; + + setQR(); + return setInterval(() => { + setQR(); + }, 1000); +} diff --git a/apps/extension/src/pages/setting/general/link-keplr-mobile/upload-to-s3.ts b/apps/extension/src/pages/setting/general/link-keplr-mobile/upload-to-s3.ts new file mode 100644 index 0000000000..53f5232719 --- /dev/null +++ b/apps/extension/src/pages/setting/general/link-keplr-mobile/upload-to-s3.ts @@ -0,0 +1,31 @@ +import { simpleFetch, SimpleFetchResponse } from "@keplr-wallet/simple-fetch"; + +type Response = { + otp: string; +} | null; + +export async function uploadToS3( + encryptedData: string +): Promise["data"]> { + const postEndpoint = + process.env["KEPLR_EXT_LINK_MOBILE_POST_ENDPOINT"] ?? + "https://p56p7lfgkbcssbsroxjb6amexu0ybdxt.lambda-url.us-west-2.on.aws"; + + if (!postEndpoint) { + throw new Error("KEPLR_EXT_LINK_MOBILE_POST_ENDPOINT is not set"); + } + + const response = await simpleFetch(postEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ data: encryptedData }), + }); + + if (response.status !== 200) { + throw new Error("Failed to upload to S3"); + } + + return response.data; +}