From 3313569628c0edd6b2f67b51d8b3c7f26cb1b3c1 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Thu, 28 Mar 2024 14:57:24 +0100 Subject: [PATCH 01/28] fix: prevent cutting off parts of account name letters --- src/popup/components/AccountInfo.vue | 23 +- src/popup/components/Avatar.vue | 18 +- src/popup/components/NameItem.vue | 376 +++++++++++++-------------- src/popup/pages/Names/NamesList.vue | 6 +- src/types/index.ts | 1 + 5 files changed, 216 insertions(+), 208 deletions(-) diff --git a/src/popup/components/AccountInfo.vue b/src/popup/components/AccountInfo.vue index 71221eb84..44b902cec 100644 --- a/src/popup/components/AccountInfo.vue +++ b/src/popup/components/AccountInfo.vue @@ -1,7 +1,7 @@ @@ -86,7 +88,9 @@ $size-xl: 56px; height: $size-rg; border-radius: 50%; overflow: hidden; - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; object-fit: cover; user-select: none; flex-shrink: 0; diff --git a/src/popup/components/NameItem.vue b/src/popup/components/NameItem.vue index e30bedbe8..b39f11826 100644 --- a/src/popup/components/NameItem.vue +++ b/src/popup/components/NameItem.vue @@ -1,140 +1,147 @@ @@ -148,7 +155,7 @@ import { watch, } from 'vue'; import { useI18n } from 'vue-i18n'; -import { ChainName } from '@/types'; +import { IName } from '@/types'; import { Clipboard } from '@capacitor/clipboard'; import { IS_EXTENSION, @@ -173,8 +180,7 @@ import { useAeNames } from '@/protocols/aeternity/composables/aeNames'; import { useAeNetworkSettings } from '@/protocols/aeternity/composables'; import { UPDATE_POINTER_ACTION } from '@/protocols/aeternity/config'; -import Avatar from './Avatar.vue'; -import Truncate from './Truncate.vue'; +import AccountInfo from './AccountInfo.vue'; import InputField from './InputField.vue'; import BtnPlain from './buttons/BtnPlain.vue'; import BtnHelp from './buttons/BtnHelp.vue'; @@ -187,9 +193,8 @@ import Paste from '../../icons/paste.svg?vue-component'; export default defineComponent({ components: { + AccountInfo, Pending, - Avatar, - Truncate, InputField, BtnPlain, BtnHelp, @@ -199,9 +204,7 @@ export default defineComponent({ Paste, }, props: { - name: { type: String as PropType, default: '' }, - address: { type: String, default: '' }, - autoExtend: { type: Boolean }, + nameEntry: { type: Object as PropType, required: true }, }, setup(props) { const { openModal } = useModals(); @@ -209,7 +212,6 @@ export default defineComponent({ const { t } = useI18n(); const { topBlockHeight } = useTopHeaderData(); const { - ownedNames, setAutoExtend, updateNamePointer, getName, @@ -224,18 +226,18 @@ export default defineComponent({ const nameError = ref(false); const pointerInput = ref(); - const nameEntry = computed( - () => ownedNames.value.find((ownedName) => ownedName.name === props.name), + const isDefault = computed( + () => getName(activeAccount.value.address).value === props.nameEntry.name, + ); + const hasPointer = computed( + (): boolean => !!props.nameEntry.pointers?.accountPubkey, ); - - const isDefault = computed(() => getName(activeAccount.value.address).value === props.name); - const hasPointer = computed((): boolean => !!nameEntry.value?.pointers?.accountPubkey); const canBeDefault = computed( - (): boolean => nameEntry.value?.pointers?.accountPubkey === activeAccount.value.address, + (): boolean => props.nameEntry?.pointers?.accountPubkey === activeAccount.value.address, ); const addressOrFirstPointer = computed((): string | null => ( - nameEntry.value?.pointers?.accountPubkey - || Object.values(nameEntry.value?.pointers || {})[0] + props.nameEntry?.pointers?.accountPubkey + || Object.values(props.nameEntry?.pointers || {})[0] )); async function readValueFromClipboard(): Promise { @@ -283,7 +285,7 @@ export default defineComponent({ async function handleSetDefault() { try { const { address } = activeAccount.value; - const { name } = props; + const { name } = props.nameEntry; const url = `${aeActiveNetworkSettings.value.backendUrl}/profile/${address}`; const currentNetworkId = nodeNetworkId.value; @@ -308,14 +310,14 @@ export default defineComponent({ } async function toggleAutoExtend() { - if (!props.autoExtend) { + if (!props.nameEntry.autoExtend) { await openModal(MODAL_CONFIRM, { icon: 'info', title: t('modals.autoextend-help.title'), msg: t('modals.autoextend-help.msg'), }); } - setAutoExtend(props.name); + setAutoExtend(props.nameEntry.name); } async function setPointer() { @@ -324,7 +326,7 @@ export default defineComponent({ return; } updateNamePointer({ - name: props.name, + name: props.nameEntry.name, address: newPointer.value, type: UPDATE_POINTER_ACTION.update, }); @@ -345,7 +347,6 @@ export default defineComponent({ expand, hasPointer, isDefault, - nameEntry, newPointer, pointerInput, showInput, @@ -379,9 +380,9 @@ export default defineComponent({ background-color: variables.$color-bg-4-hover; } - .collapsed { + .name-item-header { display: flex; - text-align: left; + align-items: flex-start; justify-content: space-between; .pending-icon { @@ -389,70 +390,63 @@ export default defineComponent({ width: 32px; } - .header { - flex: 2; - max-width: 260px; + .pending { + @extend %face-sans-12-regular; - .pending { - color: variables.$color-grey-dark; - - @extend %face-sans-12-regular; - } + color: variables.$color-grey-dark; + } - .truncate { - @extend %face-sans-15-medium; + .truncate { + @extend %face-sans-15-medium; - line-height: 16px; - } + line-height: 16px; + } - .buttons { - display: flex; - margin-top: 2px; - user-select: none; + .buttons { + display: flex; + margin-top: 2px; + user-select: none; - .button-plain:not(.btn-help) { - @extend %face-sans-12-medium; + .button-plain:not(.btn-help) { + @extend %face-sans-12-medium; - padding: 2px 8px; - white-space: nowrap; - color: variables.$color-grey-light; - background: variables.$color-border-hover; - border-radius: 6px; - opacity: 1; + padding: 2px 8px; + white-space: nowrap; + color: variables.$color-grey-light; + background: variables.$color-border-hover; + border-radius: 6px; + opacity: 1; - @include mixins.mobile { - padding: 2px 6px; - } + @include mixins.mobile { + padding: 2px 6px; + } - &.set { - background: rgba(variables.$color-warning, 0.1); - color: variables.$color-warning; - } + &.set { + background: rgba(variables.$color-warning, 0.1); + color: variables.$color-warning; + } - &.edit { - background: rgba(variables.$color-primary, 0.15); - color: variables.$color-primary; - } + &.edit { + background: rgba(variables.$color-primary, 0.15); + color: variables.$color-primary; + } - &:not(:last-of-type) { - margin-right: 4px; - } + &:not(:last-of-type) { + margin-right: 4px; } } } - .button-plain { - align-self: flex-start; - flex-basis: 24px; + .btn-toggle { + margin-top: 4px; + width: 24px; + height: 24px; .icon { width: 14px; color: variables.$color-white; opacity: 0.44; - - &.hidden { - display: none; - } + transition: all 0.2s; &.rotated { transform: rotate(180deg); diff --git a/src/popup/pages/Names/NamesList.vue b/src/popup/pages/Names/NamesList.vue index e0ff830df..fd54ad986 100644 --- a/src/popup/pages/Names/NamesList.vue +++ b/src/popup/pages/Names/NamesList.vue @@ -4,11 +4,9 @@
Date: Fri, 29 Mar 2024 10:10:48 +0100 Subject: [PATCH 02/28] fix: browser actions menu buttons appearance --- src/popup/components/BrowserActionItem.vue | 92 ------------------- .../components/Modals/BrowserActions.vue | 26 +++--- src/popup/components/buttons/BtnBase.vue | 2 +- 3 files changed, 16 insertions(+), 104 deletions(-) delete mode 100644 src/popup/components/BrowserActionItem.vue diff --git a/src/popup/components/BrowserActionItem.vue b/src/popup/components/BrowserActionItem.vue deleted file mode 100644 index ac6c49ed7..000000000 --- a/src/popup/components/BrowserActionItem.vue +++ /dev/null @@ -1,92 +0,0 @@ - - - - - diff --git a/src/popup/components/Modals/BrowserActions.vue b/src/popup/components/Modals/BrowserActions.vue index f0bb3a242..4446a221a 100644 --- a/src/popup/components/Modals/BrowserActions.vue +++ b/src/popup/components/Modals/BrowserActions.vue @@ -8,28 +8,29 @@ @close="resolve" >
- -
+ @@ -255,7 +263,7 @@ export default defineComponent({ .left { display: flex; - .home-button { + .btn-home { &.disabled { cursor: default; } @@ -318,7 +326,7 @@ export default defineComponent({ } } - .home-button + .back { + .btn-home + .back { margin-left: 22px; } } From a9219c42fba541acb8516d63cfd8cb12a5c5db20 Mon Sep 17 00:00:00 2001 From: martinkaintas Date: Wed, 3 Apr 2024 11:35:51 +0300 Subject: [PATCH 04/28] chore: update eth node urls --- src/protocols/ethereum/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/protocols/ethereum/config.ts b/src/protocols/ethereum/config.ts index c312ede16..684c5b77f 100644 --- a/src/protocols/ethereum/config.ts +++ b/src/protocols/ethereum/config.ts @@ -70,11 +70,11 @@ export const ETH_SAFE_CONFIRMATION_COUNT = 12; export const ETH_NETWORK_DEFAULT_SETTINGS: IDefaultNetworkTypeData = { [NETWORK_TYPE_MAINNET]: { - nodeUrl: 'https://ethereum.publicnode.com', // TODO replace temp values - use our own node + nodeUrl: 'https://ethereum-rpc.publicnode.com', // TODO replace temp values - use our own node chainId: '0x1', }, [NETWORK_TYPE_TESTNET]: { - nodeUrl: 'https://ethereum-sepolia.publicnode.com', // TODO replace temp values - use our own node + nodeUrl: 'https://ethereum-sepolia-rpc.publicnode.com', // TODO replace temp values - use our own node chainId: '0xaa36a7', }, }; From 2789cf4bc220d7d856add79d6be80208e458dbd1 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 5 Apr 2024 15:33:51 +0400 Subject: [PATCH 05/28] perf: use dryAeSdk to collect multisig info --- src/composables/multisigAccounts.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/composables/multisigAccounts.ts b/src/composables/multisigAccounts.ts index 676ffaf23..7131000a6 100644 --- a/src/composables/multisigAccounts.ts +++ b/src/composables/multisigAccounts.ts @@ -74,7 +74,7 @@ export function useMultisigAccounts({ }: MultisigAccountsOptions = {}) { const { onNetworkChange } = useNetworks(); const { aeActiveNetworkPredefinedSettings } = useAeNetworkSettings(); - const { nodeNetworkId, getAeSdk } = useAeSdk(); + const { nodeNetworkId, getAeSdk, getDryAeSdk } = useAeSdk(); const { aeAccounts } = useAccounts(); const allMultisigAccounts = computed(() => [ @@ -179,7 +179,7 @@ export function useMultisigAccounts({ * Refresh the list of the multisig accounts. */ async function updateMultisigAccounts() { - const aeSdk = await getAeSdk(); + const dryAeSdk = await getDryAeSdk(); /** * Establish the list of multisig accounts used by the regular accounts @@ -219,7 +219,7 @@ export function useMultisigAccounts({ ...otherMultisigData }): Promise => { try { - const contractInstance = await aeSdk.initializeContract({ + const contractInstance = await dryAeSdk.initializeContract({ aci: SimpleGAMultiSigAci, address: contractId, }); @@ -243,7 +243,7 @@ export function useMultisigAccounts({ ? { decodedResult: currentAccount.signers } : contractInstance.get_signers(), contractInstance.get_consensus_info(), - gaAccountId ? aeSdk.getBalance(gaAccountId as Encoded.AccountAddress) : 0, + gaAccountId ? dryAeSdk.getBalance(gaAccountId as Encoded.AccountAddress) : 0, ])); const decodedConsensus = consensusResult.decodedResult; From 896245345f2a2c8888980b4f0b0378269876a115 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 5 Apr 2024 16:17:15 +0400 Subject: [PATCH 06/28] build: remove unused extension permissions --- src/manifest.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/manifest.json b/src/manifest.json index a15f21ba4..89c408248 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -9,13 +9,7 @@ "storage", "unlimitedStorage", "activeTab", - "contextMenus", - "notifications", "tabs", - "webRequest", - "webNavigation", - "declarativeNetRequest", - "declarativeNetRequestFeedback", "declarativeNetRequestWithHostAccess" ], "optional_permissions": [ From d9f42fb07f1a14c2a65856ab56a5cf81a9bf6b80 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Fri, 5 Apr 2024 11:04:16 +0200 Subject: [PATCH 07/28] fix: blurred modal windows --- src/popup/components/AccountSelectOptionsItem.vue | 3 ++- src/popup/components/Modal.vue | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/popup/components/AccountSelectOptionsItem.vue b/src/popup/components/AccountSelectOptionsItem.vue index 5fa11ed59..a5d74df1f 100644 --- a/src/popup/components/AccountSelectOptionsItem.vue +++ b/src/popup/components/AccountSelectOptionsItem.vue @@ -96,10 +96,11 @@ export default defineComponent({ padding: 2px 8px; .option-wrapper { + position: relative; + z-index: +1; display: flex; align-items: center; justify-content: space-between; - position: relative; padding: 6px 8px; border-radius: 10px; width: 100%; diff --git a/src/popup/components/Modal.vue b/src/popup/components/Modal.vue index dcb8fdc11..4023c634e 100644 --- a/src/popup/components/Modal.vue +++ b/src/popup/components/Modal.vue @@ -175,7 +175,6 @@ export default defineComponent({ box-shadow: 0 0 0 1px variables.$color-border, 2px 4px 12px rgba(variables.$color-black, 0.22); - will-change: transform; @include mixins.desktop { width: calc(#{variables.$extension-width} - 32px); From 77e9e74cc1f754f589d93ace378ba98c6cb96b1f Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 3 Apr 2024 09:26:18 +0400 Subject: [PATCH 08/28] build: update sdk to 13.3.0 --- package-lock.json | 143 ++++++++++++++++++---------------------------- package.json | 4 +- 2 files changed, 57 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index a8aa2587c..e93f32a2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "2.2.1", "license": "MIT", "dependencies": { - "@aeternity/aepp-calldata": "^1.5.1", - "@aeternity/aepp-sdk": "^13.2.2", + "@aeternity/aepp-calldata": "^1.7.0", + "@aeternity/aepp-sdk": "^13.3.0", "@aeternity/bip39": "^0.1.0", "@aeternity/hd-wallet": "^0.2.0", "@aeternity/json-bigint": "^0.3.1", @@ -151,9 +151,9 @@ } }, "node_modules/@aeternity/aepp-calldata": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@aeternity/aepp-calldata/-/aepp-calldata-1.6.0.tgz", - "integrity": "sha512-usqmuGoxGPH2i1cUvSJKA38ixWBQQT8J+yqe3Zfm22QtdKC2+aD/6cepKq32rB05obJlKsnnyaNbo4fYU8y5Gg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@aeternity/aepp-calldata/-/aepp-calldata-1.7.0.tgz", + "integrity": "sha512-SxxJI+Z/FcxMNRtxmGWSRzrbRwsLI9fGKNdSWKevxa+QWVVOPF9o8XogCbM5FaW+u15P+O0dSvJWZCuJLQ7S9Q==", "dependencies": { "blakejs": "^1.2.1", "bs58": "^5.0.0", @@ -163,27 +163,27 @@ } }, "node_modules/@aeternity/aepp-sdk": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/@aeternity/aepp-sdk/-/aepp-sdk-13.2.2.tgz", - "integrity": "sha512-hTpAV4E8I0Y/IfjizsHlpzPN3gW4Bkr08GVqmRcGUh5yi7MQA9N54kbTfl+kQlcp6AYC9WVk3fhNuoTUOzISig==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@aeternity/aepp-sdk/-/aepp-sdk-13.3.0.tgz", + "integrity": "sha512-n2UCz+jWisFrZ6D60LMyl1fUOWmEPcH6KvfwjAibSEUXtEFXDAYP7BjjPEap3po0VKvHMrZpKWBI2UkGZ2ylxg==", "dependencies": { - "@aeternity/aepp-calldata": "^1.5.1", + "@aeternity/aepp-calldata": "^1.7.0", "@aeternity/argon2": "^0.0.1", "@aeternity/uuid": "^0.0.1", - "@azure/core-client": "1.6.0", - "@azure/core-rest-pipeline": "^1.11.0", - "@babel/runtime-corejs3": "^7.22.6", - "@ledgerhq/hw-transport": "^6.28.6", - "@types/aes-js": "^3.1.1", - "@types/json-bigint": "^1.0.1", - "@types/node": "~18.11.9", - "@types/sha.js": "^2.4.1", - "@types/uuid": "^9.0.2", - "@types/webextension-polyfill": "^0.10.1", - "@types/websocket": "^1.0.5", - "@types/ws": "^8.5.5", + "@azure/core-client": "^1.8.0", + "@azure/core-rest-pipeline": "^1.14.0", + "@babel/runtime-corejs3": "^7.24.0", + "@ledgerhq/hw-transport": "^6.30.4", + "@types/aes-js": "^3.1.4", + "@types/json-bigint": "^1.0.4", + "@types/node": "~18.13", + "@types/sha.js": "^2.4.4", + "@types/uuid": "^9.0.8", + "@types/webextension-polyfill": "^0.10.7", + "@types/websocket": "^1.0.10", + "@types/ws": "^8.5.10", "aes-js": "^3.1.2", - "bignumber.js": "^9.1.1", + "bignumber.js": "^9.1.2", "bip32-path": "^0.4.2", "blakejs": "^1.2.1", "bs58": "^5.0.0", @@ -199,7 +199,7 @@ "tweetnacl-auth": "^1.0.1", "varuint-bitcoin": "^1.1.2", "websocket": "^1.0.34", - "ws": "^8.13.0" + "ws": "^8.16.0" }, "engines": { "node": ">=14.19.0" @@ -297,20 +297,20 @@ } }, "node_modules/@azure/abort-controller": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", - "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.1.tgz", + "integrity": "sha512-NhzeNm5zu2fPlwGXPUjzsRCRuPx5demaZyNcyNYJDqpa/Sbxzvo/RYt9IwUaAOnDW5+r7J9UOE6f22TQnb9nhQ==", "dependencies": { - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/core-auth": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.0.tgz", - "integrity": "sha512-OuDVn9z2LjyYbpu6e7crEwSipa62jX7/ObV/pmXQfnOG8cHwm363jYtg3FSX3GB1V7jsIKri1zgq7mfXkFk/qw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.1.tgz", + "integrity": "sha512-dyeQwvgthqs/SlPVQbZQetpslXceHd4i5a7M/7z/lGEAVwnSluabnQOjF2/dk/hhWgMISusv1Ytp4mQ8JNy62A==", "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.1.0", @@ -320,38 +320,27 @@ "node": ">=18.0.0" } }, - "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.0.tgz", - "integrity": "sha512-SYtcG13aiV7znycu6plCClWUzD9BBtfnsbIxT89nkkRvQRB4n0kuZyJJvJ7hqdKOn7x7YoGKZ9lVStLJpLnOFw==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@azure/core-client": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.6.0.tgz", - "integrity": "sha512-YhSf4cb61ApSjItscp9XoaLq8KRnacPDAhmjAZSMnn/gs6FhFbZNfOBOErG2dDj7JRknVtCmJ5mLmfR2sLa11A==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.1.tgz", + "integrity": "sha512-hHYFx9lz0ZpbO5W+iotU9tmIX1jPcoIjYUEUaWGuMi1628LCQ/z05TUR4P+NRtMgyoHQuyVYyGQiD3PC47kaIA==", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-rest-pipeline": "^1.5.0", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.0.0", + "@azure/core-util": "^1.6.1", "@azure/logger": "^1.0.0", - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.15.0.tgz", - "integrity": "sha512-6kBQwE75ZVlOjBbp0/PX0fgNLHxoMDxHe3aIPV/RLVwrIDidxTbsHtkSbPNTkheMset3v9s1Z08XuMNpWRK/7w==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.15.1.tgz", + "integrity": "sha512-ZxS6i3eHxh86u+1eWZJiYywoN2vxvsSoAUx60Mny8cZ4nTwvt7UzVVBJO+m2PW2KIJfNiXMt59xBa59htOWL4g==", "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.4.0", @@ -366,21 +355,10 @@ "node": ">=18.0.0" } }, - "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.0.tgz", - "integrity": "sha512-SYtcG13aiV7znycu6plCClWUzD9BBtfnsbIxT89nkkRvQRB4n0kuZyJJvJ7hqdKOn7x7YoGKZ9lVStLJpLnOFw==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@azure/core-tracing": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.0.tgz", - "integrity": "sha512-MVeJvGHB4jmF7PeHhyr72vYJsBJ3ff1piHikMgRaabPAC4P3rxhf9fm42I+DixLysBunskJWhsDQD2A+O+plkQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.1.tgz", + "integrity": "sha512-qPbYhN1pE5XQ2jPKIHP33x8l3oBu1UqIWnYqZZ3OYnYjzY0xqIHjn49C+ptsPD9yC7uyWI9Zm7iZUZLs2R4DhQ==", "dependencies": { "tslib": "^2.6.2" }, @@ -389,9 +367,9 @@ } }, "node_modules/@azure/core-util": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.8.0.tgz", - "integrity": "sha512-w8NrGnrlGDF7fj36PBnJhGXDK2Y3kpTOgL7Ksb5snEHXq/3EAbKYOp1yqme0yWCUlSDq5rjqvxSBAJmsqYac3w==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.8.1.tgz", + "integrity": "sha512-L3voj0StUdJ+YKomvwnTv7gHzguJO+a6h30pmmZdRprJCM+RJlGMPxzuh4R7lhQu1jNmEtaHX5wvTgWLDAmbGQ==", "dependencies": { "@azure/abort-controller": "^2.0.0", "tslib": "^2.6.2" @@ -400,21 +378,10 @@ "node": ">=18.0.0" } }, - "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.0.tgz", - "integrity": "sha512-SYtcG13aiV7znycu6plCClWUzD9BBtfnsbIxT89nkkRvQRB4n0kuZyJJvJ7hqdKOn7x7YoGKZ9lVStLJpLnOFw==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@azure/logger": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.0.tgz", - "integrity": "sha512-BnfkfzVEsrgbVCtqq0RYRMePSH2lL/cgUUR5sYRF4yNN10zJZq/cODz0r89k3ykY83MqeM3twR292a3YBNgC3w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.1.tgz", + "integrity": "sha512-/+4TtokaGgC+MnThdf6HyIH9Wrjp+CnCn3Nx3ggevN7FFjjNyjqg0yLlc2i9S+Z2uAzI8GYOo35Nzb1MhQ89MA==", "dependencies": { "tslib": "^2.6.2" }, @@ -6245,9 +6212,9 @@ "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==" }, "node_modules/@types/node": { - "version": "18.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.19.tgz", - "integrity": "sha512-YUgMWAQBWLObABqrvx8qKO1enAvBUdjZOAWQ5grBAkp5LQv45jBvYKZ3oFS9iKRCQyFjqw6iuEa1vmFqtxYLZw==" + "version": "18.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz", + "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==" }, "node_modules/@types/node-forge": { "version": "1.3.11", diff --git a/package.json b/package.json index 0da603362..fc3546b94 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "release": "standard-version" }, "dependencies": { - "@aeternity/aepp-calldata": "^1.5.1", - "@aeternity/aepp-sdk": "^13.2.2", + "@aeternity/aepp-calldata": "^1.7.0", + "@aeternity/aepp-sdk": "^13.3.0", "@aeternity/bip39": "^0.1.0", "@aeternity/hd-wallet": "^0.2.0", "@aeternity/json-bigint": "^0.3.1", From 934208261d6719eb3646b777383c471fa86521ac Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 4 Apr 2024 09:18:27 +0400 Subject: [PATCH 09/28] feat: ability to sign payForTx, typed data, delegation By reusing code from MemoryAccount in AeAccountHdWallet --- .../aeternity/libs/AeAccountHdWallet.ts | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/protocols/aeternity/libs/AeAccountHdWallet.ts b/src/protocols/aeternity/libs/AeAccountHdWallet.ts index 43a51df11..fdb81e87d 100644 --- a/src/protocols/aeternity/libs/AeAccountHdWallet.ts +++ b/src/protocols/aeternity/libs/AeAccountHdWallet.ts @@ -1,15 +1,13 @@ /* eslint-disable class-methods-use-this */ import { AccountBase, + MemoryAccount, sign, - messageToHash, RpcRejectedByUserError, unpackTx, Encoded, METHODS, - buildTx, Tag, - decode, } from '@aeternity/aepp-sdk'; import { Ref } from 'vue'; import type { ITx } from '@/types'; @@ -26,13 +24,13 @@ interface InternalOptions { */ const TAGS_TO_SIGN_WITHOUT_PERMISSION: Tag[] = [Tag.SpendTx, Tag.PayingForTx]; -export class AeAccountHdWallet extends AccountBase { +export class AeAccountHdWallet extends MemoryAccount { override readonly address: Encoded.AccountAddress; nodeNetworkId: Ref; constructor(nodeNetworkId: Ref) { - super(); + super(Buffer.alloc(64)); const { getLastActiveProtocolAccount } = useAccounts(); this.address = getLastActiveProtocolAccount(PROTOCOLS.aeternity)! .address as Encoded.AccountAddress; @@ -59,15 +57,10 @@ export class AeAccountHdWallet extends AccountBase { } } - const encodedTx = decode(txBase64); - const signature = await this.sign( - Buffer.concat([ - Buffer.from(this.nodeNetworkId.value), - Buffer.from(encodedTx), - ]), - options, // Mainly to pass the `fromAccount` property - ); - return buildTx({ tag: Tag.SignedTx, encodedTx, signatures: [signature] }); + return super.signTransaction(txBase64, { + ...options, // Mainly to pass the `fromAccount` property + networkId: this.nodeNetworkId.value, + }); } override async signMessage( @@ -86,8 +79,8 @@ export class AeAccountHdWallet extends AccountBase { } } - return this.sign( - messageToHash(message), + return super.signMessage( + message, options, // Mainly to pass the `fromAccount` property ); } From dfd0fff0f943a94c554c6ecb74acd0dc25670ff3 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 10 Apr 2024 09:03:06 +0400 Subject: [PATCH 10/28] fix: do not use sdk without node --- src/composables/aeSdk.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/composables/aeSdk.ts b/src/composables/aeSdk.ts index 786c67c56..90f6cce3b 100644 --- a/src/composables/aeSdk.ts +++ b/src/composables/aeSdk.ts @@ -107,10 +107,11 @@ export function useAeSdk() { async function resetNode(oldNetwork: INetwork, newNetwork: INetwork) { isAeSdkUpdating.value = true; + const nodeInstance = await createNodeInstance(newNetwork.protocols.aeternity.nodeUrl); aeSdk.pool.delete(oldNetwork.name); aeSdk.addNode( newNetwork.name, - (await createNodeInstance(newNetwork.protocols.aeternity.nodeUrl))!, + nodeInstance!, true, ); isAeSdkUpdating.value = false; From 58b038999ebcba31aa2dea1dc8a77a04fa4f9cef Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 10 Apr 2024 09:51:03 +0400 Subject: [PATCH 11/28] fix: update dryRunSdk on network change correctly --- src/composables/aeSdk.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/composables/aeSdk.ts b/src/composables/aeSdk.ts index 90f6cce3b..559aa0ca2 100644 --- a/src/composables/aeSdk.ts +++ b/src/composables/aeSdk.ts @@ -44,7 +44,6 @@ type AeppInfoData = OnAeppConnectionParams & { origin: string }; let composableInitialized = false; let aeSdk: AeSdkSuperhero; -let storedNetworkName: string; const nodeNetworkId = ref(); @@ -114,6 +113,13 @@ export function useAeSdk() { nodeInstance!, true, ); + + if (dryAeSdk) { + dryAeSdk.pool.delete(oldNetwork.name); + // remove the new network if it exists to avoid errors + dryAeSdk.pool.delete(newNetwork.name); + dryAeSdk.addNode(newNetwork.name, nodeInstance!, true); + } isAeSdkUpdating.value = false; } @@ -125,7 +131,6 @@ export function useAeSdk() { watchUntilTruthy(areNetworksRestored), ]); - storedNetworkName = activeNetworkName.value; const nodeInstance = await createNodeInstance(aeActiveNetworkSettings.value.nodeUrl); aeSdk = new AeSdkSuperhero( @@ -196,16 +201,9 @@ export function useAeSdk() { instance: nodeInstance, }], }); - storedNetworkName = activeNetworkName.value; - return dryAeSdk; } - - if (storedNetworkName !== activeNetworkName.value) { - const nodeInstance = new Node(aeActiveNetworkSettings.value.nodeUrl, { ignoreVersion: true }); - dryAeSdk.pool.delete(storedNetworkName); - // remove the new network if it exists to avoid errors - dryAeSdk.pool.delete(activeNetworkName.value); - dryAeSdk.addNode(activeNetworkName.value, nodeInstance, true); + if (isAeSdkUpdating.value && aeSdk) { + await watchUntilTruthy(() => !isAeSdkUpdating.value); } return dryAeSdk; } From b445a5cf88a9a4a564189b78a026304f11de47d4 Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 10 Apr 2024 13:22:15 +0200 Subject: [PATCH 12/28] feat: expose address for sign message in callback --- src/popup/pages/SignMessage.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popup/pages/SignMessage.vue b/src/popup/pages/SignMessage.vue index 0eb53eefb..274a38dcf 100644 --- a/src/popup/pages/SignMessage.vue +++ b/src/popup/pages/SignMessage.vue @@ -51,7 +51,7 @@ export default defineComponent({ const signature = await aeSdk.signMessage(message as string); const signatureHex = Buffer.from(signature).toString('hex'); - openCallbackOrGoHome(true, { signature: signatureHex }); + openCallbackOrGoHome(true, { signature: signatureHex, address: aeSdk.address }); } catch (error: any) { openCallbackOrGoHome(false); From 1e4aae3ee9e88f31cea456b09d916b9522c2aced Mon Sep 17 00:00:00 2001 From: Philipp Date: Tue, 26 Mar 2024 10:10:57 +0100 Subject: [PATCH 13/28] feat: allow to replace caller in sign transaction --- src/popup/pages/SignTransaction.vue | 40 +++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/popup/pages/SignTransaction.vue b/src/popup/pages/SignTransaction.vue index 2354cd3b0..ff9a62657 100644 --- a/src/popup/pages/SignTransaction.vue +++ b/src/popup/pages/SignTransaction.vue @@ -8,9 +8,11 @@ import { IonPage } from '@ionic/vue'; import { defineComponent, onMounted } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { Encoded } from '@aeternity/aepp-sdk'; +import { + buildTx, Encoded, Tag, unpackTx, +} from '@aeternity/aepp-sdk'; -import { DEFAULT_WAITING_HEIGHT } from '@/constants'; +import { DEFAULT_WAITING_HEIGHT, PROTOCOLS } from '@/constants'; import { RejectedByUserError } from '@/lib/errors'; import { handleUnknownError } from '@/utils'; import { @@ -18,6 +20,7 @@ import { useModals, useAeSdk, useUi, + useAccounts, } from '@/composables'; export default defineComponent({ @@ -34,11 +37,30 @@ export default defineComponent({ const { nodeNetworkId, getAeSdk } = useAeSdk(); const { openDefaultModal } = useModals(); const { setLoaderVisible } = useUi(); + const { getLastActiveProtocolAccount } = useAccounts(); + + const aeSdk = await getAeSdk(); + const activeAccountAddress = getLastActiveProtocolAccount( + PROTOCOLS.aeternity, + )?.address as Encoded.AccountAddress; + + async function replaceCallerTx(tx: Encoded.Transaction, callerId: Encoded.AccountAddress) { + const unpackedTx = unpackTx(tx) as ReturnType + & { callerId: Encoded.AccountAddress }; + + if (unpackedTx.tag === Tag.ContractCallTx || unpackedTx.tag === Tag.ContractCreateTx) { + unpackedTx.callerId = callerId; + unpackedTx.nonce = (await aeSdk.api.getAccountByPubkey(callerId)).nonce + 1; + } + + return buildTx(unpackedTx); + } try { setLoaderVisible(true); - const aeSdk = await getAeSdk(); - const { transaction, networkId, broadcast } = route.query; + const { + transaction, networkId, broadcast, 'replace-caller': replaceCaller, + } = route.query; if (networkId !== nodeNetworkId.value) { await openDefaultModal({ @@ -51,8 +73,16 @@ export default defineComponent({ return; } + // replacing the caller might be chosen by an aepp if no wallet connection is available + // but a transaction should be proposed to the user anyway + // e.g. to reduce the number of steps necessary in a deep-link environment, + // from two round trips, aepp -> wallet -> aepp, to one + const txToSign = replaceCaller === 'true' + ? await replaceCallerTx(transaction as Encoded.Transaction, activeAccountAddress) + : transaction; + const signedTransaction = await aeSdk.signTransaction( - decodeURIComponent(transaction as string) as Encoded.Transaction, + decodeURIComponent(txToSign as string) as Encoded.Transaction, { networkId, aeppOrigin: callbackOrigin.value?.toString(), From 90d55bf8b47cf294a0be13cf4d2cdbfc65a07daf Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 4 Apr 2024 16:01:08 +0400 Subject: [PATCH 14/28] feat: support jwt signing --- src/background/bgPopupHandler.ts | 1 + src/composables/permissions.ts | 6 + src/constants/common.ts | 6 + src/constants/stubs.ts | 4 + src/popup/components/DetailsItem.vue | 2 +- .../components/Modals/ConfirmRawSign.vue | 2 +- .../components/Modals/ConfirmUnsafeSign.vue | 176 ++++++++++++++++++ src/popup/components/TransactionTagList.vue | 6 +- src/popup/locales/en.json | 10 + src/popup/pages/JwtSign.vue | 89 +++++++++ src/popup/pages/Popups/AccountList.vue | 2 +- src/popup/pages/Popups/Connect.vue | 2 +- src/popup/pages/Popups/MessageSign.vue | 2 +- src/popup/router/index.ts | 3 + src/popup/router/modals.ts | 6 + src/popup/router/routeNames.ts | 1 + src/popup/router/routes.ts | 21 +++ src/popup/router/webIframePopups.ts | 3 + .../aeternity/libs/AeAccountHdWallet.ts | 19 +- src/types/index.ts | 2 + src/utils/common.ts | 10 + tests/unit/transaction-tag-list.spec.js | 6 +- 22 files changed, 366 insertions(+), 13 deletions(-) create mode 100644 src/popup/components/Modals/ConfirmUnsafeSign.vue create mode 100644 src/popup/pages/JwtSign.vue diff --git a/src/background/bgPopupHandler.ts b/src/background/bgPopupHandler.ts index 7c59862bf..0b6b678ea 100644 --- a/src/background/bgPopupHandler.ts +++ b/src/background/bgPopupHandler.ts @@ -74,6 +74,7 @@ export const openPopup = async ( message: params.message, tx: params.tx, txBase64: params.txBase64, + data: params.data, }, }; return popups[id]; diff --git a/src/composables/permissions.ts b/src/composables/permissions.ts index 53c75f333..1d3159953 100644 --- a/src/composables/permissions.ts +++ b/src/composables/permissions.ts @@ -12,11 +12,13 @@ import { MODAL_CONFIRM_ACCOUNT_LIST, MODAL_CONFIRM_CONNECT, MODAL_CONFIRM_RAW_SIGN, + MODAL_CONFIRM_UNSAFE_SIGN, MODAL_CONFIRM_TRANSACTION_SIGN, MODAL_MESSAGE_SIGN, POPUP_TYPE_ACCOUNT_LIST, POPUP_TYPE_CONNECT, POPUP_TYPE_MESSAGE_SIGN, + POPUP_TYPE_UNSAFE_SIGN, POPUP_TYPE_RAW_SIGN, POPUP_TYPE_SIGN, STORAGE_KEYS, @@ -59,6 +61,10 @@ const modalAndPopupTypes: Partial = { callerId: STUB_ADDRESS, contractId: STUB_CONTRACT_ADDRESS, }, + }, + [POPUP_TYPE_UNSAFE_SIGN]: { + }, base: { app: STUB_APP_DATA, diff --git a/src/popup/components/DetailsItem.vue b/src/popup/components/DetailsItem.vue index b144f2d89..e9a0c733b 100644 --- a/src/popup/components/DetailsItem.vue +++ b/src/popup/components/DetailsItem.vue @@ -115,7 +115,7 @@ export default defineComponent({ @extend %face-sans-15-regular; letter-spacing: 0.05em; - color: $color-white; + color: rgba($color-white, 0.85); margin-bottom: 8px; .secondary { diff --git a/src/popup/components/Modals/ConfirmRawSign.vue b/src/popup/components/Modals/ConfirmRawSign.vue index 362c3bd77..726ed1227 100644 --- a/src/popup/components/Modals/ConfirmRawSign.vue +++ b/src/popup/components/Modals/ConfirmRawSign.vue @@ -6,7 +6,7 @@ data-cy="popup-aex2" > diff --git a/src/popup/components/Modals/ConfirmUnsafeSign.vue b/src/popup/components/Modals/ConfirmUnsafeSign.vue new file mode 100644 index 000000000..d1a66c4e9 --- /dev/null +++ b/src/popup/components/Modals/ConfirmUnsafeSign.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/src/popup/components/TransactionTagList.vue b/src/popup/components/TransactionTagList.vue index f82b47a39..eb7399e5e 100644 --- a/src/popup/components/TransactionTagList.vue +++ b/src/popup/components/TransactionTagList.vue @@ -37,7 +37,7 @@ export default defineComponent({ TransactionTag, }, props: { - customTitle: { type: String, default: null }, + customLabels: { type: Array as PropType, default: null }, transaction: { type: Object as PropType, default: null }, additionalTag: { type: String, default: null }, dense: Boolean, @@ -64,8 +64,8 @@ export default defineComponent({ const { getProtocolAvailableTokens } = useFungibleTokens(); const labels = computed((): string[] => { - if (props.customTitle) { - return [props.customTitle]; + if (props.customLabels) { + return [...props.customLabels]; } if ( !props.transaction?.tx diff --git a/src/popup/locales/en.json b/src/popup/locales/en.json index 1c131ad4b..688f8ab0f 100644 --- a/src/popup/locales/en.json +++ b/src/popup/locales/en.json @@ -35,6 +35,7 @@ "seconds": "{n} seconds | {n} second | {n} seconds", "secondsShort": "{n} s", "send": "Send", + "sign": "Sign", "smartContract": "Smart contract", "swap": "Swap", "browser": "Browser", @@ -307,6 +308,15 @@ "content": "Transaction data is either broken or Superhero wallet doesn’t support this type of transactions yet. Signing it may cause unexpected results including loss of your funds or other malicious effects.{0}{0}Be careful and sign the data only if you trust the aepp and its owner." } }, + "confirmUnsafeSign": { + "heading": "would like you to sign a data.", + "superheroChatJwtSign": "would like you to sign a message in order to login.", + "title": "Sign data", + "typeJwt": "JSON Web Token (JWT)", + "typeUnknown": "The data is of unknown type.", + "type": "Type", + "warning": "Sign the data only if you trust the dapp and it's owner." + }, "qrCodeReader": { "grantPermission": "Grant camera permission", "scanQr": "Scan QR code", diff --git a/src/popup/pages/JwtSign.vue b/src/popup/pages/JwtSign.vue new file mode 100644 index 000000000..b336647a7 --- /dev/null +++ b/src/popup/pages/JwtSign.vue @@ -0,0 +1,89 @@ + + + diff --git a/src/popup/pages/Popups/AccountList.vue b/src/popup/pages/Popups/AccountList.vue index cbd21abd4..02c6f139a 100644 --- a/src/popup/pages/Popups/AccountList.vue +++ b/src/popup/pages/Popups/AccountList.vue @@ -6,7 +6,7 @@ data-cy="popup-aex2" > diff --git a/src/popup/pages/Popups/Connect.vue b/src/popup/pages/Popups/Connect.vue index 59f106450..4f286ac31 100644 --- a/src/popup/pages/Popups/Connect.vue +++ b/src/popup/pages/Popups/Connect.vue @@ -6,7 +6,7 @@ data-cy="popup-aex2" > diff --git a/src/popup/pages/Popups/MessageSign.vue b/src/popup/pages/Popups/MessageSign.vue index 50042b339..eddd55b58 100644 --- a/src/popup/pages/Popups/MessageSign.vue +++ b/src/popup/pages/Popups/MessageSign.vue @@ -6,7 +6,7 @@ data-cy="popup-aex2" > diff --git a/src/popup/router/index.ts b/src/popup/router/index.ts index f3deb3d23..a082b75d9 100644 --- a/src/popup/router/index.ts +++ b/src/popup/router/index.ts @@ -13,6 +13,7 @@ import { POPUP_TYPE_SIGN, POPUP_TYPE_MESSAGE_SIGN, POPUP_TYPE_RAW_SIGN, + POPUP_TYPE_UNSAFE_SIGN, POPUP_TYPE_ACCOUNT_LIST, RUNNING_IN_POPUP, PROTOCOLS, @@ -37,6 +38,7 @@ import { ROUTE_POPUP_CONNECT, ROUTE_POPUP_MESSAGE_SIGN, ROUTE_POPUP_RAW_SIGN, + ROUTE_POPUP_UNSAFE_SIGN, ROUTE_POPUP_SIGN_TX, } from './routeNames'; @@ -99,6 +101,7 @@ router.beforeEach(async (to, from, next) => { [POPUP_TYPE_SIGN]: ROUTE_POPUP_SIGN_TX, [POPUP_TYPE_RAW_SIGN]: ROUTE_POPUP_RAW_SIGN, [POPUP_TYPE_MESSAGE_SIGN]: ROUTE_POPUP_MESSAGE_SIGN, + [POPUP_TYPE_UNSAFE_SIGN]: ROUTE_POPUP_UNSAFE_SIGN, }[POPUP_TYPE]; let popupProps: IPopupProps | null = null; diff --git a/src/popup/router/modals.ts b/src/popup/router/modals.ts index 7fd0b48ed..c0575d1f0 100644 --- a/src/popup/router/modals.ts +++ b/src/popup/router/modals.ts @@ -10,6 +10,7 @@ import { MODAL_CONFIRM_ACCOUNT_LIST, MODAL_CONFIRM_CONNECT, MODAL_CONFIRM_RAW_SIGN, + MODAL_CONFIRM_UNSAFE_SIGN, MODAL_CONFIRM_TRANSACTION_SIGN, MODAL_CONSENSUS_INFO, MODAL_DEFAULT, @@ -50,6 +51,7 @@ import ErrorLog from '../components/Modals/ErrorLog.vue'; import FormSelectOptions from '../components/Modals/FormSelectOptions.vue'; import ConfirmTransactionSign from '../components/Modals/ConfirmTransactionSign.vue'; import ConfirmRawSign from '../components/Modals/ConfirmRawSign.vue'; +import ConfirmUnsafeSign from '../components/Modals/ConfirmUnsafeSign.vue'; import QrCodeReader from '../components/Modals/QrCodeReader.vue'; import Help from '../components/Modals/Help.vue'; import AssetSelector from '../components/Modals/AssetSelector.vue'; @@ -107,6 +109,10 @@ export default () => { component: ConfirmRawSign, showInPopupIfWebFrame: true, }); + registerModal(MODAL_CONFIRM_UNSAFE_SIGN, { + component: ConfirmUnsafeSign, + showInPopupIfWebFrame: true, + }); registerModal(MODAL_CONFIRM_CONNECT, { component: ConfirmConnect, showInPopupIfWebFrame: true, diff --git a/src/popup/router/routeNames.ts b/src/popup/router/routeNames.ts index 4255b9c35..f0c227f7f 100644 --- a/src/popup/router/routeNames.ts +++ b/src/popup/router/routeNames.ts @@ -45,4 +45,5 @@ export const ROUTE_POPUP_CONNECT = 'connect'; export const ROUTE_POPUP_SIGN_TX = 'popup-sign-tx'; export const ROUTE_POPUP_ACCOUNT_LIST = 'account-list'; export const ROUTE_POPUP_RAW_SIGN = 'popup-raw-sign'; +export const ROUTE_POPUP_UNSAFE_SIGN = 'popup-unsafe-sign'; export const ROUTE_POPUP_MESSAGE_SIGN = 'message-sign'; diff --git a/src/popup/router/routes.ts b/src/popup/router/routes.ts index e71ecdaad..3fbe36e87 100644 --- a/src/popup/router/routes.ts +++ b/src/popup/router/routes.ts @@ -41,6 +41,7 @@ import { ROUTE_POPUP_SIGN_TX, ROUTE_POPUP_CONNECT, ROUTE_POPUP_RAW_SIGN, + ROUTE_POPUP_UNSAFE_SIGN, ROUTE_POPUP_MESSAGE_SIGN, ROUTE_PERMISSIONS_DETAILS, ROUTE_PERMISSIONS_SETTINGS, @@ -103,6 +104,8 @@ import DefaultPagesRouter from '../components/DefaultPagesRouter.vue'; import AppsBrowser from '../pages/AppsBrowser.vue'; import TransactionDetails from '../../protocols/aeternity/views/TransactionDetails.vue'; +import ConfirmUnsafeSign from '../components/Modals/ConfirmUnsafeSign.vue'; +import JwtSign from '../pages/JwtSign.vue'; export const routes: WalletAppRouteConfig[] = [ ...webIframePopups, @@ -343,6 +346,15 @@ export const routes: WalletAppRouteConfig[] = [ notPersist: true, }, }, + { + name: ROUTE_POPUP_UNSAFE_SIGN, + path: '/popup-unsafe-sign', + component: ConfirmUnsafeSign, + props: true, + meta: { + notPersist: true, + }, + }, { name: ROUTE_POPUP_CONNECT, path: '/connect', @@ -731,6 +743,15 @@ export const routes: WalletAppRouteConfig[] = [ notPersist: true, }, }, + { + name: 'jwt-sign', + path: '/jwt-sign', + component: JwtSign, + meta: { + title: 'signMessage', + notPersist: true, + }, + }, { name: ROUTE_APPS_BROWSER, path: '/apps-browser', diff --git a/src/popup/router/webIframePopups.ts b/src/popup/router/webIframePopups.ts index 840b499c5..15b59c14b 100644 --- a/src/popup/router/webIframePopups.ts +++ b/src/popup/router/webIframePopups.ts @@ -6,6 +6,7 @@ import { MODAL_CONFIRM_CONNECT, MODAL_CONFIRM_ACCOUNT_LIST, MODAL_CONFIRM_RAW_SIGN, + MODAL_CONFIRM_UNSAFE_SIGN, MODAL_CONFIRM_TRANSACTION_SIGN, MODAL_MESSAGE_SIGN, } from '@/constants'; @@ -16,6 +17,7 @@ import { ROUTE_WEB_IFRAME_POPUP } from './routeNames'; import ConfirmConnect from '../pages/Popups/Connect.vue'; import ConfirmAccountList from '../pages/Popups/AccountList.vue'; import ConfirmRawSign from '../components/Modals/ConfirmRawSign.vue'; +import ConfirmUnsafeSign from '../components/Modals/ConfirmUnsafeSign.vue'; import ConfirmTransactionSign from '../components/Modals/ConfirmTransactionSign.vue'; import ConfirmMessageSign from '../pages/Popups/MessageSign.vue'; @@ -50,6 +52,7 @@ const webIframePopups: WalletAppRouteConfig[] = (IS_WEB && IN_POPUP) { name: MODAL_CONFIRM_CONNECT, component: ConfirmConnect }, { name: MODAL_CONFIRM_ACCOUNT_LIST, component: ConfirmAccountList }, { name: MODAL_CONFIRM_RAW_SIGN, component: ConfirmRawSign }, + { name: MODAL_CONFIRM_UNSAFE_SIGN, component: ConfirmUnsafeSign }, { name: MODAL_CONFIRM_TRANSACTION_SIGN, component: ConfirmTransactionSign }, { name: MODAL_MESSAGE_SIGN, component: ConfirmMessageSign }, ].map(({ name, component }): WalletAppRouteConfig => ({ diff --git a/src/protocols/aeternity/libs/AeAccountHdWallet.ts b/src/protocols/aeternity/libs/AeAccountHdWallet.ts index fdb81e87d..6b1284ed8 100644 --- a/src/protocols/aeternity/libs/AeAccountHdWallet.ts +++ b/src/protocols/aeternity/libs/AeAccountHdWallet.ts @@ -59,8 +59,9 @@ export class AeAccountHdWallet extends MemoryAccount { return super.signTransaction(txBase64, { ...options, // Mainly to pass the `fromAccount` property + aeppOrigin: undefined, networkId: this.nodeNetworkId.value, - }); + } as any); } override async signMessage( @@ -81,7 +82,10 @@ export class AeAccountHdWallet extends MemoryAccount { return super.signMessage( message, - options, // Mainly to pass the `fromAccount` property + { + ...options, // Mainly to pass the `fromAccount` property + aeppOrigin: undefined, + }, ); } @@ -92,6 +96,17 @@ export class AeAccountHdWallet extends MemoryAccount { data: string | Uint8Array, options?: Record & InternalOptions, ): Promise { + if (options?.aeppOrigin) { + const { checkOrAskPermission } = usePermissions(); + const permissionGranted = await checkOrAskPermission( + METHODS.unsafeSign, + options?.aeppOrigin, + { ...options, data }, + ); + if (!permissionGranted) { + throw new RpcRejectedByUserError('Rejected by user'); + } + } const { getLastActiveProtocolAccount, getAccountByAddress } = useAccounts(); const account = (options?.fromAccount) ? getAccountByAddress(options.fromAccount) diff --git a/src/types/index.ts b/src/types/index.ts index 63458980e..ab6ddc4e4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -675,6 +675,8 @@ export interface IPopupData { type?: PopupActionType; tx?: ITx; txBase64?: Encoded.Transaction; + aeppOrigin?: string; + data?: string | Uint8Array; } /** diff --git a/src/utils/common.ts b/src/utils/common.ts index 3943f5f75..3d6be420a 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -502,3 +502,13 @@ export function getCleanModalOptions(options: T): Omit Buffer + .from(data.replace(/_/g, '/').replace(/-/g, '+'), 'base64'); + +export const toBase64Url = (data: Buffer | Uint8Array | string): string => Buffer + .from(data) + .toString('base64') + .replace(/\//g, '_') + .replace(/\+/g, '-') + .replace(/=+$/, ''); diff --git a/tests/unit/transaction-tag-list.spec.js b/tests/unit/transaction-tag-list.spec.js index c304f7664..ff092a207 100644 --- a/tests/unit/transaction-tag-list.spec.js +++ b/tests/unit/transaction-tag-list.spec.js @@ -90,8 +90,8 @@ const testCases = [ labels: [], }, { - props: { customTitle: 'customTitle' }, - labels: ['customTitle'], + props: { customLabels: ['customLabels'] }, + labels: ['customLabels'], }, { props: {}, @@ -102,7 +102,7 @@ const testCases = [ describe('TransactionTagList', () => { testCases.forEach(({ props, labels }) => it( `should have correct labels for each type of transaction: for \ - ${props.transaction?.tx?.type ?? props.customTitle}/${props.transaction?.tx?.function}`, + ${props.transaction?.tx?.type ?? props.customLabels}/${props.transaction?.tx?.function}`, () => { const wrapper = mount(TransactionTagList, { global: { plugins: [i18n] }, From 6291f652e63e0814ee49d003512aa0a4b3a85453 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 11 Apr 2024 11:30:22 +0400 Subject: [PATCH 15/28] feat: warn user that sender has been replaced --- .../components/Modals/ConfirmTransactionSign.vue | 13 +++++++++++++ src/popup/locales/en.json | 1 + src/popup/pages/SignTransaction.vue | 3 ++- src/types/index.ts | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/popup/components/Modals/ConfirmTransactionSign.vue b/src/popup/components/Modals/ConfirmTransactionSign.vue index 08f89c502..e0a3d5dae 100644 --- a/src/popup/components/Modals/ConfirmTransactionSign.vue +++ b/src/popup/components/Modals/ConfirmTransactionSign.vue @@ -37,6 +37,12 @@ class="reason" data-cy="reason" /> + + {{ $t('modals.confirmTransactionSign.senderReplaced') }} + { From a1a61936169636dd0e25efe91be550889a499129 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 11 Apr 2024 15:30:22 +0400 Subject: [PATCH 16/28] fix: allow to open index html via redirect --- src/manifest.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/manifest.json b/src/manifest.json index 89c408248..041990549 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -38,6 +38,10 @@ } ], "web_accessible_resources": [ + { + "resources": [ "/index.html" ], + "matches": [ "" ] + }, { "resources": [ "CameraRequestPermission.html", From 47aa4bd6978302074f384cc0c5e1871b5225ad29 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 12 Apr 2024 09:58:05 +0400 Subject: [PATCH 17/28] build: run npm audit fix --- package-lock.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index a8aa2587c..42c032b44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14768,9 +14768,9 @@ } }, "node_modules/express": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", - "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "dependencies": { "accepts": "~1.3.8", @@ -14778,7 +14778,7 @@ "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -14810,9 +14810,9 @@ } }, "node_modules/express/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, "engines": { "node": ">= 0.6" @@ -27364,9 +27364,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -29708,9 +29708,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "dev": true, "dependencies": { "colorette": "^2.0.10", From 27c1fb89abafc7bd27a9762ff0782723458082bd Mon Sep 17 00:00:00 2001 From: martinkaintas Date: Thu, 28 Mar 2024 13:10:23 +0200 Subject: [PATCH 18/28] fix: eth max button should stay on --- src/popup/components/InputAmount.vue | 4 +- src/popup/components/InputField.vue | 19 ++++++ src/popup/components/TokenAmount.vue | 58 +++++++++++++++---- .../TransferSend/TransferSendAmount.vue | 4 +- src/popup/components/TransferSendFormBase.vue | 1 + .../aeternity/components/TransferSendForm.vue | 41 ++++++------- .../ethereum/components/TransferSendForm.vue | 28 +++++++-- src/styles/global.scss | 8 +++ 8 files changed, 126 insertions(+), 37 deletions(-) diff --git a/src/popup/components/InputAmount.vue b/src/popup/components/InputAmount.vue index 6295886c2..eb647737d 100644 --- a/src/popup/components/InputAmount.vue +++ b/src/popup/components/InputAmount.vue @@ -7,6 +7,7 @@ :model-value="modelValue" :label="label" :message="$attrs.message" + :blink-on-change="blinkOnChange" @update:modelValue="$emit('update:modelValue', $event)" > @@ -60,6 +74,7 @@ export default defineComponent({ }, props: { fee: { type: Number, default: 0 }, + maxFee: { type: Number, default: undefined }, feeSymbol: { type: String, required: true }, customTitle: { type: String, default: '' }, protocol: { type: String as PropType, required: true }, diff --git a/src/popup/locales/en.json b/src/popup/locales/en.json index 1c131ad4b..a9c1625a2 100644 --- a/src/popup/locales/en.json +++ b/src/popup/locales/en.json @@ -400,6 +400,8 @@ }, "transaction": { "fee": "Transaction fee", + "estimatedFee": "Estimated transaction fee", + "maxFee": "Maximum transaction fee", "advancedDetails": "Advanced transaction details", "type": { "spendTx": "Spend", @@ -1199,7 +1201,7 @@ "addressGeneric": "Invalid {protocol} address", "minValue": "This field must be {0} or more", "minValueExclusive": "This field must be more than {0}", - "enoughCoin": "Not enough {0} balance", + "enoughCoin": "Not enough {0} balance to pay transaction fee", "enoughAeSigner": "Insufficient balance on your signing account to propose a transaction", "maxLength": "The value should be no longer than {0} characters", "maxValue": "This field must be {0} or less", diff --git a/src/protocols/ethereum/components/TransferSendForm.vue b/src/protocols/ethereum/components/TransferSendForm.vue index 824cd0539..c9f752f23 100644 --- a/src/protocols/ethereum/components/TransferSendForm.vue +++ b/src/protocols/ethereum/components/TransferSendForm.vue @@ -3,6 +3,7 @@ v-bind="$attrs" :transfer-data="transferData" :fee="numericFee" + :max-fee="numericMaxFee" :fee-symbol="ETH_COIN_SYMBOL" :protocol="PROTOCOLS.ethereum" :custom-title="$t('modals.send.sendAsset', { name: ETH_COIN_NAME })" @@ -27,10 +28,10 @@ :protocol="PROTOCOLS.ethereum" :blink-on-change="shouldUseMaxAmount" :validation-rules="{ - ...+balance.minus(fee) > 0 + ...+balance.minus(maxFee) > 0 ? { max_value: max } : {}, - enough_coin: [fee.toString(), ETH_COIN_SYMBOL], + enough_coin: [maxFee.toString(), ETH_COIN_SYMBOL], }" @update:model-value="shouldUseMaxAmount = false" @asset-selected="handleAssetChange" @@ -171,6 +172,7 @@ export default defineComponent({ const { max } = useEthMaxAmount({ formModel, fee: maxFee }); const numericFee = computed(() => +fee.value.toFixed()); + const numericMaxFee = computed(() => +maxFee.value.toFixed()); const recipientPlaceholderText = `${t('modals.send.recipientPlaceholderProtocol', { name: PROTOCOLS.ethereum })} ${t('modals.send.recipientPlaceholderENS')}`; @@ -253,11 +255,12 @@ export default defineComponent({ NETWORK_TYPE_TESTNET, formModel, activeNetwork, - fee, + maxFee, feeList, recipientPlaceholderText, feeSelectedIndex, numericFee, + numericMaxFee, errors, balance, max, From 4ef02190f7f51647f5d1043bb995a7e2585d7130 Mon Sep 17 00:00:00 2001 From: martinkaintas Date: Wed, 20 Mar 2024 16:06:58 +0200 Subject: [PATCH 20/28] feat: add support for biometric login on mobile apps --- android/app/capacitor.build.gradle | 3 + android/capacitor.settings.gradle | 9 + ios/App/App/Info.plist | 2 + ios/App/Podfile | 4 +- package-lock.json | 24 +++ package.json | 2 + src/composables/auth.ts | 145 ++++++++++++++ src/composables/index.ts | 1 + src/composables/ui.ts | 29 +++ src/constants/common.ts | 5 +- src/icons/fingerprint.svg | 3 + src/icons/lock.svg | 3 + src/popup/components/Header.vue | 1 + src/popup/components/IconBoxed.vue | 38 +++- src/popup/components/Modal.vue | 9 + src/popup/components/Modals/AccountImport.vue | 3 + .../components/Modals/EnableSecureLogin.vue | 150 +++++++++++++++ src/popup/components/Modals/SecureLogin.vue | 121 ++++++++++++ src/popup/components/SwitchButton.vue | 6 +- src/popup/locales/en.json | 36 +++- src/popup/pages/Index.vue | 3 + src/popup/pages/SecureLoginSettings.vue | 181 ++++++++++++++++++ src/popup/pages/Settings.vue | 26 ++- src/popup/router/index.ts | 9 +- src/popup/router/modals.ts | 10 + src/popup/router/routeNames.ts | 1 + src/popup/router/routes.ts | 11 ++ src/styles/typography.scss | 7 + tests/unit/snapshot.spec.js | 3 + 29 files changed, 831 insertions(+), 14 deletions(-) create mode 100644 src/composables/auth.ts create mode 100644 src/icons/fingerprint.svg create mode 100644 src/icons/lock.svg create mode 100644 src/popup/components/Modals/EnableSecureLogin.vue create mode 100644 src/popup/components/Modals/SecureLogin.vue create mode 100644 src/popup/pages/SecureLoginSettings.vue diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index baba09932..32899a9da 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -9,16 +9,19 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { + implementation project(':aparajita-capacitor-biometric-auth') implementation project(':capacitor-community-barcode-scanner') implementation project(':capacitor-app') implementation project(':capacitor-camera') implementation project(':capacitor-clipboard') + implementation project(':capacitor-filesystem') implementation project(':capacitor-haptics') implementation project(':capacitor-keyboard') implementation project(':capacitor-network') implementation project(':capacitor-share') implementation project(':capacitor-splash-screen') implementation project(':capacitor-status-bar') + implementation project(':capacitor-native-settings') } diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 9d6b41a97..90e8f2d08 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -2,6 +2,9 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') +include ':aparajita-capacitor-biometric-auth' +project(':aparajita-capacitor-biometric-auth').projectDir = new File('../node_modules/@aparajita/capacitor-biometric-auth/android') + include ':capacitor-community-barcode-scanner' project(':capacitor-community-barcode-scanner').projectDir = new File('../node_modules/@capacitor-community/barcode-scanner/android') @@ -14,6 +17,9 @@ project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/c include ':capacitor-clipboard' project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android') +include ':capacitor-filesystem' +project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android') + include ':capacitor-haptics' project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android') @@ -31,3 +37,6 @@ project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capa include ':capacitor-status-bar' project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android') + +include ':capacitor-native-settings' +project(':capacitor-native-settings').projectDir = new File('../node_modules/capacitor-native-settings/android') diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index abb2013b3..d8c5f3c34 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -2,6 +2,8 @@ + NSFaceIDUsageDescription + $(PRODUCT_NAME) Authentication with TouchId or FaceID CFBundleDevelopmentRegion en CFBundleDisplayName diff --git a/ios/App/Podfile b/ios/App/Podfile index f61e60969..61d172b87 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -11,17 +11,19 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' + pod 'AparajitaCapacitorBiometricAuth', :path => '../../node_modules/@aparajita/capacitor-biometric-auth' pod 'CapacitorCommunityBarcodeScanner', :path => '../../node_modules/@capacitor-community/barcode-scanner' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera' pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard' - pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem' + pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard' pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network' pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' + pod 'CapacitorNativeSettings', :path => '../../node_modules/capacitor-native-settings' pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins' end diff --git a/package-lock.json b/package-lock.json index a8aa2587c..f1b4c4014 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@aeternity/bip39": "^0.1.0", "@aeternity/hd-wallet": "^0.2.0", "@aeternity/json-bigint": "^0.3.1", + "@aparajita/capacitor-biometric-auth": "^7.1.1", "@awesome-cordova-plugins/screen-orientation": "^6.4.0", "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", "@capacitor-community/barcode-scanner": "^4.0.1", @@ -48,6 +49,7 @@ "bip32": "^4.0.0", "bitcoinjs-lib": "^6.1.3", "camelcase-keys-deep": "^0.1.0", + "capacitor-native-settings": "^5.0.1", "cordova-plugin-screen-orientation": "^3.0.3", "dayjs": "^1.11.9", "detect-browser": "^5.3.0", @@ -272,6 +274,20 @@ "node": ">=6.0.0" } }, + "node_modules/@aparajita/capacitor-biometric-auth": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@aparajita/capacitor-biometric-auth/-/capacitor-biometric-auth-7.1.1.tgz", + "integrity": "sha512-hc9mdiH+Se1/Y+H2Wn0kp3/uo3JzgSYkgJozpjZMgx9o5hCvNvQA8m+HMxFd4cpM3XYKT15y0DYcECWcYrJhEw==", + "dependencies": { + "@capacitor/android": "^5.6.0", + "@capacitor/app": "^5.0.6", + "@capacitor/core": "^5.6.0", + "@capacitor/ios": "^5.6.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@awesome-cordova-plugins/core": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/@awesome-cordova-plugins/core/-/core-6.6.0.tgz", @@ -10257,6 +10273,14 @@ "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.0.0.tgz", "integrity": "sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w==" }, + "node_modules/capacitor-native-settings": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/capacitor-native-settings/-/capacitor-native-settings-5.0.1.tgz", + "integrity": "sha512-HH7GCyaVLwWP4FEVqvOQM+cHWveQUjsA2128wfZ5yr9eOQrECVWzO5UXH9ZlikpWi1C7exnovcyEr3TlYO6l8w==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", diff --git a/package.json b/package.json index 0da603362..be4ad48c3 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@aeternity/bip39": "^0.1.0", "@aeternity/hd-wallet": "^0.2.0", "@aeternity/json-bigint": "^0.3.1", + "@aparajita/capacitor-biometric-auth": "^7.1.1", "@awesome-cordova-plugins/screen-orientation": "^6.4.0", "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", "@capacitor-community/barcode-scanner": "^4.0.1", @@ -70,6 +71,7 @@ "bip32": "^4.0.0", "bitcoinjs-lib": "^6.1.3", "camelcase-keys-deep": "^0.1.0", + "capacitor-native-settings": "^5.0.1", "cordova-plugin-screen-orientation": "^3.0.3", "dayjs": "^1.11.9", "detect-browser": "^5.3.0", diff --git a/src/composables/auth.ts b/src/composables/auth.ts new file mode 100644 index 000000000..a823b2d97 --- /dev/null +++ b/src/composables/auth.ts @@ -0,0 +1,145 @@ +import { BiometricAuth } from '@aparajita/capacitor-biometric-auth'; +import { + ref, + readonly, + watch, +} from 'vue'; + +import { tg as t } from '@/popup/plugins/i18n'; +import { IS_MOBILE_APP, MODAL_ENABLE_SECURE_LOGIN, MODAL_SECURE_LOGIN } from '@/constants'; +import { watchUntilTruthy } from '@/utils'; +import { useUi } from './ui'; +import { useModals } from './modals'; +import { createCustomScopedComposable } from './composablesHelpers'; + +export const useAuth = createCustomScopedComposable(() => { + let composableInitialized = false; + let isSecureLoginAvailable = false; + const isBiometryAvailabilityUpdating = ref(false); + const isBiometryAvailabilityChecked = ref(false); + const isAuthenticated = ref(false); + const isAuthenticating = ref(false); + + const { + setSecureLoginEnabled, + isSecureLoginEnabled, + lastTimeAppWasActive, + secureLoginTimeout, + isAppActive, + } = useUi(); + const { openModal } = useModals(); + + async function checkSecureLoginAvailability({ forceUpdate = false } = {}) { + if (isBiometryAvailabilityUpdating.value) { + await watchUntilTruthy(() => !isBiometryAvailabilityUpdating.value); + } else if (!isBiometryAvailabilityChecked.value || forceUpdate) { + isBiometryAvailabilityUpdating.value = true; + isSecureLoginAvailable = (await BiometricAuth.checkBiometry()).isAvailable; + isBiometryAvailabilityChecked.value = true; + isBiometryAvailabilityUpdating.value = false; + } + if (!isSecureLoginAvailable) { + setSecureLoginEnabled(false); + } + return isSecureLoginAvailable; + } + + /** + * Prompts the user to authenticate using biometric authentication. + * Returns a promise that resolves when the user is authenticated + * or if biometric authentication is not available. + */ + async function authenticate(): Promise { + if ( + !await checkSecureLoginAvailability() + || !isSecureLoginEnabled.value + || isAuthenticated.value + ) { + return Promise.resolve(); + } + + try { + return BiometricAuth.authenticate({ + reason: t('biometricAuth.reason'), + cancelTitle: t('common.cancel'), + allowDeviceCredential: true, + iosFallbackTitle: t('biometricAuth.fallbackTitle'), + androidTitle: t('biometricAuth.title'), + androidSubtitle: t('biometricAuth.subtitle'), + androidConfirmationRequired: false, + }).then(() => { + isAuthenticated.value = true; + }); + } catch (error) { + return Promise.reject(error); + } + } + + function logout() { + isAuthenticated.value = false; + } + + async function openSecureLoginModal() { + if ( + !isAuthenticating.value + && await checkSecureLoginAvailability() + && isSecureLoginEnabled.value + && !isAuthenticated.value + ) { + isAuthenticating.value = true; + await openModal(MODAL_SECURE_LOGIN); + // wait before closing the modal so that app doesn't register a false app resume event + await new Promise((resolve) => setTimeout(resolve, 200)); + isAuthenticating.value = false; + } + } + + async function openEnableSecureLoginModal() { + if (IS_MOBILE_APP) { + openModal(MODAL_ENABLE_SECURE_LOGIN); + } + } + + (async () => { + if (composableInitialized) { + return; + } + composableInitialized = true; + await checkSecureLoginAvailability(); + })(); + + watch( + isAppActive, + async (isActive, wasActive) => { + // App resumed from background + // Check if biometric auth is still available + await checkSecureLoginAvailability({ forceUpdate: true }); + if ( + !isAuthenticating.value + && isSecureLoginEnabled.value + && isActive + && !wasActive + && IS_MOBILE_APP + ) { + if (isAuthenticated.value && lastTimeAppWasActive.value) { + const elapsedTime = Date.now() - lastTimeAppWasActive.value; + if (elapsedTime > secureLoginTimeout.value) { + logout(); + await openSecureLoginModal(); + } + } else if (!isAuthenticated.value) { + await openSecureLoginModal(); + } + } + }, + ); + + return { + isAuthenticated: readonly(isAuthenticated), + checkSecureLoginAvailability, + authenticate, + logout, + openSecureLoginModal, + openEnableSecureLoginModal, + }; +}); diff --git a/src/composables/index.ts b/src/composables/index.ts index c5f04049e..deac2acbc 100644 --- a/src/composables/index.ts +++ b/src/composables/index.ts @@ -30,3 +30,4 @@ export * from './fungibleTokens'; export * from './scrollTransactions'; export * from './languages'; export * from './permissions'; +export * from './auth'; diff --git a/src/composables/ui.ts b/src/composables/ui.ts index 2d5b642a8..8ca740d96 100644 --- a/src/composables/ui.ts +++ b/src/composables/ui.ts @@ -3,6 +3,7 @@ import { onBeforeUnmount, onMounted, ref, + watch, } from 'vue'; import { RouteLocationRaw } from 'vue-router'; import { STORAGE_KEYS } from '@/constants'; @@ -14,6 +15,8 @@ import { useStorageRef } from './storageRef'; export interface IOtherSettings { isSeedBackedUp?: boolean; saveErrorLog?: boolean; + isSecureLoginEnabled?: boolean; + secureLoginTimeout?: number; } const homeRouteName = ref(ROUTE_ACCOUNT); @@ -21,6 +24,7 @@ const isAppActive = ref(false); const isLoaderVisible = ref(false); const loginTargetLocation = ref({ name: ROUTE_ACCOUNT }); const qrScannerOpen = ref(false); +const lastTimeAppWasActive = ref(); const hiddenCards = useStorageRef( [], @@ -43,6 +47,8 @@ const otherSettings = useStorageRef( const isSeedBackedUp = computed(() => !!otherSettings.value.isSeedBackedUp); const saveErrorLog = computed(() => !!otherSettings.value.saveErrorLog); +const isSecureLoginEnabled = computed(() => !!otherSettings.value.isSecureLoginEnabled); +const secureLoginTimeout = computed(() => otherSettings.value.secureLoginTimeout ?? 0); export function useUi() { function setHomeRouteName(routeName: string, onChangeCallback?: () => any) { @@ -82,6 +88,14 @@ export function useUi() { otherSettings.value.saveErrorLog = val; } + function setSecureLoginEnabled(val: boolean) { + otherSettings.value.isSecureLoginEnabled = val; + } + + function setSecureLoginTimeout(val: number) { + otherSettings.value.secureLoginTimeout = val; + } + function initVisibilityListeners() { handleVisibilityChange(); onMounted(() => { @@ -93,6 +107,16 @@ export function useUi() { }); } + watch( + isAppActive, + async (isActive, wasActive) => { + // App went to background + if (wasActive && !isActive) { + lastTimeAppWasActive.value = Date.now(); + } + }, + ); + function resetUiSettings() { hiddenCards.value = []; otherSettings.value = {}; @@ -107,6 +131,9 @@ export function useUi() { isLoaderVisible, isSeedBackedUp, saveErrorLog, + isSecureLoginEnabled, + secureLoginTimeout, + lastTimeAppWasActive, initVisibilityListeners, setCardHidden, setBackedUpSeed, @@ -116,5 +143,7 @@ export function useUi() { setQrScanner, setLoaderVisible, resetUiSettings, + setSecureLoginEnabled, + setSecureLoginTimeout, }; } diff --git a/src/constants/common.ts b/src/constants/common.ts index df8582510..3d1e1acb5 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -118,6 +118,7 @@ export const STORAGE_KEYS = { transactionsLoaded: 'transactions-loaded', transactionsPending: 'transactions-pending', transferSendData: 'transfer-send-data', + secureLogin: 'secure-login', } as const; export const CURRENCIES: ICurrency[] = [ @@ -318,6 +319,8 @@ export const MODAL_TRANSFER_SEND = 'transfer-send'; export const MODAL_DAPP_BROWSER_ACTIONS = 'browser-actions'; export const MODAL_WARNING_DAPP_BROWSER = 'warning-dapp-browser'; export const MODAL_CLAIM_GIFT_CARD = 'claim-gift-card'; +export const MODAL_SECURE_LOGIN = 'secure-login'; +export const MODAL_ENABLE_SECURE_LOGIN = 'enable-secure-login'; export const POPUP_TYPE_CONNECT = 'connectConfirm'; export const POPUP_TYPE_ACCOUNT_LIST = 'account-list'; @@ -389,7 +392,7 @@ export const ICON_SIZES = { md: 'md', // 20px lg: 'lg', // 24px xl: 'xl', // 28px - xxl: 'xxl', // 30px + xxl: 'xxl', // 44px } as const; export const TRANSFER_SEND_STEPS = { diff --git a/src/icons/fingerprint.svg b/src/icons/fingerprint.svg new file mode 100644 index 000000000..b863d5e6a --- /dev/null +++ b/src/icons/fingerprint.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/lock.svg b/src/icons/lock.svg new file mode 100644 index 000000000..6aa771b4c --- /dev/null +++ b/src/icons/lock.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/popup/components/Header.vue b/src/popup/components/Header.vue index d839d0bd8..9509b439e 100644 --- a/src/popup/components/Header.vue +++ b/src/popup/components/Header.vue @@ -164,6 +164,7 @@ export default defineComponent({ networkEdit: () => t('pages.titles.networkEdit'), notFound: () => t('pages.titles.notFound'), multisigProposalDetails: () => t('pages.titles.multisigProposalDetails'), + secureLogin: () => t('pages.titles.secureLogin'), }; const currentHomeRouteName = computed( diff --git a/src/popup/components/IconBoxed.vue b/src/popup/components/IconBoxed.vue index 026da4f3d..dac898654 100644 --- a/src/popup/components/IconBoxed.vue +++ b/src/popup/components/IconBoxed.vue @@ -1,5 +1,8 @@