Skip to content

Commit

Permalink
Update logics for linking mobile
Browse files Browse the repository at this point in the history
  • Loading branch information
editaahn committed Nov 25, 2024
1 parent 1691ad3 commit 8a9cb3b
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 201 deletions.
303 changes: 102 additions & 201 deletions apps/extension/src/pages/setting/general/link-keplr-mobile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = new Map();

getEntries<T = any>(): 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<T = any>(key: string): Promise<T | undefined> {
const value = this.store.get(key);
if (value != null) {
return Promise.resolve(JSON.parse(value));
}
return Promise.resolve(undefined);
}

getKeys(): Promise<string[]> {
return Promise.resolve(Array.from(this.store.keys()));
}

removeItem(key: string): Promise<void> {
this.store.delete(key);
return Promise.resolve(undefined);
}

setItem<T = any>(key: string, value: T): Promise<void> {
this.store.set(key, JSON.stringify(value));
return Promise.resolve(undefined);
}
}
import { uploadToS3 } from "./upload-to-s3";

const Styles = {
Container: styled(Stack)`
Expand Down Expand Up @@ -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<SignClient | undefined>();
const [qrCodeData, setQRCodeData] = useState<string | undefined>();

const cancelRef = useRef(cancel);
Expand Down Expand Up @@ -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<string>("");
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<string, string[] | undefined> =
{};
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 () => {
Expand All @@ -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 (
<HeaderLayout
Expand Down Expand Up @@ -533,3 +397,40 @@ const QRCodeView: FunctionComponent<{
</HeaderLayout>
);
});

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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { simpleFetch, SimpleFetchResponse } from "@keplr-wallet/simple-fetch";

type Response = {
otp: string;
} | null;

export async function uploadToS3(
encryptedData: string
): Promise<SimpleFetchResponse<Response>["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<Response>(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;
}

0 comments on commit 8a9cb3b

Please # to comment.