diff --git a/packages/extension/locales/en-US/translation.json b/packages/extension/locales/en-US/translation.json index ad25c7b0..5087158a 100644 --- a/packages/extension/locales/en-US/translation.json +++ b/packages/extension/locales/en-US/translation.json @@ -185,7 +185,8 @@ "User disconnected": "User disconnected", "Transaction building failed": "Transaction building failed", "Sending transaction failed": "Sending transaction failed", - "Sign unsigned tx failed": "Sign unsigned tx failed", + "Sign raw tx failed": "Sign raw tx failed", + "Sign message failed": "Sign message failed", "Add Network": "Add Network", "Switch Network": "Switch Network", "Network ID": "Network ID", @@ -206,8 +207,8 @@ "Sign Message": "Sign Message", "Signer": "Signer", "Hasher": "Hasher", - "Sign Unsigned TX": "Sign Unsigned TX", - "Unsigned TX": "Unsigned TX", + "Sign Raw TX": "Sign Raw TX", + "Raw TX": "Raw TX", "The Ledger app is not connected": "The Ledger app is not connected", "Insufficient token {{ tokenSymbol }}, expected at least {{ expectedStr }}, got {{ haveStr }}": "Insufficient token {{ tokenSymbol }}, expected at least {{ expectedStr }}, got {{ haveStr }}", "No account found for network {{ networkId }}": "No account found for network {{ networkId }}", diff --git a/packages/extension/package.json b/packages/extension/package.json index d14519d2..7c5019a5 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -7,9 +7,9 @@ "@alephium/get-extension-wallet": "^1.7.3", "@alephium/ledger-app": "0.6.0", "@alephium/token-list": "0.0.19", - "@alephium/web3": "^1.7.3", - "@alephium/web3-test": "^1.7.3", - "@alephium/web3-wallet": "^1.7.3", + "@alephium/web3": "^1.8.3", + "@alephium/web3-test": "^1.8.3", + "@alephium/web3-wallet": "^1.8.3", "@ledgerhq/hw-transport-webusb": "6.29.0", "@ledgerhq/hw-transport-webhid": "6.29.0", "@playwright/test": "^1.23.0", diff --git a/packages/extension/src/background/actionHandlers.ts b/packages/extension/src/background/actionHandlers.ts index 5546b11d..d43de3c3 100644 --- a/packages/extension/src/background/actionHandlers.ts +++ b/packages/extension/src/background/actionHandlers.ts @@ -1,3 +1,4 @@ +import { TransactionBuilder } from "@alephium/web3" import { getAccounts } from "../shared/account/store" import { ActionItem, @@ -74,26 +75,37 @@ export const handleActionApproval = async ( } case "ALPH_SIGN_MESSAGE": { - const account = await wallet.getAccount({ - address: action.payload.signerAddress, - networkId: action.payload.networkId, - }) - if (!account) { - throw Error("No selected account") - } + try { + const account = await wallet.getAccount({ + address: action.payload.signerAddress, + networkId: action.payload.networkId, + }) + if (!account) { + throw Error("No selected account") + } + if (account.signer.type === 'ledger') { + throw Error("Signing messages with Ledger accounts is not supported") + } - const result = await wallet.signMessage(account, action.payload) + const result = await wallet.signMessage(account, action.payload) - return { - type: "ALPH_SIGN_MESSAGE_SUCCESS", - data: { - signature: result.signature, - actionHash, - }, + return { + type: "ALPH_SIGN_MESSAGE_SUCCESS", + data: { + signature: result.signature, + actionHash, + }, + } + } catch (error) { + return { + type: "ALPH_SIGN_MESSAGE_FAILURE", + data: { actionHash, error: `${error}` }, + } } } case "ALPH_SIGN_UNSIGNED_TX": { + const { signatureOpt } = additionalData as { signatureOpt: string | undefined } try { const account = await wallet.getAccount({ address: action.payload.signerAddress, @@ -102,12 +114,23 @@ export const handleActionApproval = async ( if (!account) { throw Error("No selected account") } - - const result = await wallet.signUnsignedTx(account, action.payload) - - return { - type: "ALPH_SIGN_UNSIGNED_TX_SUCCESS", - data: { actionHash, result }, + if (signatureOpt === undefined) { + const result = await wallet.signUnsignedTx(account, action.payload) + + return { + type: "ALPH_SIGN_UNSIGNED_TX_SUCCESS", + data: { actionHash, result }, + } + } else { + const signUnsignedTxResult = TransactionBuilder.buildUnsignedTx({ + signerAddress: account.address, + unsignedTx: action.payload.unsignedTx + }) + const result = { signature: signatureOpt, ...signUnsignedTxResult } + return { + type: "ALPH_SIGN_UNSIGNED_TX_SUCCESS", + data: { actionHash, result }, + } } } catch (error) { return { diff --git a/packages/extension/src/ui/features/actions/ActionScreen.tsx b/packages/extension/src/ui/features/actions/ActionScreen.tsx index 89d6aa3e..160b10d2 100644 --- a/packages/extension/src/ui/features/actions/ActionScreen.tsx +++ b/packages/extension/src/ui/features/actions/ActionScreen.tsx @@ -188,15 +188,26 @@ export const ActionScreen: FC = () => { onSubmit={async () => { await approveAction(action) useAppState.setState({ isLoading: true }) - await waitForMessage( - "ALPH_SIGN_MESSAGE_SUCCESS", - ({ data }) => data.actionHash === action.meta.hash, - ) - await analytics.track("signedMessage", { - networkId: selectedAccount?.networkId || t("unknown"), - }) - closePopupIfLastAction() - useAppState.setState({ isLoading: false }) + const result = await Promise.race([ + waitForMessage( + 'ALPH_SIGN_MESSAGE_SUCCESS', + ({ data }) => data.actionHash === action.meta.hash, + ), + waitForMessage( + 'ALPH_SIGN_MESSAGE_FAILURE', + ({ data }) => data.actionHash === action.meta.hash, + ), + ]) + if ("error" in result) { + useAppState.setState({ + error: `${t('Sign message failed')}: ${result.error}`, + isLoading: false, + }) + navigate(routes.error()) + } else { + closePopupIfLastAction() + useAppState.setState({ isLoading: false }) + } }} onReject={onReject} selectedAccount={signerAccount} @@ -207,8 +218,8 @@ export const ActionScreen: FC = () => { return ( { - await approveAction(action) + onSubmit={async (signatureOpt) => { + await approveAction(action, signatureOpt) useAppState.setState({ isLoading: true }) const result = await Promise.race([ waitForMessage( @@ -222,7 +233,7 @@ export const ActionScreen: FC = () => { ]) if ("error" in result) { useAppState.setState({ - error: `${t('Sign unsigned tx failed')}: ${result.error}`, + error: `${t('Sign raw tx failed')}: ${result.error}`, isLoading: false, }) navigate(routes.error()) diff --git a/packages/extension/src/ui/features/actions/ApproveSignUnsignedTxScreen.tsx b/packages/extension/src/ui/features/actions/ApproveSignUnsignedTxScreen.tsx index 395b3d44..6c1df342 100644 --- a/packages/extension/src/ui/features/actions/ApproveSignUnsignedTxScreen.tsx +++ b/packages/extension/src/ui/features/actions/ApproveSignUnsignedTxScreen.tsx @@ -1,6 +1,6 @@ import { binToHex, hexToBinUnsafe, SignUnsignedTxParams } from "@alephium/web3" import { H6, H2, P4, CopyTooltip } from "@argent/ui" -import { FC } from "react" +import { FC, useCallback } from "react" import { ConfirmPageProps } from "./DeprecatedConfirmScreen" import { Box, Flex, VStack } from "@chakra-ui/react" @@ -9,30 +9,71 @@ import { AccountNetworkInfo } from "./transaction/AccountNetworkInfo" import blake from 'blakejs' import { TxHashContainer } from "./TxHashContainer" import { useTranslation } from "react-i18next" +import { LedgerStatus } from "./LedgerStatus" +import { useNavigate } from "react-router-dom" +import { useLedgerApp } from "../ledger/useLedgerApp" +import { getConfirmationTextByState } from "../ledger/types" interface ApproveSignUnsignedTxScreenProps extends Omit { params: SignUnsignedTxParams & { host: string } - onSubmit: (data: SignUnsignedTxParams) => void + onSubmit: (result: { signatureOpt: string | undefined }) => void } export const ApproveSignUnsignedTxScreen: FC = ({ params, onSubmit, + onReject, selectedAccount, ...props }) => { const { t } = useTranslation() const txId = binToHex(blake.blake2b(hexToBinUnsafe(params.unsignedTx), undefined, 32)) + + const navigate = useNavigate() + const useLedger = selectedAccount !== undefined && selectedAccount.signer.type === "ledger" + const ledgerSubmit = useCallback((signature: string) => { + onSubmit({ signatureOpt: signature }) + }, [onSubmit]) + const { ledgerState, ledgerApp, ledgerSign } = useLedgerApp({ + selectedAccount, + unsignedTx: params.unsignedTx, + onSubmit: ledgerSubmit, + navigate, + onReject + }) + return ( onSubmit(params)} + onSubmit={() => { + if (useLedger) { + ledgerSign() + } else { + onSubmit({ signatureOpt: undefined }) + } + }} + onReject={() => { + if (ledgerApp !== undefined) { + ledgerApp.close() + } + if (onReject !== undefined) { + onReject() + } else { + navigate(-1) + } + }} + footer={ + + + + } {...props} > { @@ -45,7 +86,7 @@ export const ApproveSignUnsignedTxScreen: FC = mt="3" gap="6" > -

{t("Sign Unsigned TX")}

+

{t("Sign Raw TX")}

{params.host}
} @@ -66,7 +107,7 @@ export const ApproveSignUnsignedTxScreen: FC = w="full" > - {t("Unsigned TX")} + {t("Raw TX")} { - const { t } = useTranslation() - return ( - ledgerState === "notfound" ? - - - - - - {t("The Ledger app is not connected")} - - - : <> - ) -} +import { LedgerStatus } from "./LedgerStatus" +import { useLedgerApp } from "../ledger/useLedgerApp" +import { getConfirmationTextByState } from "../ledger/types" const minimalGasFee = BigInt(20000) * BigInt(100000000000) @@ -125,7 +101,7 @@ async function buildTransaction( return { type: transaction.type, params: transaction.params, - result: await builder.buildUnsignedTx(transaction.params), + result: TransactionBuilder.buildUnsignedTx(transaction.params), } } } @@ -207,14 +183,22 @@ export const ApproveTransactionScreen: FC = ({ selectedAccount?.networkId ?? "unknown", ) - const useLedger = - selectedAccount !== undefined && selectedAccount.signer.type === "ledger" - const [ledgerState, setLedgerState] = useState< - "detecting" | "notfound" | "signing" | "succeeded" | "failed" - >() - const [ledgerApp, setLedgerApp] = useState() const { tokenDetails: allUserTokens, tokenDetailsIsInitialising } = useAllTokensWithBalance(selectedAccount) + const useLedger = selectedAccount !== undefined && selectedAccount.signer.type === "ledger" + const ledgerSubmit = useCallback((signature: string) => { + if (buildResult) { + onSubmit({ ...buildResult, signature }) + } + }, [onSubmit, buildResult]) + const { ledgerState, ledgerApp, ledgerSign } = useLedgerApp({ + selectedAccount, + unsignedTx: buildResult?.result.unsignedTx, + onSubmit: ledgerSubmit, + navigate, + onReject + }) + // TODO: handle error useEffect(() => { const build = async () => { @@ -243,39 +227,6 @@ export const ApproveTransactionScreen: FC = ({ build() }, [nodeUrl, selectedAccount, transaction, tokenDetailsIsInitialising, actionHash, navigate, t]) - const ledgerSign = useCallback(async () => { - if (selectedAccount === undefined) { - return - } - setLedgerState(oldState => oldState === undefined ? "detecting" : oldState) - - if (buildResult) { - let app: LedgerAlephium | undefined - try { - app = await LedgerAlephium.create() - setLedgerApp(app) - setLedgerState("signing") - const unsignedTx = Buffer.from(buildResult.result.unsignedTx, "hex") - const signature = await app.signUnsignedTx(selectedAccount, unsignedTx) - setLedgerState("succeeded") - onSubmit({ ...buildResult, signature }) - } catch (e) { - if (app === undefined) { - setLedgerState(oldState => oldState === undefined || oldState === "detecting" ? "notfound" : oldState) - setTimeout(ledgerSign, 1000) - } else { - await app.close() - setLedgerState("failed") - if (onReject !== undefined) { - onReject() - } else { - navigate(-1) - } - } - } - } - }, [selectedAccount, buildResult, onSubmit, onReject, navigate]) - if (!selectedAccount) { rejectAction(actionHash, t("No account found for network {{ networkId }}", { networkId })) return @@ -287,21 +238,7 @@ export const ApproveTransactionScreen: FC = ({ return ( { + const { t } = useTranslation() + return ( + ledgerState === "notfound" ? + + + + + + {t("The Ledger app is not connected")} + + + : <> + ) +} diff --git a/packages/extension/src/ui/features/ledger/types.ts b/packages/extension/src/ui/features/ledger/types.ts new file mode 100644 index 00000000..314db7e4 --- /dev/null +++ b/packages/extension/src/ui/features/ledger/types.ts @@ -0,0 +1,14 @@ +export type LedgerState = "detecting" | "notfound" | "signing" | "succeeded" | "failed" +export type LedgerConfirmation = "Sign with Ledger" | "Ledger: Detecting" | "Ledger: Signing" | "Ledger: Succeeded" | "Ledger: Failed" + +export function getConfirmationTextByState(ledgerState: LedgerState | undefined): LedgerConfirmation { + return ledgerState === undefined + ? "Sign with Ledger" + : (ledgerState === "detecting") || (ledgerState === "notfound") + ? "Ledger: Detecting" + : ledgerState === "signing" + ? "Ledger: Signing" + : ledgerState === "succeeded" + ? "Ledger: Succeeded" + : "Ledger: Failed" +} \ No newline at end of file diff --git a/packages/extension/src/ui/features/ledger/useLedgerApp.ts b/packages/extension/src/ui/features/ledger/useLedgerApp.ts new file mode 100644 index 00000000..cf759243 --- /dev/null +++ b/packages/extension/src/ui/features/ledger/useLedgerApp.ts @@ -0,0 +1,55 @@ +import { useCallback, useState } from "react" +import { Account } from "../accounts/Account" +import { LedgerAlephium } from "../ledger/utils" +import { LedgerState } from "./types" + +export function useLedgerApp({ + selectedAccount, + unsignedTx, + onSubmit, + navigate, + onReject, +} : { + selectedAccount: Account | undefined, + unsignedTx: string | undefined, + onSubmit: (signature: string) => void, + navigate: (n: number) => void, + onReject?: () => void +}) { + const [ledgerState, setLedgerState] = useState() + const [ledgerApp, setLedgerApp] = useState() + + const ledgerSign = useCallback(async () => { + if (selectedAccount === undefined) { + return + } + setLedgerState(oldState => oldState === undefined ? "detecting" : oldState) + + let app: LedgerAlephium | undefined + if (unsignedTx !== undefined) { + try { + app = await LedgerAlephium.create() + setLedgerApp(app) + setLedgerState("signing") + const signature = await app.signUnsignedTx(selectedAccount, Buffer.from(unsignedTx, "hex")) + setLedgerState("succeeded") + onSubmit(signature) + } catch (e) { + if (app === undefined) { + setLedgerState(oldState => oldState === undefined || oldState === "detecting" ? "notfound" : oldState) + setTimeout(ledgerSign, 1000) + } else { + await app.close() + setLedgerState("failed") + if (onReject !== undefined) { + onReject() + } else { + navigate(-1) + } + } + } + } + }, [selectedAccount, onSubmit, onReject, navigate, unsignedTx]) + + return { ledgerState, ledgerApp, ledgerSign } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1bcc8000..ce91ae1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -42,13 +42,13 @@ dependencies: cross-fetch "^3.1.8" -"@alephium/web3-test@^1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@alephium/web3-test/-/web3-test-1.7.3.tgz#b26c5ce51e6d57f14a0fb75d7abc9f1f63540e56" - integrity sha512-Tz32fxXgyMREWz6Q9rCGYnPDoGIdf18kNZeBlQCZYgHc255okIW7CXPmvftUUS1gTStQJ9W7rFhtbje0MzzsDg== +"@alephium/web3-test@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@alephium/web3-test/-/web3-test-1.8.3.tgz#c2ac50374281eb2b416000c0b6e7e686bd967c26" + integrity sha512-btut4b4/uuAGzLAudatK0R7JA5HsVn+a+sbmzmFw26TGqAMiEdjBlFGHtXYowdlla+ozdgreiQARhXhCrMTlVA== dependencies: - "@alephium/web3" "^1.7.3" - "@alephium/web3-wallet" "^1.7.3" + "@alephium/web3" "^1.8.3" + "@alephium/web3-wallet" "^1.8.3" "@alephium/web3-wallet@^1.7.3": version "1.7.3" @@ -63,6 +63,19 @@ elliptic "6.5.4" fs-extra "10.0.1" +"@alephium/web3-wallet@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@alephium/web3-wallet/-/web3-wallet-1.8.3.tgz#34e331ffe65357caf36b0f4963926da958b42b48" + integrity sha512-tTdUbL31t7RSXL6LouZQ2rCQrZLuTbyH4S7j4+9JCkQ3xMH/nWyinIBPODqSBCOdVvZNGQ53B2g23GXvsZDrpA== + dependencies: + "@alephium/web3" "^1.8.3" + "@noble/secp256k1" "1.7.1" + "@types/node" "^16.18.23" + bip32 "3.1.0" + bip39 "3.0.4" + elliptic "6.5.4" + fs-extra "10.0.1" + "@alephium/web3@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@alephium/web3/-/web3-1.5.0.tgz#2949fd08fb1650a930c66730353957cdbef0dce0" @@ -97,6 +110,23 @@ path-browserify "^1.0.1" stream-browserify "^3.0.0" +"@alephium/web3@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@alephium/web3/-/web3-1.8.3.tgz#3cdcd5d3449783d2a7b7e6a16a7273bea6de343c" + integrity sha512-F2qrgcEZDZRv4RiN/7fGHphErsxSK5PjPrJWbffkSXak1z5xZT7aRsU6tr8gsubNF2JitmWL2i7P1oUJj8scFA== + dependencies: + "@noble/secp256k1" "1.7.1" + base-x "4.0.0" + bignumber.js "^9.1.1" + blakejs "1.2.1" + bn.js "5.2.1" + cross-fetch "^3.1.5" + crypto-browserify "^3.12.0" + elliptic "6.5.4" + eventemitter3 "^4.0.7" + path-browserify "^1.0.1" + stream-browserify "^3.0.0" + "@alloc/quick-lru@^5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" @@ -21286,16 +21316,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -21395,7 +21416,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -21409,13 +21430,6 @@ strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -23735,7 +23749,7 @@ worker-rpc@^0.1.0: dependencies: microevent.ts "~0.1.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -23753,15 +23767,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"