@@ -244,6 +280,21 @@ export default function SignDataItem() {
showOptions={false}
back={cancel}
/>
+ {mismatch && transfer && (
+
+
+ {" "}
+
+
+
{browser.i18n.getMessage("mismatch_warning_title")}
+
+ {browser.i18n.getMessage("mismatch_warning")}
+
+ {/* Read more */}
+
+
+
+ )}
{browser.i18n.getMessage(
diff --git a/src/routes/auth/signMessage.tsx b/src/routes/auth/signKeystone.tsx
similarity index 68%
rename from src/routes/auth/signMessage.tsx
rename to src/routes/auth/signKeystone.tsx
index 6fa406f08..fdbb94e45 100644
--- a/src/routes/auth/signMessage.tsx
+++ b/src/routes/auth/signKeystone.tsx
@@ -1,15 +1,13 @@
import { replyToAuthRequest, useAuthParams, useAuthUtils } from "~utils/auth";
-import { decodeSignature, messageToUR } from "~wallets/hardware/keystone";
+import {
+ dataItemToUR,
+ decodeSignature,
+ messageToUR
+} from "~wallets/hardware/keystone";
import { useEffect, useState } from "react";
import { useScanner } from "@arconnect/keystone-sdk";
import { useActiveWallet } from "~wallets/hooks";
import type { UR } from "@ngraveio/bc-ur";
-import {
- Properties,
- PropertyName,
- PropertyValue,
- TransactionProperty
-} from "~routes/popup/transaction/[id]";
import {
ButtonV2,
Section,
@@ -24,31 +22,69 @@ import Progress from "~components/Progress";
import browser from "webextension-polyfill";
import Head from "~components/popup/Head";
import Message from "~components/auth/Message";
-
-export default function SignMessage() {
+import type { AuthKeystoneType } from "~api/modules/sign/sign_auth";
+import { onMessage, sendMessage } from "@arconnect/webext-bridge";
+import type { Chunk } from "~api/modules/sign/chunks";
+import { bytesFromChunks } from "~api/modules/sign/transaction_builder";
+export default function SignKeystone() {
// sign params
const params = useAuthParams<{
- data: string;
+ collectionId: string;
+ keystoneSignType: string;
}>();
-
// reconstructed transaction
const [dataToSign, setDataToSign] = useState();
+ const [dataType, setDataType] = useState("Message");
useEffect(() => {
(async () => {
- if (!params?.data) return;
- // reset tx
- setDataToSign(Buffer.from(params?.data, "base64"));
+ // request chunks
+ if (params) {
+ setDataType(params?.keystoneSignType);
+ sendMessage("auth_listening", null, "background");
+
+ const chunks: Chunk[] = [];
+
+ // listen for chunks
+ onMessage("auth_chunk", ({ sender, data }) => {
+ // check data type
+ if (
+ data.collectionID !== params.collectionID ||
+ sender.context !== "background" ||
+ data.type === "start"
+ ) {
+ return;
+ }
+ // end chunk stream
+ if (data.type === "end") {
+ const bytes = bytesFromChunks(chunks);
+ const signData = Buffer.from(bytes);
+ setDataToSign(signData);
+ } else if (data.type === "bytes") {
+ // add chunk
+ chunks.push(data);
+ }
+ });
+ }
})();
}, [params]);
+ useEffect(() => {
+ (async () => {
+ if (dataType === "DataItem" && !!dataToSign) {
+ await loadTransactionUR();
+ setPage("qr");
+ }
+ })();
+ }, [dataType, dataToSign]);
+
// get auth utils
- const { closeWindow, cancel } = useAuthUtils("signMessage", params?.authID);
+ const { closeWindow, cancel } = useAuthUtils("signKeystone", params?.authID);
// authorize
async function authorize(data?: any) {
// reply to request
- await replyToAuthRequest("signMessage", params.authID, undefined, data);
+ await replyToAuthRequest("signKeystone", params.authID, undefined, data);
// close the window
closeWindow();
@@ -69,11 +105,14 @@ export default function SignMessage() {
async function loadTransactionUR() {
if (wallet.type !== "hardware" || !dataToSign) return;
-
// load the ur data
- const ur = await messageToUR(dataToSign, wallet.xfp);
-
- setTransactionUR(ur);
+ if (dataType === "DataItem") {
+ const ur = await dataItemToUR(dataToSign, wallet.xfp);
+ setTransactionUR(ur);
+ } else {
+ const ur = await messageToUR(dataToSign, wallet.xfp);
+ setTransactionUR(ur);
+ }
}
// loading
@@ -106,7 +145,7 @@ export default function SignMessage() {
// reply to request
await replyToAuthRequest(
- "signMessage",
+ "signKeystone",
params.authID,
"Failed to decode signature from keystone"
);
@@ -133,7 +172,7 @@ export default function SignMessage() {
allowOpen={false}
/>
- {(!page && dataToSign && (
+ {(!page && dataToSign && dataType === "Message" && (
@@ -145,13 +184,13 @@ export default function SignMessage() {
<>
+ onError={(error) => {
setToast({
type: "error",
duration: 2300,
content: browser.i18n.getMessage(`keystone_${error}`)
- })
- }
+ });
+ }}
/>
diff --git a/src/routes/popup/announcement.tsx b/src/routes/popup/announcement.tsx
index 349c012df..7b649c2b5 100644
--- a/src/routes/popup/announcement.tsx
+++ b/src/routes/popup/announcement.tsx
@@ -1,16 +1,15 @@
-import {
- ButtonV2,
- ModalV2,
- Spacer,
- Text,
- type DisplayTheme
-} from "@arconnect/components";
+import { ButtonV2, ModalV2, Spacer } from "@arconnect/components";
import { ExtensionStorage } from "~utils/storage";
import { useEffect, useRef, useState } from "react";
import browser from "webextension-polyfill";
import aoLogo from "url:/assets/ecosystem/ao-token-logo.png";
-import styled from "styled-components";
import { useStorage } from "@plasmohq/storage/hook";
+import {
+ ContentWrapper,
+ Content,
+ HeaderText,
+ CenterText
+} from "~components/modals/Components";
export const AnnouncementPopup = ({ isOpen, setOpen }) => {
const [notifications, setNotifications] = useStorage({
@@ -27,11 +26,6 @@ export const AnnouncementPopup = ({ isOpen, setOpen }) => {
}
}, [notifications]);
- const handleCheckbox = async () => {
- setChecked((prev) => !prev);
- setNotifications((prev) => !prev);
- };
-
const handleClickOutside = (event) => {
if (modalRef.current && !modalRef.current.contains(event.target)) {
ExtensionStorage.set("show_announcement", false);
@@ -87,83 +81,3 @@ export const AnnouncementPopup = ({ isOpen, setOpen }) => {
);
};
-
-export const Content = styled.div`
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- flex: none;
- align-self: stretch;
- flex-grow: 0;
-`;
-
-export const ContentWrapper = styled.div`
- height: 100%;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: space-between;
-`;
-
-const CenterText = styled(Text).attrs({
- noMargin: true
-})<{ displayTheme?: DisplayTheme }>`
- width: 245px;
- text-align: center;
- color: ${(props) =>
- props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"};
- font-weight: 500;
- font-size: 11px;
- line-height: 16px;
- align-self: stretch;
- flex: none;
- flex-grow: 0;
-`;
-
-const Link = styled.u`
- cursor: pointer;
-`;
-
-const CheckContainer = styled.div`
- width: 245px;
- display: flex;
- flex-direction: row;
- padding-left: 72px;
- align-items: center;
- isolation: isolate;
- font-weight: 500;
- font-size: 11px;
- flex: none;
- flex-grow: 0;
- gap: 8px;
-`;
-
-const CheckedSvg = styled.svg`
- position: absolute;
- left: calc(50% + 4px / 2 - 113px);
- width: 18px;
- height: 18px;
- cursor: pointer;
- flex: none;
- flex-grow: 0;
- background: #8e7bea;
- border-radius: 2px;
-`;
-
-const UncheckedSvg = styled.svg`
- position: absolute;
- left: calc(50% + 4px / 2 - 113px);
- width: 18px;
- height: 18px;
- cursor: pointer;
- flex: none;
- flex-grow: 0;
-`;
-
-const HeaderText = styled(Text)<{ displayTheme?: DisplayTheme }>`
- font-size: 18px;
- font-weight: 500;
- color: ${(props) =>
- props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"};
-`;
diff --git a/src/routes/popup/index.tsx b/src/routes/popup/index.tsx
index d9a6cd89c..84e0fe085 100644
--- a/src/routes/popup/index.tsx
+++ b/src/routes/popup/index.tsx
@@ -107,17 +107,13 @@ export default function Home() {
useEffect(() => {
const checkBits = async () => {
- const bits = await checkWalletBits();
+ if (!loggedIn) return;
- if (bits === null) {
- return;
- } else {
- await trackEvent(EventType.BITS_LENGTH, { mismatch: bits });
- }
+ const bits = await checkWalletBits();
};
checkBits();
- }, []);
+ }, [loggedIn]);
useEffect(() => {
// check whether to show announcement
diff --git a/src/routes/popup/passwordPopup.tsx b/src/routes/popup/passwordPopup.tsx
index e9648c101..0b0f0d765 100644
--- a/src/routes/popup/passwordPopup.tsx
+++ b/src/routes/popup/passwordPopup.tsx
@@ -12,7 +12,7 @@ import aoLogo from "url:/assets/ecosystem/ao-token-logo.png";
import styled from "styled-components";
import { CheckIcon, CloseIcon } from "@iconicicons/react";
import { ResetButton } from "~components/dashboard/Reset";
-import { Content, ContentWrapper } from "./announcement";
+import { Content, ContentWrapper } from "~components/modals/Components";
export const PasswordWarningModal = ({
open,
diff --git a/src/routes/popup/send/announcement.tsx b/src/routes/popup/send/announcement.tsx
index e5f77b6b8..91f8484a0 100644
--- a/src/routes/popup/send/announcement.tsx
+++ b/src/routes/popup/send/announcement.tsx
@@ -1,14 +1,13 @@
-import {
- ButtonV2,
- ModalV2,
- Spacer,
- Text,
- type DisplayTheme
-} from "@arconnect/components";
+import { ButtonV2, ModalV2, Spacer } from "@arconnect/components";
import { useRef } from "react";
import browser from "webextension-polyfill";
import aoLogo from "url:/assets/ecosystem/ao-token-logo.png";
-import styled from "styled-components";
+import {
+ HeaderText,
+ CenterText,
+ Content,
+ ContentWrapper
+} from "~components/modals/Components";
export const AnnouncementPopup = ({ isOpen, setOpen }) => {
const modalRef = useRef(null);
@@ -60,48 +59,3 @@ export const AnnouncementPopup = ({ isOpen, setOpen }) => {
);
};
-
-const Content = styled.div`
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- flex: none;
- align-self: stretch;
- flex-grow: 0;
-`;
-
-const ContentWrapper = styled.div`
- height: 100%;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: space-between;
-`;
-
-const CenterText = styled(Text).attrs({
- noMargin: true
-})<{ displayTheme?: DisplayTheme }>`
- width: 245px;
- text-align: center;
- color: ${(props) =>
- props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"};
- font-weight: 500;
- font-size: 11px;
- line-height: 16px;
- align-self: stretch;
- flex: none;
- flex-grow: 0;
-
- a {
- color: rgb(${(props) => props.theme.theme});
- text-decoration: none;
- }
-`;
-
-const HeaderText = styled(Text)<{ displayTheme?: DisplayTheme }>`
- font-size: 18px;
- font-weight: 500;
- color: ${(props) =>
- props.theme.displayTheme === "light" ? "#191919" : "#FFFFFF"};
-`;
diff --git a/src/routes/popup/send/confirm.tsx b/src/routes/popup/send/confirm.tsx
index 0b745bb7f..a32e82f15 100644
--- a/src/routes/popup/send/confirm.tsx
+++ b/src/routes/popup/send/confirm.tsx
@@ -45,16 +45,26 @@ import {
import { fractionedToBalance } from "~tokens/currency";
import { type Token } from "~tokens/token";
import { useContact } from "~contacts/hooks";
-import { sendAoTransfer, useAo } from "~tokens/aoTokens/ao";
+import {
+ sendAoTransfer,
+ sendAoTransferKeystone,
+ useAo
+} from "~tokens/aoTokens/ao";
import { useActiveWallet } from "~wallets/hooks";
import { UR } from "@ngraveio/bc-ur";
-import { decodeSignature, transactionToUR } from "~wallets/hardware/keystone";
+import {
+ KeystoneSigner,
+ decodeSignature,
+ transactionToUR,
+ type KeystoneInteraction
+} from "~wallets/hardware/keystone";
import { useScanner } from "@arconnect/keystone-sdk";
import Progress from "~components/Progress";
import { updateSubscription } from "~subscriptions";
import { SubscriptionStatus } from "~subscriptions/subscription";
import { checkPassword } from "~wallets/auth";
import BigNumber from "bignumber.js";
+import { SignType } from "@keystonehq/bc-ur-registry-arweave";
interface Props {
tokenID: string;
@@ -507,9 +517,34 @@ export default function Confirm({ tokenID, qty, subscription }: Props) {
const [transactionUR, setTransactionUR] = useState();
const [preparedTx, setPreparedTx] = useState>();
+ const keystoneInteraction = useMemo(() => {
+ const keystoneInteraction: KeystoneInteraction = {
+ display(data) {
+ setIsLoading(false);
+ setTransactionUR(data);
+ }
+ };
+ return keystoneInteraction;
+ }, [setIsLoading]);
+
+ const keystoneSigner = useMemo(() => {
+ if (wallet?.type !== "hardware") return null;
+ const keystoneSigner = new KeystoneSigner(
+ Buffer.from(Arweave.utils.b64UrlToBuffer(wallet.publicKey)),
+ wallet.xfp,
+ isAo ? SignType.DataItem : SignType.Transaction,
+ keystoneInteraction
+ );
+ return keystoneSigner;
+ }, [wallet, isAo, keystoneInteraction]);
+
useEffect(() => {
(async () => {
- if (!recipient?.address) return;
+ setIsLoading(true);
+ if (!recipient?.address) {
+ setIsLoading(false);
+ return;
+ }
// get the tx from storage
const prepared = await prepare(recipient.address);
@@ -522,7 +557,36 @@ export default function Confirm({ tokenID, qty, subscription }: Props) {
// check if the current wallet
// is a hardware wallet
- if (wallet?.type !== "hardware") return;
+ if (wallet?.type !== "hardware") {
+ setIsLoading(false);
+ return;
+ }
+
+ if (isAo) {
+ try {
+ setPreparedTx(prepared);
+ const res = await sendAoTransferKeystone(
+ ao,
+ tokenID,
+ recipient.address,
+ fractionedToBalance(amount, token, "AO"),
+ keystoneSigner
+ );
+ if (res) {
+ setToast({
+ type: "success",
+ content: browser.i18n.getMessage("sent_tx"),
+ duration: 2000
+ });
+ push(`/transaction/${res}`);
+ setIsLoading(false);
+ }
+ return res;
+ } catch (err) {
+ console.log("err in ao", err);
+ throw err;
+ }
+ }
const arweave = new Arweave(prepared.gateway);
const convertedTransaction = arweave.transactions.fromRaw(
@@ -531,6 +595,7 @@ export default function Confirm({ tokenID, qty, subscription }: Props) {
// get tx UR
try {
+ setIsLoading(false);
setTransactionUR(
await transactionToUR(
convertedTransaction,
@@ -548,7 +613,7 @@ export default function Confirm({ tokenID, qty, subscription }: Props) {
push("/send/transfer");
}
})();
- }, [wallet, recipient]);
+ }, [wallet, recipient, keystoneSigner, setIsLoading]);
// current hardware wallet operation
const [hardwareStatus, setHardwareStatus] = useState<"play" | "scan">();
@@ -582,6 +647,11 @@ export default function Confirm({ tokenID, qty, subscription }: Props) {
// decode signature
const { id, signature } = await decodeSignature(res);
+ if (isAo) {
+ keystoneSigner.submitSignature(signature);
+ return;
+ }
+
// set signature
transaction.setSignature({
id,
diff --git a/src/routes/popup/send/index.tsx b/src/routes/popup/send/index.tsx
index 1b801e80d..ea6e621ef 100644
--- a/src/routes/popup/send/index.tsx
+++ b/src/routes/popup/send/index.tsx
@@ -138,9 +138,6 @@ export default function Send({ id }: Props) {
"token"
);
- const wallet = useActiveWallet();
- const keystoneError = wallet?.type === "hardware" && isAo;
-
// tokens
const tokens = useTokens();
@@ -428,32 +425,21 @@ export default function Send({ id }: Props) {
{AO_NATIVE_TOKEN === tokenID && (
)}
-
+
{/* TOP INPUT */}
- {(keystoneError || degraded) && (
+ {degraded && (
- {keystoneError ? (
- <>
-
{browser.i18n.getMessage("keystone_ao_title")}
-
- {browser.i18n.getMessage("keystone_ao_description")}
-
- >
- ) : (
- <>
- {browser.i18n.getMessage("ao_degraded")}
-
- {browser.i18n
- .getMessage("ao_degraded_description")
- .replace("
", "")}
-
- >
- )}
+ {browser.i18n.getMessage("ao_degraded")}
+
+ {browser.i18n
+ .getMessage("ao_degraded_description")
+ .replace("
", "")}
+
)}
diff --git a/src/routes/popup/settings/apps/[url]/permissions.tsx b/src/routes/popup/settings/apps/[url]/permissions.tsx
index 2f9ea8304..2145bd6cc 100644
--- a/src/routes/popup/settings/apps/[url]/permissions.tsx
+++ b/src/routes/popup/settings/apps/[url]/permissions.tsx
@@ -9,7 +9,7 @@ import { useLocation } from "wouter";
export default function AppPermissions({ url }: Props) {
// app settings
- const app = new Application(url);
+ const app = new Application(decodeURIComponent(url));
const [settings, updateSettings] = app.hook();
const [, setLocation] = useLocation();
@@ -24,42 +24,53 @@ export default function AppPermissions({ url }: Props) {
{browser.i18n.getMessage("permissions")}
{Object.keys(permissionData).map(
- (permissionName: PermissionType, i) => (
-
-
-
- updateSettings((val) => {
- // toggle permission
- if (
- checked &&
- !val.permissions.includes(permissionName)
- ) {
- val.permissions.push(permissionName);
- } else if (!checked) {
- val.permissions = val.permissions.filter(
- (p) => p !== permissionName
- );
- }
+ (permissionName: PermissionType, i) => {
+ let formattedPermissionName = permissionName
+ .split("_")
+ .map((word) => word.charAt(0) + word.slice(1).toLowerCase())
+ .join(" ");
- return val;
- })
- }
- checked={settings.permissions.includes(permissionName)}
- />
-
-
{permissionName}
-
- {browser.i18n.getMessage(permissionData[permissionName])}
-
-
-
- {i !== Object.keys(permissionData).length - 1 && (
-
- )}
-
- )
+ if (permissionName === "SIGNATURE") {
+ formattedPermissionName = "Sign Data";
+ }
+
+ return (
+
+
+
+ updateSettings((val) => {
+ // toggle permission
+ if (
+ checked &&
+ !val.permissions.includes(permissionName)
+ ) {
+ val.permissions.push(permissionName);
+ } else if (!checked) {
+ val.permissions = val.permissions.filter(
+ (p) => p !== permissionName
+ );
+ }
+
+ return val;
+ })
+ }
+ checked={settings.permissions.includes(permissionName)}
+ />
+
+
{formattedPermissionName}
+
+ {browser.i18n.getMessage(permissionData[permissionName])}
+
+
+
+ {i !== Object.keys(permissionData).length - 1 && (
+
+ )}
+
+ );
+ }
)}
diff --git a/src/routes/welcome/generate/backup.tsx b/src/routes/welcome/generate/backup.tsx
index 846ea7233..38976d24f 100644
--- a/src/routes/welcome/generate/backup.tsx
+++ b/src/routes/welcome/generate/backup.tsx
@@ -1,6 +1,6 @@
import { ButtonV2, Spacer, Text } from "@arconnect/components";
import { useLocation, useRoute } from "wouter";
-import { useContext, useEffect, useState } from "react";
+import { useContext, useEffect, useRef, useState } from "react";
import { WalletContext } from "../setup";
import Paragraph from "~components/Paragraph";
import browser from "webextension-polyfill";
@@ -20,7 +20,10 @@ export default function Backup() {
const [shown, setShown] = useState(false);
// wallet context
- const generatedWallet = useContext(WalletContext);
+ const { wallet: generatedWallet } = useContext(WalletContext);
+
+ // ref to track the latest generated wallet
+ const walletRef = useRef(generatedWallet);
// route
const [, params] = useRoute<{ setup: string; page: string }>("/:setup/:page");
@@ -36,6 +39,10 @@ export default function Backup() {
setTimeout(() => setCopyDisplay(true), 1050);
}
+ useEffect(() => {
+ walletRef.current = generatedWallet;
+ }, [generatedWallet]);
+
// Segment
useEffect(() => {
trackPage(PageType.ONBOARD_BACKUP);
diff --git a/src/routes/welcome/generate/confirm.tsx b/src/routes/welcome/generate/confirm.tsx
index 96a4d46ab..6e2430106 100644
--- a/src/routes/welcome/generate/confirm.tsx
+++ b/src/routes/welcome/generate/confirm.tsx
@@ -10,7 +10,7 @@ import { PageType, trackPage } from "~utils/analytics";
export default function Confirm() {
// wallet context
- const generatedWallet = useContext(WalletContext);
+ const { wallet: generatedWallet } = useContext(WalletContext);
// toasts
const { setToast } = useToasts();
diff --git a/src/routes/welcome/generate/done.tsx b/src/routes/welcome/generate/done.tsx
index c9bebde0b..89f2a93e1 100644
--- a/src/routes/welcome/generate/done.tsx
+++ b/src/routes/welcome/generate/done.tsx
@@ -5,7 +5,7 @@ import { formatAddress } from "~utils/format";
import Paragraph from "~components/Paragraph";
import browser from "webextension-polyfill";
import { addWallet } from "~wallets";
-import { useContext, useEffect } from "react";
+import { useContext, useEffect, useRef, useState } from "react";
import {
EventType,
PageType,
@@ -22,7 +22,14 @@ import { addExpiration } from "~wallets/auth";
export default function Done() {
// wallet context
- const wallet = useContext(WalletContext);
+ const { wallet } = useContext(WalletContext);
+ const walletRef = useRef(wallet);
+
+ // loading
+ const [loading, setLoading] = useState(false);
+
+ // wallet generation taking longer
+ const [showLongWaitMessage, setShowLongWaitMessage] = useState(false);
const [, setLocation] = useLocation();
@@ -36,13 +43,32 @@ export default function Done() {
// add generated wallet
async function done() {
+ if (loading) return;
+
+ const startTime = Date.now();
+
+ setLoading(true);
// add wallet
let nickname: string;
- if (!wallet.address || !wallet.jwk) return;
+ if (!walletRef.current.address || !walletRef.current.jwk) {
+ await new Promise((resolve) => {
+ const checkState = setInterval(() => {
+ if (walletRef.current.jwk) {
+ clearInterval(checkState);
+ resolve(null);
+ }
+ if (!showLongWaitMessage) {
+ setShowLongWaitMessage(Date.now() - startTime > 10000);
+ }
+ }, 1000);
+ });
+ }
try {
- const ansProfile = (await getAnsProfile(wallet.address)) as AnsUser;
+ const ansProfile = (await getAnsProfile(
+ walletRef.current.address
+ )) as AnsUser;
if (ansProfile) {
nickname = ansProfile.currentLabel;
@@ -51,7 +77,9 @@ export default function Done() {
// add the wallet
await addWallet(
- nickname ? { nickname, wallet: wallet.jwk } : wallet.jwk,
+ nickname
+ ? { nickname, wallet: walletRef.current.jwk }
+ : walletRef.current.jwk,
password
);
@@ -68,6 +96,12 @@ export default function Done() {
// redirect to getting started pages
setLocation("/getting-started/1");
+
+ setShowLongWaitMessage(false);
+ setLoading(false);
+
+ // reset before unload
+ window.onbeforeunload = null;
}
useEffect(() => {
@@ -90,6 +124,10 @@ export default function Done() {
getLocation();
}, []);
+ useEffect(() => {
+ walletRef.current = wallet;
+ }, [wallet]);
+
// Segment
useEffect(() => {
trackPage(PageType.ONBOARD_COMPLETE);
@@ -113,9 +151,14 @@ export default function Done() {
{browser.i18n.getMessage("analytics_title")}
-
+
{browser.i18n.getMessage("done")}
+ {loading && showLongWaitMessage && (
+
+ {browser.i18n.getMessage("longer_than_usual")}
+
+ )}
>
);
}
diff --git a/src/routes/welcome/gettingStarted.tsx b/src/routes/welcome/gettingStarted.tsx
index cd74a1541..a79d7a20f 100644
--- a/src/routes/welcome/gettingStarted.tsx
+++ b/src/routes/welcome/gettingStarted.tsx
@@ -38,6 +38,8 @@ export default function GettingStarted({ page }) {
if (pageNum < 5) {
setLocation(`/getting-started/${pageNum}`);
} else {
+ // reset before unload
+ window.onbeforeunload = null;
window.top.close();
}
};
diff --git a/src/routes/welcome/load/done.tsx b/src/routes/welcome/load/done.tsx
index 329c7d863..fdbc45368 100644
--- a/src/routes/welcome/load/done.tsx
+++ b/src/routes/welcome/load/done.tsx
@@ -25,6 +25,9 @@ export default function Done() {
await setAnalytics(false);
}
+ // reset before unload
+ window.onbeforeunload = null;
+
// redirect to getting started pages
setLocation("/getting-started/1");
}
diff --git a/src/routes/welcome/load/password.tsx b/src/routes/welcome/load/password.tsx
index e961db789..16e6b9bb5 100644
--- a/src/routes/welcome/load/password.tsx
+++ b/src/routes/welcome/load/password.tsx
@@ -70,16 +70,12 @@ export default function Password() {
setLocation(`/${params.setup}/${Number(params.page) + 1}`);
}
- // password valid
- const validPassword = useMemo(
- () => checkPasswordValid(passwordInput.state),
- [passwordInput]
- );
-
// passwords match
const matches = useMemo(
- () => passwordInput.state === validPasswordInput.state && validPassword,
- [passwordInput, validPasswordInput, validPassword]
+ () =>
+ passwordInput.state === validPasswordInput.state &&
+ passwordInput.state?.length >= 5,
+ [passwordInput, validPasswordInput]
);
// Segment
diff --git a/src/routes/welcome/load/wallets.tsx b/src/routes/welcome/load/wallets.tsx
index 1bca81ea8..2b5791014 100644
--- a/src/routes/welcome/load/wallets.tsx
+++ b/src/routes/welcome/load/wallets.tsx
@@ -1,6 +1,11 @@
import { isValidMnemonic, jwkFromMnemonic } from "~wallets/generator";
import { ExtensionStorage, OLD_STORAGE_NAME } from "~utils/storage";
-import { addWallet, getWallets, setActiveWallet } from "~wallets";
+import {
+ addWallet,
+ getWalletKeyLength,
+ getWallets,
+ setActiveWallet
+} from "~wallets";
import type { KeystoneAccount } from "~wallets/hardware/keystone";
import type { JWKInterface } from "arweave/web/lib/wallet";
import { useContext, useEffect, useMemo, useState } from "react";
@@ -23,11 +28,15 @@ import Paragraph from "~components/Paragraph";
import browser from "webextension-polyfill";
import styled from "styled-components";
import { addExpiration } from "~wallets/auth";
+import { WalletKeySizeErrorModal } from "~components/modals/WalletKeySizeErrorModal";
export default function Wallets() {
// password context
const { password } = useContext(PasswordContext);
+ // wallet generation taking longer
+ const [showLongWaitMessage, setShowLongWaitMessage] = useState(false);
+
// migration available
const [oldState] = useStorage({
key: OLD_STORAGE_NAME,
@@ -37,6 +46,9 @@ export default function Wallets() {
// migration modal
const migrationModal = useModal();
+ // wallet size error modal
+ const walletModal = useModal();
+
// wallets to migrate
const [walletsToMigrate, setWalletsToMigrate] = useState([]);
@@ -101,6 +113,7 @@ export default function Wallets() {
const finishUp = () => {
// reset before unload
window.onbeforeunload = null;
+ setShowLongWaitMessage(false);
setLoading(false);
};
@@ -126,11 +139,30 @@ export default function Wallets() {
if (loadedWallet) {
// load jwk from seedphrase input state
- const jwk =
+ const startTime = Date.now();
+
+ let jwk =
typeof loadedWallet === "string"
? await jwkFromMnemonic(loadedWallet)
: loadedWallet;
+ let { actualLength, expectedLength } = await getWalletKeyLength(jwk);
+ if (expectedLength !== actualLength) {
+ if (typeof loadedWallet !== "string") {
+ walletModal.setOpen(true);
+ finishUp();
+ return;
+ } else {
+ while (expectedLength !== actualLength) {
+ setShowLongWaitMessage(Date.now() - startTime > 30000);
+ jwk = await jwkFromMnemonic(loadedWallet);
+ ({ actualLength, expectedLength } = await getWalletKeyLength(
+ jwk
+ ));
+ }
+ }
+ }
+
// add wallet
await addWallet(jwk, password);
await addExpiration();
@@ -198,6 +230,11 @@ export default function Wallets() {
{browser.i18n.getMessage("next")}
+ {loading && showLongWaitMessage && (
+
+ {browser.i18n.getMessage("longer_than_usual")}
+
+ )}
+ setLocation(`/`)} />
>
);
}
diff --git a/src/routes/welcome/setup.tsx b/src/routes/welcome/setup.tsx
index 63dc56ebb..6162d6301 100644
--- a/src/routes/welcome/setup.tsx
+++ b/src/routes/welcome/setup.tsx
@@ -26,6 +26,7 @@ import LoadDone from "./load/done";
import Theme from "./load/theme";
import { defaultGateway } from "~gateways/gateway";
import Pagination, { Status } from "~components/Pagination";
+import { getWalletKeyLength } from "~wallets";
/** Wallet generate pages */
const generatePages = [
@@ -92,54 +93,65 @@ export default function Setup({ setupMode, page }: Props) {
const { setToast } = useToasts();
// generate wallet in the background
- const [generatedWallet, setGeneratedWallet] = useState(
- {}
- );
+ const [generatedWallet, setGeneratedWallet] = useState({});
const navigate = () => {
setLocation(`/${params.setup}/${page - 1}`);
};
- useEffect(() => {
- (async () => {
- // only generate wallet if the
- // setup mode is wallet generation
- if (!isGenerateWallet || generatedWallet.address) return;
+ async function generateWallet() {
+ // only generate wallet if the
+ // setup mode is wallet generation
+ if (!isGenerateWallet || generatedWallet.address) return;
- // prevent user from closing the window
- // while ArConnect is generating a wallet
- window.onbeforeunload = () =>
- browser.i18n.getMessage("close_tab_generate_wallet_message");
+ // prevent user from closing the window
+ // while ArConnect is generating a wallet
+ window.onbeforeunload = () =>
+ browser.i18n.getMessage("close_tab_generate_wallet_message");
- try {
- const arweave = new Arweave(defaultGateway);
+ try {
+ const arweave = new Arweave(defaultGateway);
- // generate seed
- const seed = await bip39.generateMnemonic();
+ // generate seed
+ const seed = await bip39.generateMnemonic();
- setGeneratedWallet({ mnemonic: seed });
+ setGeneratedWallet({ mnemonic: seed });
- // generate wallet from seedphrase
- const generatedKeyfile = await jwkFromMnemonic(seed);
+ // generate wallet from seedphrase
+ let generatedKeyfile = await jwkFromMnemonic(seed);
- setGeneratedWallet((val) => ({ ...val, jwk: generatedKeyfile }));
+ let { actualLength, expectedLength } = await getWalletKeyLength(
+ generatedKeyfile
+ );
+ while (expectedLength !== actualLength) {
+ generatedKeyfile = await jwkFromMnemonic(seed);
+ ({ actualLength, expectedLength } = await getWalletKeyLength(
+ generatedKeyfile
+ ));
+ }
- // get address
- const address = await arweave.wallets.jwkToAddress(generatedKeyfile);
+ setGeneratedWallet((val) => ({ ...val, jwk: generatedKeyfile }));
- setGeneratedWallet((val) => ({ ...val, address }));
- } catch (e) {
- console.log("Error generating wallet", e);
- setToast({
- type: "error",
- content: browser.i18n.getMessage("error_generating_wallet"),
- duration: 2300
- });
- }
+ // get address
+ const address = await arweave.wallets.jwkToAddress(generatedKeyfile);
+
+ setGeneratedWallet((val) => ({ ...val, address }));
+
+ return generatedWallet;
+ } catch (e) {
+ console.log("Error generating wallet", e);
+ setToast({
+ type: "error",
+ content: browser.i18n.getMessage("error_generating_wallet"),
+ duration: 2300
+ });
+ }
- // reset before unload
- window.onbeforeunload = null;
- })();
+ return {};
+ }
+
+ useEffect(() => {
+ generateWallet();
}, [isGenerateWallet]);
// animate content sice
@@ -189,7 +201,9 @@ export default function Setup({ setupMode, page }: Props) {
-
+
@@ -290,9 +304,17 @@ export const PasswordContext = createContext({
password: ""
});
-export const WalletContext = createContext({});
+export const WalletContext = createContext({
+ wallet: {},
+ generateWallet: (retry?: boolean) => Promise.resolve({})
+});
interface WalletContextValue {
+ wallet: GeneratedWallet;
+ generateWallet: (retry?: boolean) => Promise;
+}
+
+interface GeneratedWallet {
address?: string;
mnemonic?: string;
jwk?: JWKInterface;
diff --git a/src/tabs/auth.tsx b/src/tabs/auth.tsx
index 0d416d350..54dd641e5 100644
--- a/src/tabs/auth.tsx
+++ b/src/tabs/auth.tsx
@@ -16,7 +16,7 @@ import SignDataItem from "~routes/auth/signDataItem";
import Token from "~routes/auth/token";
import Sign from "~routes/auth/sign";
import Subscription from "~routes/auth/subscription";
-import SignMessage from "~routes/auth/signMessage";
+import SignKeystone from "~routes/auth/signKeystone";
export default function Auth() {
const theme = useTheme();
@@ -36,7 +36,7 @@ export default function Auth() {
-
+
diff --git a/src/tokens/aoTokens/ao.ts b/src/tokens/aoTokens/ao.ts
index 184077568..53e7b50ae 100644
--- a/src/tokens/aoTokens/ao.ts
+++ b/src/tokens/aoTokens/ao.ts
@@ -14,6 +14,7 @@ import {
AO_NATIVE_TOKEN_BALANCE_MIRROR
} from "~utils/ao_import";
import type { Alarms } from "webextension-polyfill";
+import type { KeystoneSigner } from "~wallets/hardware/keystone";
export type AoInstance = ReturnType;
@@ -101,7 +102,7 @@ export function useAoTokens(
() =>
tokens.map((token) => ({
...token,
- balance: balances.find((bal) => bal.id === token.id)?.balance ?? null
+ balance: balances.find((bal) => bal.id === token.id)?.balance
})),
[tokens, balances]
);
@@ -486,6 +487,54 @@ export const aoTokensCacheHandler = async (alarmInfo?: Alarms.Alarm) => {
await ExtensionStorage.set("ao_tokens", updatedTokens);
};
+export const sendAoTransferKeystone = async (
+ ao: AoInstance,
+ process: string,
+ recipient: string,
+ amount: string,
+ keystoneSigner: KeystoneSigner
+) => {
+ try {
+ const dataItemSigner = async ({
+ data,
+ tags = [],
+ target,
+ anchor
+ }: {
+ data: any;
+ tags?: { name: string; value: string }[];
+ target?: string;
+ anchor?: string;
+ }): Promise<{ id: string; raw: ArrayBuffer }> => {
+ const signer = keystoneSigner;
+ const dataItem = createData(data, signer, { tags, target, anchor });
+ const serial = dataItem.getRaw();
+ const signature = await signer.sign(serial);
+ dataItem.setSignature(Buffer.from(signature));
+
+ return {
+ id: dataItem.id,
+ raw: dataItem.getRaw()
+ };
+ };
+ const transferID = await ao.message({
+ process,
+ signer: dataItemSigner,
+ tags: [
+ { name: "Action", value: "Transfer" },
+ {
+ name: "Recipient",
+ value: recipient
+ },
+ { name: "Quantity", value: amount }
+ ]
+ });
+ return transferID;
+ } catch (err) {
+ console.log("err", err);
+ }
+};
+
export interface TokenInfo {
Name?: string;
Ticker?: string;
diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts
index 544b84514..db2f82099 100644
--- a/src/utils/analytics.ts
+++ b/src/utils/analytics.ts
@@ -1,7 +1,12 @@
import { getSetting } from "~settings";
import { ExtensionStorage, TempTransactionStorage } from "./storage";
import { AnalyticsBrowser } from "@segment/analytics-next";
-import { getWallets, getActiveKeyfile, getActiveAddress } from "~wallets";
+import {
+ getWallets,
+ getActiveKeyfile,
+ getActiveAddress,
+ getWalletKeyLength
+} from "~wallets";
import Arweave from "arweave";
import { defaultGateway } from "~gateways/gateway";
import { v4 as uuid } from "uuid";
@@ -9,7 +14,6 @@ import browser, { type Alarms } from "webextension-polyfill";
import BigNumber from "bignumber.js";
import axios from "axios";
import { isLocalWallet } from "./assertions";
-import { ArweaveSigner } from "arbundles";
import { freeDecryptedWallet } from "~wallets/encryption";
const PUBLIC_SEGMENT_WRITEKEY = "J97E4cvSZqmpeEdiUQNC2IxS1Kw4Cwxm";
@@ -234,6 +238,11 @@ const setToStartOfNextMonth = (currentDate: Date): Date => {
return newDate;
};
+export interface WalletBitsCheck {
+ checked: boolean;
+ mismatch: boolean;
+}
+
/**
* Checks the bit length the active Arweave wallet.
*
@@ -254,11 +263,16 @@ export const checkWalletBits = async (): Promise => {
if (!activeAddress) {
return null;
}
- const hasBeenTracked = await ExtensionStorage.get(
- `bits_check_${activeAddress}`
+
+ const storageKey = `bits_check_${activeAddress}`;
+
+ const hasBeenTracked = await ExtensionStorage.get(
+ storageKey
);
- if (hasBeenTracked) {
+ if (typeof hasBeenTracked === "boolean") {
+ await ExtensionStorage.remove(storageKey);
+ } else if (hasBeenTracked && hasBeenTracked.checked) {
return null;
}
@@ -268,16 +282,20 @@ export const checkWalletBits = async (): Promise => {
});
isLocalWallet(decryptedWallet);
- const signer = new ArweaveSigner(decryptedWallet.keyfile);
- const owner = signer.publicKey;
- const expectedLength = signer.ownerLength;
- const actualLength = owner.byteLength;
+ const { actualLength, expectedLength } = await getWalletKeyLength(
+ decryptedWallet.keyfile
+ );
+
+ freeDecryptedWallet(decryptedWallet.keyfile);
const lengthsMatch = expectedLength === actualLength;
- freeDecryptedWallet(decryptedWallet.keyfile);
+ await ExtensionStorage.set(`bits_check_${activeAddress}`, {
+ checked: true,
+ mismatch: !lengthsMatch
+ });
- await ExtensionStorage.set(`bits_check_${activeAddress}`, true);
+ await trackEvent(EventType.BITS_LENGTH, { mismatch: !lengthsMatch });
return !lengthsMatch;
} catch (error) {
diff --git a/src/utils/data_item.ts b/src/utils/data_item.ts
new file mode 100644
index 000000000..dc707bcdf
--- /dev/null
+++ b/src/utils/data_item.ts
@@ -0,0 +1,39 @@
+import {
+ ArweaveSigner,
+ DataItem,
+ Signer,
+ createData,
+ type DataItemCreateOptions
+} from "arbundles";
+
+export interface SignerConfig {
+ signatureType: number;
+ signatureLength: number;
+ ownerLength: number;
+ publicKey: Buffer;
+}
+
+class DummySigner implements Signer {
+ publicKey: Buffer;
+ signatureType: number;
+ signatureLength: number;
+ ownerLength: number;
+ pem?: string | Buffer;
+ constructor(signerConfig: SignerConfig) {
+ this.publicKey = signerConfig.publicKey;
+ this.signatureLength = signerConfig.signatureLength;
+ this.signatureType = signerConfig.signatureType;
+ this.ownerLength = signerConfig.ownerLength;
+ }
+ sign(message: Uint8Array, _opts?: any): Uint8Array | Promise {
+ throw new Error("Method not implemented.");
+ }
+}
+
+export const createDataItem = (
+ binary: Uint8Array,
+ signerConfig: SignerConfig,
+ options: DataItemCreateOptions
+): DataItem => {
+ return createData(binary, new DummySigner(signerConfig), options);
+};
diff --git a/src/wallets/hardware/keystone.ts b/src/wallets/hardware/keystone.ts
index 4458c773a..471a7e713 100644
--- a/src/wallets/hardware/keystone.ts
+++ b/src/wallets/hardware/keystone.ts
@@ -10,6 +10,51 @@ import type { UR } from "@ngraveio/bc-ur";
import { v4 as uuid } from "uuid";
import Arweave from "arweave";
import { defaultGateway } from "~gateways/gateway";
+import { Signer } from "arbundles";
+import { EventEmitter } from "events";
+
+export interface KeystoneInteraction {
+ display(data: UR);
+}
+
+export class KeystoneSigner implements Signer {
+ readonly signatureType: number = 1;
+ readonly ownerLength: number = 512;
+ readonly signatureLength: number = 512;
+ #_event = new EventEmitter();
+ public get publicKey(): Buffer {
+ return this._publicKey;
+ }
+
+ constructor(
+ private _publicKey: Buffer,
+ private mfp: string,
+ private signType: SignType,
+ private interaction: KeystoneInteraction,
+ private options: SignatureOptions = { saltLength: 32 }
+ ) {}
+ sign(message: Uint8Array, _opts?: any): Promise {
+ const data = Buffer.from(message);
+ const signRequest = ArweaveSignRequest.constructArweaveRequest(
+ data,
+ this.mfp,
+ this.signType,
+ this.options.saltLength
+ );
+ return new Promise(async (resolve) => {
+ const ur = signRequest.toUR();
+ this.interaction.display(ur);
+ this.#_event.once("submit-signature", (signature) => {
+ resolve(signature);
+ });
+ });
+ }
+
+ submitSignature(signature: string) {
+ const signatureBytes = Buffer.from(signature, "base64");
+ this.#_event.emit("submit-signature", signatureBytes);
+ }
+}
/**
* Decode cbor result from a keystone QR code
@@ -98,6 +143,24 @@ export async function messageToUR(
return signRequest.toUR();
}
+export async function dataItemToUR(
+ data: Uint8Array,
+ xfp: string,
+ options: SignatureOptions = { saltLength: 32 }
+) {
+ const messageBuff = Buffer.from(data);
+
+ // construct request
+ const signRequest = ArweaveSignRequest.constructArweaveRequest(
+ messageBuff,
+ xfp,
+ SignType.DataItem,
+ options.saltLength
+ );
+
+ return signRequest.toUR();
+}
+
/**
* Decode cbor result from a keystone QR code
* with an Arweave transaction
diff --git a/src/wallets/index.ts b/src/wallets/index.ts
index d0ec04541..abcd442c9 100644
--- a/src/wallets/index.ts
+++ b/src/wallets/index.ts
@@ -20,6 +20,7 @@ import {
getDecryptionKey,
setDecryptionKey
} from "./auth";
+import { ArweaveSigner } from "arbundles";
/**
* Locally stored wallet
@@ -395,3 +396,10 @@ export async function syncLabels(alarmInfo?: Alarms.Alarm) {
}))
);
}
+
+export async function getWalletKeyLength(jwk: JWKInterface) {
+ const signer = new ArweaveSigner(jwk);
+ const expectedLength = signer.ownerLength;
+ const actualLength = signer.publicKey.byteLength;
+ return { actualLength, expectedLength };
+}