diff --git a/package.json b/package.json index 4d5d7997754..3a934219491 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "@subwallet/react-ui": "5.1.2-b79", "@subwallet/ui-keyring": "^0.1.12", "@types/bn.js": "^5.1.6", - "@zondax/ledger-substrate": "1.0.1", + "@zondax/ledger-substrate": "1.1.2", "babel-core": "^7.0.0-bridge.0", "babel-jest": "^29.3.1", "browserify-sign": "^4.2.2", diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 55e17c5cf93..5533914fa1e 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -850,6 +850,7 @@ export interface CreateHardwareAccountItem { isEthereum: boolean; isGeneric: boolean; isLedgerRecovery?: boolean; + isSubstrateECDSA?: boolean; } export interface RequestAccountCreateHardwareV2 extends CreateHardwareAccountItem { @@ -1433,10 +1434,17 @@ export interface LedgerNetwork { isRecovery?: boolean; /** Slip44 in the derivation path */ slip44: number; + /** Signature substrate scheme */ + scheme?: LEDGER_SCHEME; +} + +export const enum LEDGER_SCHEME { + ED25519 = 0, + ECDSA = 2 } export interface MigrationLedgerNetwork extends Omit { - ss58_addr_type: number + ss58_addr_type: number; } /// Qr Sign diff --git a/packages/extension-base/src/background/types.ts b/packages/extension-base/src/background/types.ts index d79cc5d13ea..df878b7cba8 100644 --- a/packages/extension-base/src/background/types.ts +++ b/packages/extension-base/src/background/types.ts @@ -149,8 +149,9 @@ export type AccountAuthType = 'substrate' | 'evm' | 'ton' | 'cardano'; export interface RequestAuthorizeTab { origin: string; accountAuthTypes?: AccountAuthType[]; - allowedAccounts?: string[] - reConfirm?: boolean + allowedAccounts?: string[]; + reConfirm?: boolean; + isSubstrateConnector?: boolean; } export interface RequestAuthorizeApprove { @@ -245,7 +246,8 @@ export interface RequestAccountBatchExport { export interface RequestAccountList { anyType?: boolean; - accountAuthType?: AccountAuthType + accountAuthType?: AccountAuthType; + isSubstrateConnector?: boolean; } export interface RequestAccountSubscribe { diff --git a/packages/extension-base/src/core/logic-validation/request.ts b/packages/extension-base/src/core/logic-validation/request.ts index 4095109b948..59f218bbe44 100644 --- a/packages/extension-base/src/core/logic-validation/request.ts +++ b/packages/extension-base/src/core/logic-validation/request.ts @@ -327,7 +327,7 @@ export async function validationEvmDataTransactionMiddleware (koni: KoniState, u const errors: Error[] = payload.errors || []; let estimateGas = ''; const transactionParams = payload.payloadAfterValidated as EvmSendTransactionParams; - const { address: fromAddress, networkKey } = payload; + const { address: fromAddress, networkKey, pair: pair_ } = payload; const evmApi = koni.getEvmApi(networkKey || ''); const web3 = evmApi?.api; @@ -371,6 +371,16 @@ export async function validationEvmDataTransactionMiddleware (koni: KoniState, u handleError('the sender address must be the ethereum address type'); } + const pair = pair_ || keyring.getPair(fromAddress); + + if (!pair) { + handleError('Not found address to sign'); + } + + if (pair_?.meta.isSubstrateECDSA) { + handleError('Substrate account can not send this transaction'); + } + if (transaction.to && !isEthereumAddress(transaction.to)) { handleError('invalid recipient address'); } @@ -535,6 +545,14 @@ export async function validationEvmSignMessageMiddleware (koni: KoniState, url: const pair = pair_ || keyring.getPair(address); + if (!pair) { + handleError('Not found address to sign'); + } + + if (pair_?.meta.isSubstrateECDSA) { + handleError('Substrate account can not sign this message'); + } + if (method) { if (['eth_sign', 'personal_sign', 'eth_signTypedData', 'eth_signTypedData_v1', 'eth_signTypedData_v3', 'eth_signTypedData_v4'].indexOf(method) < 0) { handleError('Unsupported action'); diff --git a/packages/extension-base/src/core/types.ts b/packages/extension-base/src/core/types.ts index 41d9a680f31..3faf23104b0 100644 --- a/packages/extension-base/src/core/types.ts +++ b/packages/extension-base/src/core/types.ts @@ -4,13 +4,14 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { AccountJson } from '@subwallet/extension-base/types'; -export type LedgerMustCheckType = 'polkadot' | 'migration' | 'unnecessary'; +export type LedgerMustCheckType = 'polkadot' | 'migration' | 'polkadot_ecdsa' | 'unnecessary'; export enum ValidationCondition { IS_NOT_NULL = 'IS_NOT_NULL', IS_ADDRESS = 'IS_ADDRESS', IS_VALID_ADDRESS_FOR_ECOSYSTEM = 'IS_VALID_ADDRESS_FOR_ECOSYSTEM', IS_VALID_SUBSTRATE_ADDRESS_FORMAT = 'IS_VALID_SUBSTRATE_ADDRESS_FORMAT', + IS_VALID_EVM_ADDRESS_FORMAT = 'IS_VALID_EVM_ADDRESS_FORMAT', IS_VALID_TON_ADDRESS_FORMAT = 'IS_VALID_TON_ADDRESS_FORMAT', IS_VALID_CARDANO_ADDRESS_FORMAT = 'IS_VALID_CARDANO_ADDRESS_FORMAT', IS_NOT_DUPLICATE_ADDRESS = 'IS_NOT_DUPLICATE_ADDRESS', diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index bb2d6c91711..ffca096a479 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -1405,7 +1405,7 @@ export default class KoniExtension { } private async makeTransfer (inputData: RequestSubmitTransfer): Promise { - const { chain, feeCustom, feeOption, from, to, tokenPayFeeSlug, tokenSlug, transferAll, transferBounceable, value } = inputData; + const { chain, feeCustom, feeOption, from, isSubstrateTransaction, to, tokenPayFeeSlug, tokenSlug, transferAll, transferBounceable, value } = inputData; const transferTokenInfo = this.#koniState.chainService.getAssetBySlug(tokenSlug); const errors = validateTransferRequest(transferTokenInfo, from, to, value, transferAll); const warnings: TransactionWarning[] = []; @@ -1427,7 +1427,7 @@ export default class KoniExtension { const transferTokenAvailable = await this.getAddressTransferableBalance({ address: from, networkKey: chain, token: tokenSlug, extrinsicType }); try { - if (isEthereumAddress(from) && isEthereumAddress(to) && _isTokenTransferredByEvm(transferTokenInfo)) { + if (isEthereumAddress(from) && isEthereumAddress(to) && _isTokenTransferredByEvm(transferTokenInfo) && !isSubstrateTransaction) { chainType = ChainType.EVM; const txVal: string = transferAll ? transferTokenAvailable.value : (value || '0'); const evmApi = this.#koniState.getEvmApi(chain); diff --git a/packages/extension-base/src/koni/background/handlers/Tabs.ts b/packages/extension-base/src/koni/background/handlers/Tabs.ts index a8645a00898..2566e022e98 100644 --- a/packages/extension-base/src/koni/background/handlers/Tabs.ts +++ b/packages/extension-base/src/koni/background/handlers/Tabs.ts @@ -44,7 +44,7 @@ interface AccountSub { url: string; } -function transformAccountsV2 (accounts: SubjectInfo, anyType = false, authInfo?: AuthUrlInfo, accountAuthTypes?: AccountAuthType[]): InjectedAccount[] { +function transformAccountsV2 (accounts: SubjectInfo, anyType = false, authInfo?: AuthUrlInfo, accountAuthTypes?: AccountAuthType[], isSubstrateConnector?: boolean): InjectedAccount[] { const accountSelected = authInfo ? ( authInfo.isAllowed @@ -80,6 +80,11 @@ function transformAccountsV2 (accounts: SubjectInfo, anyType = false, authInfo?: return false; } + // If the dApp has not connected to the Substrate type yet, we do not return Substrate ECDSA accounts. + if (type === 'ethereum' && json.meta.isSubstrateECDSA && !isSubstrateConnector) { + return false; + } + return true; } else { return true; @@ -316,7 +321,7 @@ export default class KoniTabs { return authList[shortenUrl]; } - private async accountsListV2 (url: string, { accountAuthType, anyType }: RequestAccountList): Promise { + private async accountsListV2 (url: string, { accountAuthType, anyType, isSubstrateConnector }: RequestAccountList): Promise { const authInfo = await this.getAuthInfo(url); const accountAuthTypes: AccountAuthType[] = []; @@ -341,7 +346,7 @@ export default class KoniTabs { } } - return transformAccountsV2(this.#koniState.keyringService.context.pairs, anyType, authInfo, accountAuthTypes); + return transformAccountsV2(this.#koniState.keyringService.context.pairs, anyType, authInfo, accountAuthTypes, isSubstrateConnector); } // TODO: Update logic @@ -369,7 +374,7 @@ export default class KoniTabs { const accounts = this.#koniState.keyringService.context.pairs; - return cb(transformAccountsV2(accounts, false, authInfo, accountAuthTypes)); + return cb(transformAccountsV2(accounts, false, authInfo, accountAuthTypes, true)); }) .catch(console.error); }), diff --git a/packages/extension-base/src/page/index.ts b/packages/extension-base/src/page/index.ts index 2d2ec953265..0effa2a96bc 100644 --- a/packages/extension-base/src/page/index.ts +++ b/packages/extension-base/src/page/index.ts @@ -59,7 +59,7 @@ export function sendMessage (message: TMessag export async function enable (origin: string, opt?: AuthRequestOption): Promise { const accountAuthTypes: AccountAuthType[] = opt?.accountAuthType === 'both' ? ['substrate', 'evm'] : [opt?.accountAuthType || 'substrate']; - await sendMessage('pub(authorize.tabV2)', { origin, accountAuthTypes }); + await sendMessage('pub(authorize.tabV2)', { origin, accountAuthTypes, isSubstrateConnector: true }); return new Injected(sendMessage); } diff --git a/packages/extension-base/src/page/substrate/Accounts.ts b/packages/extension-base/src/page/substrate/Accounts.ts index 42e8cce5f7a..5bcda6cc2b1 100644 --- a/packages/extension-base/src/page/substrate/Accounts.ts +++ b/packages/extension-base/src/page/substrate/Accounts.ts @@ -13,7 +13,7 @@ export default class Accounts implements InjectedAccounts { } public get (anyType?: boolean): Promise { - return sendRequest('pub(accounts.listV2)', { anyType }); + return sendRequest('pub(accounts.listV2)', { anyType, isSubstrateConnector: true }); } public subscribe (cb: (accounts: InjectedAccount[]) => unknown): Unsubcall { diff --git a/packages/extension-base/src/services/chain-service/utils/index.ts b/packages/extension-base/src/services/chain-service/utils/index.ts index be76faf0c8b..b48b97408fc 100644 --- a/packages/extension-base/src/services/chain-service/utils/index.ts +++ b/packages/extension-base/src/services/chain-service/utils/index.ts @@ -159,6 +159,10 @@ export function _isChainEvmCompatible (chainInfo: _ChainInfo) { return !!chainInfo.evmInfo; } +export function _isSubstrateEvmCompatibleChain (chainInfo: _ChainInfo) { + return !!chainInfo.evmInfo && !!chainInfo.substrateInfo; +} + export function _isChainBitcoinCompatible (chainInfo: _ChainInfo) { return !!chainInfo.bitcoinInfo; } diff --git a/packages/extension-base/src/services/keyring-service/context/handlers/Ledger.ts b/packages/extension-base/src/services/keyring-service/context/handlers/Ledger.ts index 04babfc65a1..238ade2bdc8 100644 --- a/packages/extension-base/src/services/keyring-service/context/handlers/Ledger.ts +++ b/packages/extension-base/src/services/keyring-service/context/handlers/Ledger.ts @@ -3,7 +3,7 @@ import { RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2 } from '@subwallet/extension-base/background/KoniTypes'; import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; -import { KeyringPair, KeyringPair$Meta } from '@subwallet/keyring/types'; +import { KeypairType, KeyringPair, KeyringPair$Meta } from '@subwallet/keyring/types'; import { keyring } from '@subwallet/ui-keyring'; import { t } from 'i18next'; @@ -86,7 +86,7 @@ export class AccountLedgerHandler extends AccountBaseHandler { const pairs: KeyringPair[] = []; for (const account of accounts) { - const { accountIndex, address, addressOffset, genesisHash, hardwareType, isEthereum, isGeneric, isLedgerRecovery, name, originGenesisHash } = account; + const { accountIndex, address, addressOffset, genesisHash, hardwareType, isEthereum, isGeneric, isLedgerRecovery, isSubstrateECDSA, name, originGenesisHash } = account; const baseMeta: KeyringPair$Meta = { name, @@ -96,10 +96,16 @@ export class AccountLedgerHandler extends AccountBaseHandler { genesisHash, originGenesisHash, isGeneric, - isLedgerRecovery + isLedgerRecovery, + isSubstrateECDSA }; - const type = isEthereum ? 'ethereum' : 'sr25519'; + let type: KeypairType = 'sr25519'; + + if (isEthereum || isSubstrateECDSA) { + type = 'ethereum'; + } + const pair = keyring.keyring.createFromAddress( address, { diff --git a/packages/extension-base/src/services/request-service/handler/AuthRequestHandler.ts b/packages/extension-base/src/services/request-service/handler/AuthRequestHandler.ts index 1c2b1eaba2d..ad662c56a37 100644 --- a/packages/extension-base/src/services/request-service/handler/AuthRequestHandler.ts +++ b/packages/extension-base/src/services/request-service/handler/AuthRequestHandler.ts @@ -60,6 +60,10 @@ export default class AuthRequestHandler { value.currentNetworkMap = {}; } + if (value.accountAuthTypes?.includes('substrate')) { + value.isSubstrateConnector = true; + } + acc[key] = { ...value }; return acc; @@ -97,6 +101,22 @@ export default class AuthRequestHandler { return addressList.reduce((addressList, v) => ({ ...addressList, [v]: value }), {}); } + private getEcdsaAddressList (): Set { + const addressList = Object.keys(this.keyringService.context.pairs); + const pairs = this.keyringService.context.pairs; + const ecdsaAddressList = new Set(); + + addressList.forEach((address) => { + const pair = pairs[address]; + + if (pair && pair.json.meta.isSubstrateECDSA) { + ecdsaAddressList.add(address); + } + }); + + return ecdsaAddressList; + } + public get numAuthRequestsV2 (): number { return Object.keys(this.#authRequestsV2).length; } @@ -193,6 +213,7 @@ export default class AuthRequestHandler { private authCompleteV2 = (id: string, url: string, resolve: (result: boolean) => void, reject: (error: Error) => void): Resolver => { const isAllowedMap = this.getAddressList(); + const ecdsaAddressList = this.getEcdsaAddressList(); const complete = (result: boolean | Error, cb: () => void, accounts?: string[]) => { const isAllowed = result === true; @@ -212,7 +233,7 @@ export default class AuthRequestHandler { }); } - const { accountAuthTypes, idStr, request: { allowedAccounts, origin }, url } = this.#authRequestsV2[id]; + const { accountAuthTypes, idStr, request: { allowedAccounts, isSubstrateConnector, origin }, url } = this.#authRequestsV2[id]; // Note: accountAuthTypes represents the accountAuthType of this request // allowedAccounts is a list of connected accounts that exist for this origin during this request. @@ -220,7 +241,7 @@ export default class AuthRequestHandler { if (accountAuthTypes.length !== ALL_ACCOUNT_AUTH_TYPES.length) { const backupAllowed = (allowedAccounts || []) .filter((a) => { - if (isEthereumAddress(a) && !accountAuthTypes.includes('evm')) { + if (isEthereumAddress(a) && (isSubstrateConnector || !ecdsaAddressList.has(a)) && !accountAuthTypes.includes('evm')) { return true; } @@ -284,7 +305,8 @@ export default class AuthRequestHandler { origin, url, accountAuthTypes: [...new Set([...accountAuthTypes, ...(existed?.accountAuthTypes || [])])], - currentNetworkMap: existed ? { ...defaultNetworkMap, ...existed.currentNetworkMap } : defaultNetworkMap + currentNetworkMap: existed ? { ...defaultNetworkMap, ...existed.currentNetworkMap } : defaultNetworkMap, + isSubstrateConnector: isSubstrateConnector || existed?.isSubstrateConnector }; this.setAuthorize(authorizeList, () => { @@ -315,6 +337,7 @@ export default class AuthRequestHandler { const idStr = stripUrl(url); const isAllowedDappConnectBothType = !!DAPP_CONNECT_BOTH_TYPE_ACCOUNT_URL.find((url_) => url.includes(url_)); let accountAuthTypes = [...new Set(isAllowedDappConnectBothType ? ['evm', 'substrate'] : (request.accountAuthTypes || ['substrate']))]; + const isSubstrateConnector = !!request.isSubstrateConnector; if (!authList) { authList = {}; @@ -385,10 +408,11 @@ export default class AuthRequestHandler { .filter((item) => (item !== '')); let allowedListByRequestType = [...request.allowedAccounts]; + const ecdsaAddressList = this.getEcdsaAddressList(); allowedListByRequestType = accountAuthTypes.reduce((list, accountAuthType) => { if (accountAuthType === 'evm') { - list.push(...allowedListByRequestType.filter((a) => isEthereumAddress(a))); + list.push(...allowedListByRequestType.filter((a) => isEthereumAddress(a) && (isSubstrateConnector || !ecdsaAddressList.has(a)))); } else if (accountAuthType === 'substrate') { list.push(...allowedListByRequestType.filter((a) => isSubstrateAddress(a))); } else if (accountAuthType === 'ton') { @@ -421,7 +445,8 @@ export default class AuthRequestHandler { origin, url, accountAuthTypes: ALL_ACCOUNT_AUTH_TYPES, - currentNetworkMap: {} + currentNetworkMap: {}, + isSubstrateConnector }; this.setAuthorize(authList); diff --git a/packages/extension-base/src/services/request-service/types.ts b/packages/extension-base/src/services/request-service/types.ts index d9623692d85..23773c8b102 100644 --- a/packages/extension-base/src/services/request-service/types.ts +++ b/packages/extension-base/src/services/request-service/types.ts @@ -27,6 +27,7 @@ export interface AuthUrlInfo { isAllowedMap: Record; currentNetworkMap: Partial>; currentAccount?: string; + isSubstrateConnector?: boolean; } export interface AuthUrlInfoNeedMigration extends Omit { diff --git a/packages/extension-base/src/types/account/info/keyring.ts b/packages/extension-base/src/types/account/info/keyring.ts index a092645719d..fb3a0ee4d90 100644 --- a/packages/extension-base/src/types/account/info/keyring.ts +++ b/packages/extension-base/src/types/account/info/keyring.ts @@ -61,6 +61,8 @@ export interface AccountLedgerData { availableGenesisHashes?: string[]; /** Is Ledger recovery chain */ isLedgerRecovery?: boolean; + /** Is Ledger substrate ECDSA scheme signature */ + isSubstrateECDSA?: boolean; } /** @@ -113,6 +115,7 @@ export enum AccountSignMode { QR = 'qr', LEGACY_LEDGER = 'legacy-ledger', GENERIC_LEDGER = 'generic-ledger', + ECDSA_SUBSTRATE_LEDGER = 'ecdsa-substrate-ledger', READ_ONLY = 'readonly', ALL_ACCOUNT = 'all', INJECTED = 'injected', diff --git a/packages/extension-base/src/types/balance/transfer.ts b/packages/extension-base/src/types/balance/transfer.ts index 9db102c5874..8b408bc0e40 100644 --- a/packages/extension-base/src/types/balance/transfer.ts +++ b/packages/extension-base/src/types/balance/transfer.ts @@ -30,4 +30,5 @@ export interface RequestSubmitTransfer extends BaseRequestSign, TransactionFee { transferAll: boolean; value: string; transferBounceable?: boolean; + isSubstrateTransaction?: boolean; } diff --git a/packages/extension-base/src/utils/account/transform.ts b/packages/extension-base/src/utils/account/transform.ts index 6f1d6637bb3..331a47aeb39 100644 --- a/packages/extension-base/src/utils/account/transform.ts +++ b/packages/extension-base/src/utils/account/transform.ts @@ -88,6 +88,10 @@ export const getAccountSignMode = (address: string, _meta?: KeyringPair$Meta): A if (meta.isExternal) { if (meta.isHardware) { if (meta.isGeneric) { + if (meta.isSubstrateECDSA) { + return AccountSignMode.ECDSA_SUBSTRATE_LEDGER; + } + return AccountSignMode.GENERIC_LEDGER; } else { return AccountSignMode.LEGACY_LEDGER; @@ -379,6 +383,12 @@ export const getAccountTransactionActions = (signMode: AccountSignMode, networkT result.push(...CLAIM_AVAIL_BRIDGE); } + return result; + } else if (signMode === AccountSignMode.ECDSA_SUBSTRATE_LEDGER) { // Only for account substrate with ECDSA scheme format + const result: ExtrinsicType[] = []; + + result.push(...BASE_TRANSFER_ACTIONS, ...NATIVE_STAKE_ACTIONS, ...POOL_STAKE_ACTIONS, ExtrinsicType.TRANSFER_XCM, ExtrinsicType.SWAP, ExtrinsicType.CROWDLOAN); + return result; } @@ -506,6 +516,7 @@ export const convertAccountProxyType = (accountSignMode: AccountSignMode): Accou switch (accountSignMode) { case AccountSignMode.GENERIC_LEDGER: case AccountSignMode.LEGACY_LEDGER: + case AccountSignMode.ECDSA_SUBSTRATE_LEDGER: return AccountProxyType.LEDGER; case AccountSignMode.QR: return AccountProxyType.QR; @@ -623,6 +634,7 @@ export const _combineAccounts = (accounts: AccountJson[], modifyPairs: ModifyPai switch (account.signMode) { case AccountSignMode.GENERIC_LEDGER: case AccountSignMode.LEGACY_LEDGER: + case AccountSignMode.ECDSA_SUBSTRATE_LEDGER: specialChain = account.specialChain; break; } diff --git a/packages/extension-koni-ui/src/Popup/Account/ConnectLedger.tsx b/packages/extension-koni-ui/src/Popup/Account/ConnectLedger.tsx index f2e05880603..45e197252dc 100644 --- a/packages/extension-koni-ui/src/Popup/Account/ConnectLedger.tsx +++ b/packages/extension-koni-ui/src/Popup/Account/ConnectLedger.tsx @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { LedgerNetwork, MigrationLedgerNetwork } from '@subwallet/extension-base/background/KoniTypes'; +import { LEDGER_SCHEME, LedgerNetwork, MigrationLedgerNetwork } from '@subwallet/extension-base/background/KoniTypes'; import { reformatAddress } from '@subwallet/extension-base/utils'; import { AccountItemWithName, AccountWithNameSkeleton, BasicOnChangeFunction, ChainSelector, DualLogo, InfoIcon, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; import { LedgerChainSelector, LedgerItemType } from '@subwallet/extension-koni-ui/components/Field/LedgerChainSelector'; @@ -254,7 +254,8 @@ const Component: React.FC = (props: Props) => { name: item.name, isEthereum: selectedChain.isEthereum, isGeneric: selectedChain.isGeneric, - isLedgerRecovery: selectedChain?.isRecovery + isLedgerRecovery: selectedChain?.isRecovery, + isSubstrateECDSA: selectedChain.scheme === LEDGER_SCHEME.ECDSA })) }) .then(() => { diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx index ffff2f4edcd..57db04252e5 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx @@ -3,6 +3,7 @@ import { ConfirmationDefinitions, ConfirmationDefinitionsCardano, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { AuthorizeRequest, MetadataRequest, SigningRequest } from '@subwallet/extension-base/background/types'; +import { _isChainEvmCompatible, _isChainSubstrateCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; import { AccountJson, ProcessType } from '@subwallet/extension-base/types'; import { _isRuntimeUpdated, detectTranslate } from '@subwallet/extension-base/utils'; @@ -10,9 +11,10 @@ import { AlertModal } from '@subwallet/extension-koni-ui/components'; import { isProductionMode, NEED_SIGN_CONFIRMATION } from '@subwallet/extension-koni-ui/constants'; import { useAlert, useConfirmationsInfo, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { SubmitApiConfirmation } from '@subwallet/extension-koni-ui/Popup/Confirmations/variants/Action'; +import { RootState } from '@subwallet/extension-koni-ui/stores'; import { ConfirmationType } from '@subwallet/extension-koni-ui/stores/base/RequestState'; import { AccountSignMode, ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { findAccountByAddress, getSignMode, isRawPayload } from '@subwallet/extension-koni-ui/utils'; +import { findAccountByAddress, findChainInfoByGenesisHash, getSignMode, isRawPayload } from '@subwallet/extension-koni-ui/utils'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -51,6 +53,7 @@ const Component = function ({ className }: Props) { const { t } = useTranslation(); const { alertProps, closeAlert, openAlert } = useAlert(alertModalId); const { transactionRequest } = useSelector((state) => state.requestState); + const chainInfoMap = useSelector((state: RootState) => state.chainStore.chainInfoMap); const nextConfirmation = useCallback(() => { setIndex((val) => Math.min(val + 1, numberOfConfirmations - 1)); @@ -76,11 +79,18 @@ const Component = function ({ className }: Props) { const address = request.request.payload.address; account = findAccountByAddress(accounts, address) || undefined; - const isEthereum = isEthereumAddress(address); + const isEthereum = isEthereumAddress(address) && !account?.isSubstrateECDSA; if (account?.isHardware) { if (account?.isGeneric) { - canSign = !isEthereum; + if (account.isSubstrateECDSA && !_isMessage) { + const payload = request.request.payload as SignerPayloadJSON; + const chainInfo = findChainInfoByGenesisHash(chainInfoMap, payload.genesisHash); + + canSign = !!chainInfo && (_isChainEvmCompatible(chainInfo) && _isChainSubstrateCompatible(chainInfo)); + } else { + canSign = !isEthereum; + } } else { if (_isMessage) { canSign = true; @@ -227,7 +237,7 @@ const Component = function ({ className }: Props) { } return null; - }, [accounts, closeAlert, confirmation, openAlert]); + }, [accounts, chainInfoMap, closeAlert, confirmation, openAlert]); const headerTitle = useMemo((): string => { if (!confirmation) { diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Substrate.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Substrate.tsx index e2fa23cb4e3..5bd6a23743b 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Substrate.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Substrate.tsx @@ -5,7 +5,7 @@ import { ExtrinsicType, NotificationType } from '@subwallet/extension-base/backg import { RequestSign } from '@subwallet/extension-base/background/types'; import { _isRuntimeUpdated, detectTranslate } from '@subwallet/extension-base/utils'; import { AlertBox, AlertModal } from '@subwallet/extension-koni-ui/components'; -import { CONFIRMATION_QR_MODAL, NotNeedMigrationGens, SUBSTRATE_GENERIC_KEY, SUBSTRATE_MIGRATION_KEY } from '@subwallet/extension-koni-ui/constants'; +import { CONFIRMATION_QR_MODAL, NotNeedMigrationGens, SUBSTRATE_ECDSA_KEY, SUBSTRATE_GENERIC_KEY, SUBSTRATE_MIGRATION_KEY, SubstrateLedgerSignModeSupport } from '@subwallet/extension-koni-ui/constants'; import { InjectContext } from '@subwallet/extension-koni-ui/contexts/InjectContext'; import { useAlert, useGetAccountByAddress, useGetChainInfoByGenesisHash, useLedger, useMetadata, useNotification, useParseSubstrateRequestPayload, useSelector, useUnlockChecker } from '@subwallet/extension-koni-ui/hooks'; import { approveSignPasswordV2, approveSignSignature, cancelSignRequest, shortenMetadata } from '@subwallet/extension-koni-ui/messaging'; @@ -36,19 +36,31 @@ interface AlertData { title: string; type: 'info' | 'warning' | 'error'; } + const alertModalId = 'dapp-alert-modal'; +const metadataFAQUrl = 'https://docs.subwallet.app/main/extension-user-guide/faqs#how-do-i-update-network-metadata'; +const genericFAQUrl = 'https://docs.subwallet.app/main/extension-user-guide/faqs#how-do-i-re-attach-a-new-polkadot-account-on-ledger'; +const migrationFAQUrl = 'https://docs.subwallet.app/main/extension-user-guide/faqs#how-do-i-move-assets-from-a-substrate-network-to-the-new-polkadot-account-on-ledger'; const handleConfirm = async (id: string) => await approveSignPasswordV2({ id }); - const handleCancel = async (id: string) => await cancelSignRequest(id); - const handleSignature = async (id: string, { signature, signedTransaction }: SubstrateSigData) => await approveSignSignature(id, signature, signedTransaction); -const metadataFAQUrl = 'https://docs.subwallet.app/main/extension-user-guide/faqs#how-do-i-update-network-metadata'; -const genericFAQUrl = 'https://docs.subwallet.app/main/extension-user-guide/faqs#how-do-i-re-attach-a-new-polkadot-account-on-ledger'; -const migrationFAQUrl = 'https://docs.subwallet.app/main/extension-user-guide/faqs#how-do-i-move-assets-from-a-substrate-network-to-the-new-polkadot-account-on-ledger'; +const getChainSlug = (signMode: AccountSignMode, originGenesisHash?: string | null, accountChainInfoSlug?: string) => { + if (signMode === AccountSignMode.GENERIC_LEDGER) { + return originGenesisHash ? SUBSTRATE_MIGRATION_KEY : SUBSTRATE_GENERIC_KEY; + } + + if (signMode === AccountSignMode.ECDSA_SUBSTRATE_LEDGER) { + return SUBSTRATE_ECDSA_KEY; + } + + return accountChainInfoSlug || ''; +}; + +const isRequireMetadata = (signMode: AccountSignMode, isRuntimeUpdated: boolean) => signMode === AccountSignMode.GENERIC_LEDGER || signMode === AccountSignMode.ECDSA_SUBSTRATE_LEDGER || (signMode === AccountSignMode.LEGACY_LEDGER && isRuntimeUpdated); -const modeCanSignMessage: AccountSignMode[] = [AccountSignMode.QR, AccountSignMode.PASSWORD, AccountSignMode.INJECTED, AccountSignMode.LEGACY_LEDGER, AccountSignMode.GENERIC_LEDGER]; +const modeCanSignMessage: AccountSignMode[] = [AccountSignMode.QR, AccountSignMode.PASSWORD, AccountSignMode.INJECTED, AccountSignMode.LEGACY_LEDGER, AccountSignMode.GENERIC_LEDGER, AccountSignMode.ECDSA_SUBSTRATE_LEDGER]; const Component: React.FC = (props: Props) => { const { className, extrinsicType, id, isInternal, request, txExpirationTime } = props; @@ -73,7 +85,7 @@ const Component: React.FC = (props: Props) => { : _payload.genesisHash; }, [account?.genesisHash, chainInfoMap.polkadot.substrateInfo?.genesisHash, request.payload]); const signMode = useMemo(() => getSignMode(account), [account]); - const isLedger = useMemo(() => signMode === AccountSignMode.LEGACY_LEDGER || signMode === AccountSignMode.GENERIC_LEDGER, [signMode]); + const isLedger = useMemo(() => SubstrateLedgerSignModeSupport.includes(signMode), [signMode]); const isRuntimeUpdated = useMemo(() => { const _payload = request.payload; @@ -83,7 +95,7 @@ const Component: React.FC = (props: Props) => { return _isRuntimeUpdated(_payload.signedExtensions); } }, [request.payload]); - const requireMetadata = useMemo(() => signMode === AccountSignMode.GENERIC_LEDGER || (signMode === AccountSignMode.LEGACY_LEDGER && isRuntimeUpdated), [isRuntimeUpdated, signMode]); + const requireMetadata = useMemo(() => isRequireMetadata(signMode, isRuntimeUpdated), [isRuntimeUpdated, signMode]); const requireSpecVersion = useMemo((): number | undefined => { const _payload = request.payload; @@ -107,6 +119,7 @@ const Component: React.FC = (props: Props) => { return QrCode; case AccountSignMode.LEGACY_LEDGER: case AccountSignMode.GENERIC_LEDGER: + case AccountSignMode.ECDSA_SUBSTRATE_LEDGER: return Swatches; case AccountSignMode.INJECTED: return Wallet; @@ -114,7 +127,7 @@ const Component: React.FC = (props: Props) => { return CheckCircle; } }, [signMode]); - const chainSlug = useMemo(() => signMode === AccountSignMode.GENERIC_LEDGER ? account?.originGenesisHash ? SUBSTRATE_MIGRATION_KEY : SUBSTRATE_GENERIC_KEY : (accountChainInfo?.slug || ''), [account?.originGenesisHash, accountChainInfo?.slug, signMode]); + const chainSlug = useMemo(() => getChainSlug(signMode, account?.originGenesisHash, accountChainInfo?.slug), [account?.originGenesisHash, accountChainInfo?.slug, signMode]); const networkName = useMemo(() => chainInfo?.name || chain?.name || toShort(genesisHash), [chainInfo, genesisHash, chain]); const isMetadataOutdated = useMemo(() => { @@ -162,7 +175,7 @@ const Component: React.FC = (props: Props) => { }, [closeAlert, isOpenAlert, networkName, openAlert, t]); const alertData = useMemo((): AlertData | undefined => { - const requireMetadata = signMode === AccountSignMode.GENERIC_LEDGER || (signMode === AccountSignMode.LEGACY_LEDGER && isRuntimeUpdated); + const requireMetadata = isRequireMetadata(signMode, isRuntimeUpdated); if (!isMessage && !loadingChain) { if (!chain || !chain.hasMetadata || isMetadataOutdated) { @@ -242,7 +255,7 @@ const Component: React.FC = (props: Props) => { } } } else { - if (signMode === AccountSignMode.GENERIC_LEDGER) { + if (signMode === AccountSignMode.GENERIC_LEDGER || signMode === AccountSignMode.ECDSA_SUBSTRATE_LEDGER) { return { type: 'error', title: t('Error!'), @@ -446,6 +459,7 @@ const Component: React.FC = (props: Props) => { break; case AccountSignMode.LEGACY_LEDGER: case AccountSignMode.GENERIC_LEDGER: + case AccountSignMode.ECDSA_SUBSTRATE_LEDGER: onConfirmLedger(); break; case AccountSignMode.INJECTED: diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/AuthorizeConfirmation.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/AuthorizeConfirmation.tsx index f7abadf4f0e..6e4d29f6e8e 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/AuthorizeConfirmation.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/AuthorizeConfirmation.tsx @@ -40,7 +40,7 @@ async function handleBlock ({ id }: AuthorizeRequest) { function Component ({ className, request }: Props) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); - const { accountAuthTypes, allowedAccounts } = request.request; + const { accountAuthTypes, allowedAccounts, isSubstrateConnector } = request.request; const { accountProxies, accounts } = useSelector((state: RootState) => state.accountState); const navigate = useNavigate(); @@ -48,8 +48,8 @@ function Component ({ className, request }: Props) { const setSelectedAccountTypes = useSetSelectedAccountTypes(true); // List all of all accounts by auth type - const visibleAccountProxies = useMemo(() => (filterAuthorizeAccountProxies(accountProxies, accountAuthTypes || ALL_ACCOUNT_AUTH_TYPES)), - [accountAuthTypes, accountProxies]); + const visibleAccountProxies = useMemo(() => (filterAuthorizeAccountProxies(accountProxies, accountAuthTypes || ALL_ACCOUNT_AUTH_TYPES, isSubstrateConnector)), + [accountAuthTypes, accountProxies, isSubstrateConnector]); // Selected map with default values is map of all accounts const [selectedMap, setSelectedMap] = useState>({}); diff --git a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx index 005307b6569..6a9e811a81c 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx @@ -3,6 +3,7 @@ import { NotificationType } from '@subwallet/extension-base/background/KoniTypes'; import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; +import { _isSubstrateEvmCompatibleChain } from '@subwallet/extension-base/services/chain-service/utils'; import { EarningRewardHistoryItem, SpecialYieldPoolInfo, SpecialYieldPositionInfo, YieldPoolInfo, YieldPoolType, YieldPositionInfo } from '@subwallet/extension-base/types'; import { AlertModal, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; import { BN_TEN, BN_ZERO, DEFAULT_EARN_PARAMS, DEFAULT_UN_STAKE_PARAMS, EARN_TRANSACTION, UN_STAKE_TRANSACTION } from '@subwallet/extension-koni-ui/constants'; @@ -52,10 +53,14 @@ function Component ({ compound, return ALL_ACCOUNT_KEY; } - const accountAddress = currentAccountProxy?.accounts.find(({ chainType }) => { + const accountAddress = currentAccountProxy?.accounts.find(({ chainType, isSubstrateECDSA }) => { if (chainInfoMap[poolInfo.chain]) { const chainInfo = chainInfoMap[poolInfo.chain]; + if (isSubstrateECDSA) { + return _isSubstrateEvmCompatibleChain(chainInfo); + } + return isChainInfoAccordantAccountChainType(chainInfo, chainType); } diff --git a/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/Detail.tsx b/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/Detail.tsx index 887277f37e9..fd90bcd306f 100644 --- a/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/Detail.tsx +++ b/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/Detail.tsx @@ -13,7 +13,7 @@ import { RootState } from '@subwallet/extension-koni-ui/stores'; import { updateAuthUrls } from '@subwallet/extension-koni-ui/stores/utils'; import { Theme, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { ManageWebsiteAccessDetailParam } from '@subwallet/extension-koni-ui/types/navigation'; -import { convertAuthorizeTypeToChainTypes } from '@subwallet/extension-koni-ui/utils'; +import { convertAuthorizeTypeToChainTypes, isSubstrateEcdsaAccountProxy } from '@subwallet/extension-koni-ui/utils'; import { Icon, ModalContext, Switch, SwList } from '@subwallet/react-ui'; import { ArrowsLeftRight, GearSix, MagnifyingGlass, Plugs, PlugsConnected, ShieldCheck, ShieldSlash, X } from 'phosphor-react'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; @@ -32,14 +32,14 @@ type WrapperProps = ThemeProps; const ActionModalId = 'actionModalId'; // const FilterModalId = 'filterModalId'; -const checkAccountAddressValid = (chainType: AccountChainType, accountAuthTypes?: AccountAuthType[]): boolean => { +const checkAccountAddressValid = (chainType: AccountChainType, accountAuthTypes?: AccountAuthType[], isECDSA?: boolean): boolean => { if (!accountAuthTypes) { return false; } switch (chainType) { case AccountChainType.SUBSTRATE: return accountAuthTypes.includes('substrate'); - case AccountChainType.ETHEREUM: return accountAuthTypes.includes('evm'); + case AccountChainType.ETHEREUM: return accountAuthTypes.includes('evm') || !!isECDSA; case AccountChainType.TON: return accountAuthTypes.includes('ton'); case AccountChainType.CARDANO: return accountAuthTypes.includes('cardano'); } @@ -55,7 +55,11 @@ function Component ({ accountAuthTypes, authInfo, className = '', goBack, origin const { t } = useTranslation(); const { token } = useTheme() as Theme; const accountProxyItems = useMemo(() => { - return accountProxies.filter((ap) => ap.id !== 'ALL' && ap.chainTypes.some((chainType) => checkAccountAddressValid(chainType, accountAuthTypes))); + return accountProxies.filter((ap) => { + const isECDSA = isSubstrateEcdsaAccountProxy(ap); + + return ap.id !== 'ALL' && ap.chainTypes.some((chainType) => checkAccountAddressValid(chainType, accountAuthTypes, isECDSA)); + }); }, [accountAuthTypes, accountProxies]); const onOpenActionModal = useCallback(() => { @@ -157,7 +161,7 @@ function Component ({ accountAuthTypes, authInfo, className = '', goBack, origin const newAllowedMap = { ...authInfo.isAllowedMap }; item.accounts.forEach((account) => { - if (checkAccountAddressValid(account.chainType, authInfo.accountAuthTypes)) { + if (checkAccountAddressValid(account.chainType, authInfo.accountAuthTypes, account.isSubstrateECDSA)) { newAllowedMap[account.address] = !isEnabled; } }); diff --git a/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/index.tsx b/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/index.tsx index b4456e1744b..fb48166240a 100644 --- a/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/index.tsx @@ -36,7 +36,9 @@ function getAccountCount (item: AuthUrlInfo, accountProxies: AccountProxy[]): nu return accountProxies.filter((ap) => { return ap.accounts.some((account) => { if (isEthereumAddress(account.address)) { - return authType.includes('evm') && item.isAllowedMap[account.address]; + const supportECDSASubstrateAddress = account.isSubstrateECDSA && authType.includes('substrate'); + + return item.isAllowedMap[account.address] && (authType.includes('evm') || supportECDSASubstrateAddress); } if (isSubstrateAddress(account.address)) { diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx index b764a0fd311..251b6b43fad 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -30,7 +30,7 @@ import { CommonActionType, commonProcessReducer, DEFAULT_COMMON_PROCESS } from ' import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, ChainItemType, FormCallbacks, Theme, ThemeProps, TransferParams } from '@subwallet/extension-koni-ui/types'; import { TokenSelectorItemType } from '@subwallet/extension-koni-ui/types/field'; -import { findAccountByAddress, formatBalance, getChainsByAccountAll, getChainsByAccountType, noop, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; +import { findAccountByAddress, formatBalance, getChainsByAccountAll, getChainsByAccountType, isHasOnlySubstrateEcdsaAccountProxy, isSubstrateEcdsaAccountProxy, noop, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; import { Button, Form, Icon } from '@subwallet/react-ui'; import { Rule } from '@subwallet/react-ui/es/form'; import BigN from 'bignumber.js'; @@ -65,12 +65,13 @@ function getTokenItems ( accountProxies: AccountProxy[], chainInfoMap: Record, assetRegistry: Record, - tokenGroupSlug?: string // is ether a token slug or a multiChainAsset slug + tokenGroupSlug?: string, // is ether a token slug or a multiChainAsset slug + isSubstrateEcdsa?: boolean ): TokenSelectorItemType[] { let allowedChains: string[]; if (!isAccountAll(accountProxy.id)) { - allowedChains = getChainsByAccountType(chainInfoMap, accountProxy.chainTypes, accountProxy.specialChain); + allowedChains = getChainsByAccountType(chainInfoMap, accountProxy.chainTypes, accountProxy.specialChain, isSubstrateEcdsa); } else { allowedChains = getChainsByAccountAll(accountProxy, accountProxies, chainInfoMap); } @@ -176,6 +177,14 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone const { getCurrentConfirmation, renderConfirmationButtons } = useGetConfirmationByScreen('send-fund'); const checkAction = usePreCheckAction(fromValue, true, detectTranslate('The account you are using is {{accountTitle}}, you cannot send assets with it')); + const isSubstrateEcdsa = useMemo(() => { + if (!isAllAccount) { + return isSubstrateEcdsaAccountProxy(targetAccountProxy); + } else { + return isHasOnlySubstrateEcdsaAccountProxy(accountProxies); + } + }, [accountProxies, isAllAccount, targetAccountProxy]); + const currentConfirmation = useMemo(() => { if (chainValue && destChainValue) { return getCurrentConfirmation([chainValue, destChainValue]); @@ -320,7 +329,8 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone accountProxies, chainInfoMap, assetRegistry, - sendFundSlug + sendFundSlug, + isSubstrateEcdsa ); const tokenBalanceMap = getAccountTokenBalance( @@ -353,7 +363,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone sortTokensByBalanceInSelector(tokenItemsSorted, priorityTokens); return tokenItemsSorted; - }, [accountProxies, assetRegistry, chainInfoMap, chainStateMap, getAccountTokenBalance, priorityTokens, sendFundSlug, targetAccountProxy, targetAccountProxyIdForGetBalance]); + }, [accountProxies, assetRegistry, chainInfoMap, chainStateMap, getAccountTokenBalance, isSubstrateEcdsa, priorityTokens, sendFundSlug, targetAccountProxy, targetAccountProxyIdForGetBalance]); const isNotShowAccountSelector = !isAllAccount && accountAddressItems.length < 2; @@ -524,7 +534,8 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone transferBounceable: options.isTransferBounceable, feeOption: selectedTransactionFee?.feeOption, feeCustom: selectedTransactionFee?.feeCustom, - tokenPayFeeSlug: currentTokenPayFee + tokenPayFeeSlug: currentTokenPayFee, + isSubstrateTransaction: isSubstrateEcdsa }); } else { // Make cross chain transfer @@ -544,7 +555,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone } return sendPromise; - }, [currentTokenPayFee, selectedTransactionFee?.feeOption, selectedTransactionFee?.feeCustom]); + }, [selectedTransactionFee?.feeOption, selectedTransactionFee?.feeCustom, currentTokenPayFee, isSubstrateEcdsa]); // todo: must refactor later, temporary solution to support SnowBridge const handleBridgeSpendingApproval = useCallback((values: TransferParams): Promise => { diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx index 71fa59a46a6..e5269a324d2 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx @@ -28,7 +28,7 @@ import { CommonActionType, commonProcessReducer, DEFAULT_COMMON_PROCESS } from ' import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, FormCallbacks, FormFieldData, SwapParams, ThemeProps, TokenBalanceItemType } from '@subwallet/extension-koni-ui/types'; import { TokenSelectorItemType } from '@subwallet/extension-koni-ui/types/field'; -import { convertFieldToObject, findAccountByAddress, getChainsByAccountAll, isAccountAll, isChainInfoAccordantAccountChainType, isTokenCompatibleWithAccountChainTypes, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; +import { convertFieldToObject, findAccountByAddress, getChainsByAccountAll, isAccountAll, isChainInfoAccordantAccountChainType, isSubstrateEcdsaAccountProxy, isTokenCompatibleWithAccountChainTypes, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; import { Button, Form, Icon, ModalContext } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; import CN from 'classnames'; @@ -295,7 +295,9 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { return false; } - if (!isTokenCompatibleWithAccountChainTypes(slug, targetAccountProxy.chainTypes, chainInfoMap)) { + const isSubstrateECDSA = isSubstrateEcdsaAccountProxy(targetAccountProxy); + + if (!isTokenCompatibleWithAccountChainTypes(slug, targetAccountProxy.chainTypes, chainInfoMap, isSubstrateECDSA)) { return false; } @@ -322,8 +324,10 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { const destChainValue = _getAssetOriginChain(toAssetInfo); const isSwitchable = useMemo(() => { - return isTokenCompatibleWithAccountChainTypes(toTokenSlugValue, targetAccountProxy.chainTypes, chainInfoMap); - }, [chainInfoMap, targetAccountProxy.chainTypes, toTokenSlugValue]); + const isSubstrateECDSA = isSubstrateEcdsaAccountProxy(targetAccountProxy); + + return isTokenCompatibleWithAccountChainTypes(toTokenSlugValue, targetAccountProxy.chainTypes, chainInfoMap, isSubstrateECDSA); + }, [chainInfoMap, targetAccountProxy, toTokenSlugValue]); // Unable to use useEffect due to infinity loop caused by conflict setCurrentSlippage and currentQuote const slippage = useMemo(() => { diff --git a/packages/extension-koni-ui/src/assets/logo/index.ts b/packages/extension-koni-ui/src/assets/logo/index.ts index 03d63598a16..35a19bfdcc9 100644 --- a/packages/extension-koni-ui/src/assets/logo/index.ts +++ b/packages/extension-koni-ui/src/assets/logo/index.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { SUBSTRATE_GENERIC_KEY, SUBSTRATE_MIGRATION_KEY } from '@subwallet/extension-koni-ui/constants'; +import { SUBSTRATE_ECDSA_KEY, SUBSTRATE_GENERIC_KEY, SUBSTRATE_MIGRATION_KEY } from '@subwallet/extension-koni-ui/constants'; export const DefaultLogosMap: Record = { subwallet: './images/projects/subwallet.png', @@ -32,6 +32,7 @@ export const DefaultLogosMap: Record = { currency_hkd: '/images/projects/CurrencyHKD.png', currency_vnd: '/images/projects/CurrencyVND.png', [SUBSTRATE_GENERIC_KEY]: './images/projects/polkadot.png', + [SUBSTRATE_ECDSA_KEY]: './images/projects/polkadot.png', [SUBSTRATE_MIGRATION_KEY]: './images/projects/polkadot-migration.png', ton: './images/projects/ton.png', ...Object.fromEntries( // Can use image from chain-list instead of local image diff --git a/packages/extension-koni-ui/src/assets/subwallet/index.ts b/packages/extension-koni-ui/src/assets/subwallet/index.ts index e169be13ce0..9f8ad72f9c9 100644 --- a/packages/extension-koni-ui/src/assets/subwallet/index.ts +++ b/packages/extension-koni-ui/src/assets/subwallet/index.ts @@ -3,7 +3,7 @@ import { SwapProviderId } from '@subwallet/extension-base/types/swap'; import { DefaultLogosMap } from '@subwallet/extension-koni-ui/assets/logo'; -import { SUBSTRATE_GENERIC_KEY, SUBSTRATE_MIGRATION_KEY } from '@subwallet/extension-koni-ui/constants'; +import { SUBSTRATE_ECDSA_KEY, SUBSTRATE_GENERIC_KEY, SUBSTRATE_MIGRATION_KEY } from '@subwallet/extension-koni-ui/constants'; const SwLogosMap: Record = { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -25,6 +25,7 @@ const SwLogosMap: Record = { hydradx_mainnet: DefaultLogosMap.hydradx, hydradx_testnet: DefaultLogosMap.hydradx, [SUBSTRATE_GENERIC_KEY]: DefaultLogosMap[SUBSTRATE_GENERIC_KEY], + [SUBSTRATE_ECDSA_KEY]: DefaultLogosMap[SUBSTRATE_ECDSA_KEY], [SUBSTRATE_MIGRATION_KEY]: DefaultLogosMap[SUBSTRATE_MIGRATION_KEY], [SwapProviderId.POLKADOT_ASSET_HUB.toLowerCase()]: DefaultLogosMap.polkadot_assethub, [SwapProviderId.KUSAMA_ASSET_HUB.toLowerCase()]: DefaultLogosMap.kusama_assethub, diff --git a/packages/extension-koni-ui/src/components/Account/Card/AccountCardItem.tsx b/packages/extension-koni-ui/src/components/Account/Card/AccountCardItem.tsx index abf131d33e3..85ceb5fd02c 100644 --- a/packages/extension-koni-ui/src/components/Account/Card/AccountCardItem.tsx +++ b/packages/extension-koni-ui/src/components/Account/Card/AccountCardItem.tsx @@ -73,6 +73,7 @@ function Component (props: _AccountCardItem): React.ReactElement<_AccountCardIte switch (signMode) { case AccountSignMode.LEGACY_LEDGER: case AccountSignMode.GENERIC_LEDGER: + case AccountSignMode.ECDSA_SUBSTRATE_LEDGER: return { type: 'icon', value: Swatches diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx index c7631481a39..618b5372696 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx @@ -24,7 +24,7 @@ function Component ({ chainTypes, className }: Props): React.ReactElement result.sort((a, b) => ACCOUNT_CHAIN_TYPE_ORDINAL_MAP[a] - ACCOUNT_CHAIN_TYPE_ORDINAL_MAP[b]); - return result; + return [...new Set(result)]; }, [chainTypes]); return ( diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxyItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxyItem.tsx index 64280b2f50e..5a28a1db67f 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxyItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxyItem.tsx @@ -29,7 +29,6 @@ type Props = ThemeProps & { function Component (props: Props): React.ReactElement { const { accountProxy, accountProxyName, chainTypes, className, isSelected, leftPartNode, onClick, renderRightPart, rightPartNode, showUnselectIcon } = props; const token = useContext(ThemeContext as Context).token; - const checkedIconNode = ((showUnselectIcon || isSelected) && (
>(authInfo?.isAllowedMap || {}); const accountProxies = useSelector((state: RootState) => state.accountState.accountProxies); + const accounts = useSelector((state: RootState) => state.accountState.accounts); const currentAccountProxy = useSelector((state: RootState) => state.accountState.currentAccountProxy); // const [oldConnected, setOldConnected] = useState(0); const [isSubmit, setIsSubmit] = useState(false); @@ -48,16 +49,18 @@ function Component ({ authInfo, className = '', id, isBlocked = true, isNotConne const isEvmAuthorize = useMemo(() => !!authInfo?.accountAuthTypes.includes('evm'), [authInfo?.accountAuthTypes]); const currentEvmNetworkInfo = useMemo(() => authInfo?.currentNetworkMap?.evm && chainInfoMap[authInfo?.currentNetworkMap.evm], [authInfo?.currentNetworkMap?.evm, chainInfoMap]); + const substrateEcdsaAddresses = useMemo(() => + accounts.filter((ac) => ac.isSubstrateECDSA) + .map(({ address }) => address), [accounts]); const handlerUpdateMap = useCallback((accountProxy: AccountProxy, oldValue: boolean) => { return () => { setAllowedMap((values) => { const newValues = { ...values }; - const listAddress = accountProxy.accounts.map(({ address }) => address); + const listAddress = accountProxy.accounts + .filter(({ address }) => isAddressAllowedWithAuthType(address, authInfo?.accountAuthTypes || [])); - listAddress.forEach((address) => { - const addressIsValid = isAddressAllowedWithAuthType(address, authInfo?.accountAuthTypes || []); - - addressIsValid && (newValues[address] = !oldValue); + listAddress.forEach(({ address }) => { + newValues[address] = !oldValue; }); return newValues; @@ -126,7 +129,7 @@ function Component ({ authInfo, className = '', id, isBlocked = true, isNotConne // setOldConnected(0); setAllowedMap({}); } - }, [authInfo?.accountAuthTypes, authInfo?.isAllowedMap]); + }, [authInfo?.accountAuthTypes, authInfo?.isAllowedMap, substrateEcdsaAddresses]); const actionButtons = useMemo(() => { if (_isNotConnected) { @@ -265,7 +268,7 @@ function Component ({ authInfo, className = '', id, isBlocked = true, isNotConne ); } - const listAccountProxy = filterAuthorizeAccountProxies(accountProxies, authInfo?.accountAuthTypes || []).map((proxy) => { + const listAccountProxy = filterAuthorizeAccountProxies(accountProxies, authInfo?.accountAuthTypes || [], authInfo?.isSubstrateConnector).map((proxy) => { const value = proxy.accounts.some(({ address }) => allowedMap[address]); return { diff --git a/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/index.tsx b/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/index.tsx index dfbde8aea25..eec4b3b1d89 100644 --- a/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/index.tsx +++ b/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/index.tsx @@ -90,6 +90,10 @@ function Component ({ className }: Props): React.ReactElement { return accounts.filter(({ address }) => !isAccountAll(address)); }, [accounts]); + const substrateEcdsaAddresses = useMemo(() => + accounts.filter((ac) => ac.isSubstrateECDSA) + .map(({ address }) => address), [accounts]); + useEffect(() => { if (currentAuth) { if (!currentAuth.isAllowed) { @@ -100,7 +104,7 @@ function Component ({ className }: Props): React.ReactElement { const types = currentAuth.accountAuthTypes || ['substrate']; const allowedMap = currentAuth.isAllowedMap; - const filterType = (address: string) => { + const filterType = (address: string, isSubstrateECDSA?: boolean) => { return isAddressAllowedWithAuthType(address, types); }; @@ -113,15 +117,15 @@ function Component ({ className }: Props): React.ReactElement { const idProxiesCanConnect = new Set(); const allowedIdProxies = new Set(); - accountToCheck.forEach(({ address, proxyId }) => { - if (filterType(address) && proxyId) { + accountToCheck.forEach(({ address, isSubstrateECDSA, proxyId }) => { + if (filterType(address, isSubstrateECDSA) && proxyId) { idProxiesCanConnect.add(proxyId); } }); Object.entries(allowedMap) .forEach(([address, value]) => { - if (filterType(address)) { + if (filterType(address, substrateEcdsaAddresses.includes(address))) { const account = accountToCheck.find(({ address: accAddress }) => accAddress === address); if (account?.proxyId && value) { @@ -159,7 +163,7 @@ function Component ({ className }: Props): React.ReactElement { setConnected(0); setConnectionState(ConnectionStatement.NOT_CONNECTED); } - }, [currentAccountProxy, currentAuth, isAllAccount, noAllAccounts]); + }, [currentAccountProxy, currentAuth, isAllAccount, noAllAccounts, substrateEcdsaAddresses]); const visibleText = useMemo((): string => { switch (connectionState) { diff --git a/packages/extension-koni-ui/src/connector/Ledger/SubstrateECDSALedger.ts b/packages/extension-koni-ui/src/connector/Ledger/SubstrateECDSALedger.ts new file mode 100644 index 00000000000..383b78145a7 --- /dev/null +++ b/packages/extension-koni-ui/src/connector/Ledger/SubstrateECDSALedger.ts @@ -0,0 +1,15 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { LEDGER_SCHEME } from '@subwallet/extension-base/background/KoniTypes'; +import { LedgerTypes } from '@subwallet/extension-koni-ui/types'; + +import { SubstrateGenericLedger } from './SubstrateGenericLedger'; + +export class SubstrateECDSALedger extends SubstrateGenericLedger { + constructor (transport: LedgerTypes, slip44: number) { + super(transport, slip44); + + this.scheme = LEDGER_SCHEME.ECDSA; + } +} diff --git a/packages/extension-koni-ui/src/connector/Ledger/SubstrateGenericLedger.ts b/packages/extension-koni-ui/src/connector/Ledger/SubstrateGenericLedger.ts index 5e8bdca42e1..3a0859172d8 100644 --- a/packages/extension-koni-ui/src/connector/Ledger/SubstrateGenericLedger.ts +++ b/packages/extension-koni-ui/src/connector/Ledger/SubstrateGenericLedger.ts @@ -1,8 +1,10 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { LEDGER_SCHEME } from '@subwallet/extension-base/background/KoniTypes'; import { wrapBytes } from '@subwallet/extension-dapp'; import { PolkadotGenericApp } from '@zondax/ledger-substrate'; +import { GenericeResponseAddress } from '@zondax/ledger-substrate/dist/common'; import { LEDGER_SUCCESS_CODE } from '@polkadot/hw-ledger/constants'; import { AccountOptions, LedgerAddress, LedgerSignature, LedgerVersion } from '@polkadot/hw-ledger/types'; @@ -24,6 +26,8 @@ export async function loadWasm () { export class SubstrateGenericLedger extends BaseLedger { protected ss58_addr_type = 42; + protected scheme = LEDGER_SCHEME.ED25519; + getVersion (): Promise { return this.withApp(async (app): Promise => { const { deviceLocked: locked, major, minor, patch, testMode } = await app.getVersion(); @@ -47,11 +51,18 @@ export class SubstrateGenericLedger extends BaseLedger { getAddress (confirm?: boolean, accountOffset?: number, addressOffset?: number, accountOptions?: Partial): Promise { return this.withApp(async (app): Promise => { const path = this.serializePath(accountOffset, addressOffset, accountOptions); - const { address, pubKey } = await this.wrapError(app.getAddress(path, this.ss58_addr_type, confirm)); + let result: GenericeResponseAddress; + + if (this.scheme === LEDGER_SCHEME.ECDSA) { + result = await this.wrapError(app.getAddressEcdsa(path, confirm)); + result.address = hexAddPrefix(result.address); + } else { + result = await this.wrapError(app.getAddressEd25519(path, this.ss58_addr_type, confirm)); + } return { - address, - publicKey: hexAddPrefix(pubKey) + address: result.address, + publicKey: hexAddPrefix(result.pubKey) }; }); } @@ -59,7 +70,9 @@ export class SubstrateGenericLedger extends BaseLedger { async signTransaction (message: Uint8Array, metadata: Uint8Array, accountOffset?: number, addressOffset?: number, accountOptions?: Partial): Promise { return this.withApp(async (app): Promise => { const path = this.serializePath(accountOffset, addressOffset, accountOptions); - const rs = await this.wrapError((app.signWithMetadata(path, Buffer.from(message), Buffer.from(metadata)))); + const rs = this.scheme === LEDGER_SCHEME.ECDSA + ? await this.wrapError(app.signWithMetadataEcdsa(path, Buffer.from(message), Buffer.from(metadata))) + : await this.wrapError(app.signWithMetadataEd25519(path, Buffer.from(message), Buffer.from(metadata))); return { signature: hexAddPrefix(u8aToHex(rs.signature)) @@ -71,18 +84,26 @@ export class SubstrateGenericLedger extends BaseLedger { return this.withApp(async (app): Promise => { const path = this.serializePath(accountOffset, addressOffset, accountOptions); - const rs = await this.wrapError(app.signRaw(path, Buffer.from(wrapBytes(message)))); - - const raw = hexStripPrefix(u8aToHex(rs.signature)); - const firstByte = raw.slice(0, 2); - // Source: https://github.com/polkadot-js/common/blob/a82ebdf6f9d78791bd1f21cd3c534deee37e0840/packages/keyring/src/pair/index.ts#L29-L34 - const isExtraByte = firstByte === '00'; - // Remove first byte (signature_type) from signature - const signature = isExtraByte ? hexAddPrefix(raw.slice(2)) : hexAddPrefix(raw); + if (this.scheme === LEDGER_SCHEME.ECDSA) { + const result = await this.wrapError(app.signRawEcdsa(path, Buffer.from(wrapBytes(message)))); - return { - signature - }; + return { + signature: hexAddPrefix(u8aToHex(result.signature)) + }; + } else { + const rs = await this.wrapError(app.signRawEd25519(path, Buffer.from(wrapBytes(message)))); + + const raw = hexStripPrefix(u8aToHex(rs.signature)); + const firstByte = raw.slice(0, 2); + // Source: https://github.com/polkadot-js/common/blob/a82ebdf6f9d78791bd1f21cd3c534deee37e0840/packages/keyring/src/pair/index.ts#L29-L34 + const isExtraByte = firstByte === '00'; + // Remove first byte (signature_type) from signature + const signature = isExtraByte ? hexAddPrefix(raw.slice(2)) : hexAddPrefix(raw); + + return { + signature + }; + } }); } diff --git a/packages/extension-koni-ui/src/connector/Ledger/index.ts b/packages/extension-koni-ui/src/connector/Ledger/index.ts index 70d29d7d8c4..365e8ce39e9 100644 --- a/packages/extension-koni-ui/src/connector/Ledger/index.ts +++ b/packages/extension-koni-ui/src/connector/Ledger/index.ts @@ -5,3 +5,4 @@ export * from './EVMLedger'; export * from './SubstrateGenericLedger'; export * from './SubstrateLegacyLedger'; export * from './SubstrateMigrationLedger'; +export * from './SubstrateLegacyLedger'; diff --git a/packages/extension-koni-ui/src/constants/ledger.ts b/packages/extension-koni-ui/src/constants/ledger.ts index 61dd559cab2..88f83751d7c 100644 --- a/packages/extension-koni-ui/src/constants/ledger.ts +++ b/packages/extension-koni-ui/src/constants/ledger.ts @@ -2,10 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { ChainInfoMap } from '@subwallet/chain-list'; -import { LedgerNetwork, MigrationLedgerNetwork } from '@subwallet/extension-base/background/KoniTypes'; +import { LEDGER_SCHEME, LedgerNetwork, MigrationLedgerNetwork } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountSignMode } from '@subwallet/extension-koni-ui/types'; export const SUBSTRATE_GENERIC_KEY = 'substrate_generic'; export const SUBSTRATE_MIGRATION_KEY = 'substrate_migration'; +export const SUBSTRATE_ECDSA_KEY = 'substrate_ecdsa'; export const POLKADOT_KEY = 'polkadot'; export const POLKADOT_SLIP_44 = 354; export const RECOVERY_SLUG = '_recovery'; @@ -24,6 +26,20 @@ export const PredefinedLedgerNetwork: LedgerNetwork[] = [ isEthereum: false, slip44: 354 }, + { + accountName: 'Polkadot ECDSA', + appName: 'Polkadot', + networkName: 'Polkadot ECDSA', + genesisHash: '', + network: 'PokadotECDSA', + icon: 'substrate', + slug: SUBSTRATE_ECDSA_KEY, + isDevMode: false, + isGeneric: true, + isEthereum: false, + slip44: 354, + scheme: LEDGER_SCHEME.ECDSA + }, { accountName: 'Polkadot Migration', appName: 'Polkadot Migration', @@ -251,6 +267,7 @@ export const PredefinedLedgerNetwork: LedgerNetwork[] = [ isEthereum: false, slip44: 799 } + // { // displayName: 'Centrifuge', // genesisHash: '0xb3db41421702df9a7fcac62b53ffeac85f7853cc4e689e0b93aeb3db18c09d82', @@ -517,8 +534,9 @@ export const isLedgerCapable = !!(window as unknown as { USB?: unknown }).USB; export const PolkadotDerivationPathGens: string[] = [POLKADOT_KEY].map((slug) => ChainInfoMap[slug].substrateInfo?.genesisHash || ''); export const StandardDerivationPathGens: string[] = Object.values(PredefinedLedgerNetwork) .filter((network) => { - return ![POLKADOT_KEY, SUBSTRATE_MIGRATION_KEY].includes(network.slug) && network.slip44 === POLKADOT_SLIP_44 && !network.isGeneric; + return ![POLKADOT_KEY, SUBSTRATE_MIGRATION_KEY, SUBSTRATE_ECDSA_KEY].includes(network.slug) && network.slip44 === POLKADOT_SLIP_44 && !network.isGeneric; }) .map(({ genesisHash }) => genesisHash); export const NotNeedMigrationGens: string[] = [...PolkadotDerivationPathGens, ...StandardDerivationPathGens]; +export const SubstrateLedgerSignModeSupport: AccountSignMode[] = [AccountSignMode.LEGACY_LEDGER, AccountSignMode.GENERIC_LEDGER, AccountSignMode.ECDSA_SUBSTRATE_LEDGER]; diff --git a/packages/extension-koni-ui/src/constants/signing.ts b/packages/extension-koni-ui/src/constants/signing.ts index bed0132a83e..fc8561ae88a 100644 --- a/packages/extension-koni-ui/src/constants/signing.ts +++ b/packages/extension-koni-ui/src/constants/signing.ts @@ -4,6 +4,6 @@ import { ConfirmationType } from '@subwallet/extension-koni-ui/stores/base/RequestState'; import { AccountSignMode } from '@subwallet/extension-koni-ui/types/account'; -export const MODE_CAN_SIGN: AccountSignMode[] = [AccountSignMode.PASSWORD, AccountSignMode.QR, AccountSignMode.LEGACY_LEDGER, AccountSignMode.GENERIC_LEDGER]; +export const MODE_CAN_SIGN: AccountSignMode[] = [AccountSignMode.PASSWORD, AccountSignMode.QR, AccountSignMode.LEGACY_LEDGER, AccountSignMode.GENERIC_LEDGER, AccountSignMode.ECDSA_SUBSTRATE_LEDGER]; export const NEED_SIGN_CONFIRMATION: ConfirmationType[] = ['evmSignatureRequest', 'evmSendTransactionRequest', 'signingRequest']; diff --git a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx index 08558301d6a..bc28212dcc3 100644 --- a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx +++ b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx @@ -15,7 +15,8 @@ const useGetAccountChainAddresses = (accountProxy: AccountProxy): AccountChainAd return useMemo(() => { const result: AccountChainAddress[] = []; - const chains: string[] = getChainsByAccountType(chainInfoMap, accountProxy.chainTypes, accountProxy.specialChain); + const isSubstrateECDSA = !!accountProxy.accounts[0]?.isSubstrateECDSA; + const chains: string[] = getChainsByAccountType(chainInfoMap, accountProxy.chainTypes, accountProxy.specialChain, isSubstrateECDSA); accountProxy.accounts.forEach((a) => { for (const chain of chains) { diff --git a/packages/extension-koni-ui/src/hooks/account/useGetAccountTitleByAddress.ts b/packages/extension-koni-ui/src/hooks/account/useGetAccountTitleByAddress.ts index 954c3ccebfc..e2de815a49d 100644 --- a/packages/extension-koni-ui/src/hooks/account/useGetAccountTitleByAddress.ts +++ b/packages/extension-koni-ui/src/hooks/account/useGetAccountTitleByAddress.ts @@ -18,6 +18,7 @@ const useGetAccountTitleByAddress = (address?: string): string => { switch (signMode) { case AccountSignMode.LEGACY_LEDGER: case AccountSignMode.GENERIC_LEDGER: + case AccountSignMode.ECDSA_SUBSTRATE_LEDGER: return t('Ledger account'); case AccountSignMode.ALL_ACCOUNT: return t('All account'); diff --git a/packages/extension-koni-ui/src/hooks/account/usePreCheckAction.ts b/packages/extension-koni-ui/src/hooks/account/usePreCheckAction.ts index b6e22bf7356..e18b2edd682 100644 --- a/packages/extension-koni-ui/src/hooks/account/usePreCheckAction.ts +++ b/packages/extension-koni-ui/src/hooks/account/usePreCheckAction.ts @@ -4,7 +4,7 @@ import { ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { AccountChainType } from '@subwallet/extension-base/types'; import { detectTranslate } from '@subwallet/extension-base/utils'; -import { ALL_STAKING_ACTIONS, isLedgerCapable, isProductionMode, ledgerIncompatible } from '@subwallet/extension-koni-ui/constants'; +import { ALL_STAKING_ACTIONS, isLedgerCapable, isProductionMode, ledgerIncompatible, SubstrateLedgerSignModeSupport } from '@subwallet/extension-koni-ui/constants'; // TODO: Use AccountSignMode from the background for consistency. import { AccountSignMode } from '@subwallet/extension-koni-ui/types'; import { useCallback } from 'react'; @@ -22,6 +22,7 @@ const usePreCheckAction = (address?: string, blockAllAccount = true, message?: s switch (signMode) { case AccountSignMode.LEGACY_LEDGER: case AccountSignMode.GENERIC_LEDGER: + case AccountSignMode.ECDSA_SUBSTRATE_LEDGER: return t('Ledger account'); case AccountSignMode.ALL_ACCOUNT: return t('All account'); @@ -72,6 +73,7 @@ const usePreCheckAction = (address?: string, blockAllAccount = true, message?: s case AccountSignMode.LEGACY_LEDGER: case AccountSignMode.GENERIC_LEDGER: + case AccountSignMode.ECDSA_SUBSTRATE_LEDGER: if (account.chainType === AccountChainType.ETHEREUM) { accountTitle = t('Ledger - EVM account'); } else if (account.chainType === AccountChainType.SUBSTRATE) { @@ -82,7 +84,7 @@ const usePreCheckAction = (address?: string, blockAllAccount = true, message?: s } } - if (mode === AccountSignMode.LEGACY_LEDGER || mode === AccountSignMode.GENERIC_LEDGER) { + if (SubstrateLedgerSignModeSupport.includes(mode)) { if (!isLedgerCapable) { notify({ message: t(ledgerIncompatible), diff --git a/packages/extension-koni-ui/src/hooks/ledger/useLedger.ts b/packages/extension-koni-ui/src/hooks/ledger/useLedger.ts index 7cca4c9ffb3..9faeddc838e 100644 --- a/packages/extension-koni-ui/src/hooks/ledger/useLedger.ts +++ b/packages/extension-koni-ui/src/hooks/ledger/useLedger.ts @@ -1,10 +1,11 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { LedgerNetwork, MigrationLedgerNetwork } from '@subwallet/extension-base/background/KoniTypes'; +import { LEDGER_SCHEME, LedgerNetwork, MigrationLedgerNetwork } from '@subwallet/extension-base/background/KoniTypes'; import { _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { createPromiseHandler, isSameAddress } from '@subwallet/extension-base/utils'; import { EVMLedger, SubstrateGenericLedger, SubstrateLegacyLedger, SubstrateMigrationLedger } from '@subwallet/extension-koni-ui/connector'; +import { SubstrateECDSALedger } from '@subwallet/extension-koni-ui/connector/Ledger/SubstrateECDSALedger'; import { isLedgerCapable, ledgerIncompatible, NotNeedMigrationGens } from '@subwallet/extension-koni-ui/constants'; import { useSelector } from '@subwallet/extension-koni-ui/hooks'; import { Ledger, SignMessageLedger, SignTransactionLedger } from '@subwallet/extension-koni-ui/types'; @@ -67,7 +68,9 @@ const retrieveLedger = (chainSlug: string, ledgerChains: LedgerNetwork[], migrat if (def.isEthereum) { return new EVMLedger('webusb', def.slip44); } else { - if (originGenesisHash) { + if (def.scheme === LEDGER_SCHEME.ECDSA) { + return new SubstrateECDSALedger('webusb', def.slip44); + } else if (originGenesisHash) { const def = getNetworkByGenesisHash(migrateLedgerChains, originGenesisHash); assert(def, 'There is no known Ledger app available for this chain'); diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useGetChainSlugsByAccount.ts b/packages/extension-koni-ui/src/hooks/screen/home/useGetChainSlugsByAccount.ts index e61dab201a2..9bf742a6fb4 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useGetChainSlugsByAccount.ts +++ b/packages/extension-koni-ui/src/hooks/screen/home/useGetChainSlugsByAccount.ts @@ -4,7 +4,7 @@ import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; import { AccountChainType } from '@subwallet/extension-base/types'; import { RootState } from '@subwallet/extension-koni-ui/stores'; -import { findAccountByAddress, getChainsByAccountAll, getChainsByAccountType, isAccountAll } from '@subwallet/extension-koni-ui/utils'; +import { findAccountByAddress, getChainsByAccountAll, getChainsByAccountType, isAccountAll, isHasOnlySubstrateEcdsaAccountProxy, isSubstrateEcdsaAccountProxy } from '@subwallet/extension-koni-ui/utils'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; @@ -65,6 +65,24 @@ export const useGetChainSlugsByAccount = (address?: string): string[] => { return undefined; }, [accountProxies, accounts, address, currentAccountProxy?.id]); + const isSubstrateECDSA = useMemo((): boolean => { + const _address = address || currentAccountProxy?.id; + + if (_address) { + if (isAccountAll(_address)) { + return isHasOnlySubstrateEcdsaAccountProxy(accountProxies); + } + + const proxy = accountProxies.find((proxy) => proxy.id === _address); + + if (proxy) { + return isSubstrateEcdsaAccountProxy(proxy); + } + } + + return false; + }, [accountProxies, address, currentAccountProxy?.id]); + return useMemo(() => { const _address = address || currentAccountProxy?.id; @@ -74,6 +92,6 @@ export const useGetChainSlugsByAccount = (address?: string): string[] => { return allAccount ? getChainsByAccountAll(allAccount, accountProxies, chainInfoMap) : []; } - return getChainsByAccountType(chainInfoMap, chainTypes, specialChain); - }, [address, currentAccountProxy?.id, accountProxies, chainTypes, chainInfoMap, specialChain]); + return getChainsByAccountType(chainInfoMap, chainTypes, specialChain, isSubstrateECDSA); + }, [address, currentAccountProxy?.id, chainInfoMap, chainTypes, specialChain, isSubstrateECDSA, accountProxies]); }; diff --git a/packages/extension-koni-ui/src/types/account.ts b/packages/extension-koni-ui/src/types/account.ts index 88598e0bf09..20aeef01c78 100644 --- a/packages/extension-koni-ui/src/types/account.ts +++ b/packages/extension-koni-ui/src/types/account.ts @@ -22,6 +22,7 @@ export enum AccountSignMode { QR = 'qr', LEGACY_LEDGER = 'legacy-ledger', GENERIC_LEDGER = 'generic-ledger', + ECDSA_SUBSTRATE_LEDGER = 'ecdsa-substrate-ledger', READ_ONLY = 'readonly', ALL_ACCOUNT = 'all', INJECTED = 'injected', diff --git a/packages/extension-koni-ui/src/utils/account/ledger.ts b/packages/extension-koni-ui/src/utils/account/ledger.ts index 8f7e6c00ed2..5391ee59c1d 100644 --- a/packages/extension-koni-ui/src/utils/account/ledger.ts +++ b/packages/extension-koni-ui/src/utils/account/ledger.ts @@ -5,6 +5,7 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { LedgerNetwork } from '@subwallet/extension-base/background/KoniTypes'; import { _ChainState } from '@subwallet/extension-base/services/chain-service/types'; import { _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { AccountProxy } from '@subwallet/extension-base/types'; import { PredefinedLedgerNetwork, RECOVERY_SLUG } from '@subwallet/extension-koni-ui/constants/ledger'; interface ChainItem extends _ChainState { @@ -27,3 +28,13 @@ export const getSupportedLedger = (networkInfoMap: Record, n }; export const convertNetworkSlug = (network: LedgerNetwork) => network.slug.concat(network.isRecovery ? RECOVERY_SLUG : ''); + +export const isSubstrateEcdsaAccountProxy = (accountProxy: AccountProxy) => { + return accountProxy.accounts.some((account) => account.isSubstrateECDSA); +}; + +export const isHasOnlySubstrateEcdsaAccountProxy = (accountProxies: AccountProxy[]) => { + return accountProxies.every((accountProxy) => { + return accountProxy.accounts.length === 1 && accountProxy.accounts[0].isSubstrateECDSA; + }); +}; diff --git a/packages/extension-koni-ui/src/utils/accountProxy/authorizeAccountProxy.ts b/packages/extension-koni-ui/src/utils/accountProxy/authorizeAccountProxy.ts index ccc70c6e601..9ed177fa1c1 100644 --- a/packages/extension-koni-ui/src/utils/accountProxy/authorizeAccountProxy.ts +++ b/packages/extension-koni-ui/src/utils/accountProxy/authorizeAccountProxy.ts @@ -5,17 +5,19 @@ import { AccountAuthType } from '@subwallet/extension-base/background/types'; import { AccountChainType, AccountProxy } from '@subwallet/extension-base/types'; import { isAccountAll } from '@subwallet/extension-koni-ui/utils'; -export const filterAuthorizeAccountProxies = (accountProxies: AccountProxy[], accountAuthTypes: AccountAuthType[]): AccountProxy[] => { - const rs = accountProxies.filter(({ chainTypes, id }) => { +export const filterAuthorizeAccountProxies = (accountProxies: AccountProxy[], accountAuthTypes: AccountAuthType[], isSubstrateConnector = false): AccountProxy[] => { + const rs = accountProxies.filter(({ accounts, chainTypes, id }) => { if (isAccountAll(id)) { return false; } + const isEcdsaAccountProxy = accounts.some((account) => account.isSubstrateECDSA); + return accountAuthTypes.some((type) => { if (type === 'substrate') { return chainTypes.includes(AccountChainType.SUBSTRATE); } else if (type === 'evm') { - return chainTypes.includes(AccountChainType.ETHEREUM); + return chainTypes.includes(AccountChainType.ETHEREUM) && (isSubstrateConnector || !isEcdsaAccountProxy); } else if (type === 'ton') { return chainTypes.includes(AccountChainType.TON); } else if (type === 'cardano') { diff --git a/packages/extension-koni-ui/src/utils/chain/chain.ts b/packages/extension-koni-ui/src/utils/chain/chain.ts index 93371a8b53b..a0c693b3a85 100644 --- a/packages/extension-koni-ui/src/utils/chain/chain.ts +++ b/packages/extension-koni-ui/src/utils/chain/chain.ts @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { _ChainInfo, _ChainStatus } from '@subwallet/chain-list/types'; -import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureSubstrateChain } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureSubstrateChain, _isSubstrateEvmCompatibleChain } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountChainType, AccountProxy } from '@subwallet/extension-base/types'; import { isAccountAll } from '@subwallet/extension-base/utils'; +import { isHasOnlySubstrateEcdsaAccountProxy } from '@subwallet/extension-koni-ui/utils'; export const findChainInfoByGenesisHash = (chainMap: Record, genesisHash?: string): _ChainInfo | null => { if (!genesisHash) { @@ -62,7 +63,7 @@ export const isChainCompatibleWithAccountChainTypes = (chainInfo: _ChainInfo, ch return chainTypes.some((chainType) => isChainInfoAccordantAccountChainType(chainInfo, chainType)); }; -export const getChainsByAccountType = (_chainInfoMap: Record, chainTypes: AccountChainType[], specialChain?: string): string[] => { +export const getChainsByAccountType = (_chainInfoMap: Record, chainTypes: AccountChainType[], specialChain?: string, isSubstrateECDSA?: boolean): string[] => { const chainInfoMap = Object.fromEntries(Object.entries(_chainInfoMap).filter(([, chainInfo]) => chainInfo.chainStatus === _ChainStatus.ACTIVE)); if (specialChain) { @@ -71,7 +72,11 @@ export const getChainsByAccountType = (_chainInfoMap: Record const result: string[] = []; for (const chainInfo of Object.values(chainInfoMap)) { - if (isChainCompatibleWithAccountChainTypes(chainInfo, chainTypes)) { + if (isSubstrateECDSA) { + if (_isSubstrateEvmCompatibleChain(chainInfo)) { + result.push(chainInfo.slug); + } + } else if (isChainCompatibleWithAccountChainTypes(chainInfo, chainTypes)) { result.push(chainInfo.slug); } } @@ -84,12 +89,25 @@ export const getChainsByAccountType = (_chainInfoMap: Record export const getChainsByAccountAll = (accountAllProxy: AccountProxy, accountProxies: AccountProxy[], _chainInfoMap: Record): string[] => { const specialChainRecord: Record = {} as Record; const { chainTypes, specialChain } = accountAllProxy; + const isSubstrateECDSA = isHasOnlySubstrateEcdsaAccountProxy(accountProxies); const chainInfoMap = Object.fromEntries(Object.entries(_chainInfoMap).filter(([, chainInfo]) => chainInfo.chainStatus === _ChainStatus.ACTIVE)); /* Special chain List *: All network */ + const result: string[] = []; + + if (isSubstrateECDSA) { + Object.values(chainInfoMap).forEach((chainInfo) => { + if (_isSubstrateEvmCompatibleChain(chainInfo)) { + result.push(chainInfo.slug); + } + }); + + return result; + } + for (const proxy of accountProxies) { if (proxy.specialChain) { specialChainRecord[proxy.chainTypes[0]] = [...specialChainRecord[proxy.chainTypes[0]] || [], proxy.specialChain]; @@ -100,8 +118,6 @@ export const getChainsByAccountAll = (accountAllProxy: AccountProxy, accountProx } } - const result: string[] = []; - if (!specialChain) { Object.values(chainInfoMap).forEach((chainInfo) => { const isAllowed = chainTypes.some((chainType) => { diff --git a/packages/extension-koni-ui/src/utils/chain/chainAndAsset.ts b/packages/extension-koni-ui/src/utils/chain/chainAndAsset.ts index e5909e510f8..8413c748bc1 100644 --- a/packages/extension-koni-ui/src/utils/chain/chainAndAsset.ts +++ b/packages/extension-koni-ui/src/utils/chain/chainAndAsset.ts @@ -4,7 +4,7 @@ import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; import { AssetSetting } from '@subwallet/extension-base/background/KoniTypes'; import { _ChainState } from '@subwallet/extension-base/services/chain-service/types'; -import { _getOriginChainOfAsset, _isAssetFungibleToken } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getOriginChainOfAsset, _isAssetFungibleToken, _isSubstrateEvmCompatibleChain } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountChainType } from '@subwallet/extension-base/types'; import { isChainCompatibleWithAccountChainTypes } from '@subwallet/extension-koni-ui/utils'; @@ -38,8 +38,13 @@ export function getChainInfoFromToken (tokenSlug: string, chainInfoMap: Record): boolean { + chainInfoMap: Record, + isSubstrateECDSA?: boolean): boolean { const chainInfo = getChainInfoFromToken(tokenSlug, chainInfoMap); + if (isSubstrateECDSA) { + return !!chainInfo && _isSubstrateEvmCompatibleChain(chainInfo); + } + return !!chainInfo && isChainCompatibleWithAccountChainTypes(chainInfo, chainTypes); } diff --git a/yarn.lock b/yarn.lock index 87450b25afa..9b599c32651 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9441,23 +9441,23 @@ __metadata: languageName: node linkType: hard -"@zondax/ledger-js@npm:^0.11.0": - version: 0.11.0 - resolution: "@zondax/ledger-js@npm:0.11.0" +"@zondax/ledger-js@npm:^1.2.0": + version: 1.2.0 + resolution: "@zondax/ledger-js@npm:1.2.0" dependencies: - "@ledgerhq/hw-transport": 6.30.6 - checksum: 38743b887fceaeb13460d70a127cb385f5df034fe014f698d4f164f0d111add71dd01381c182ac71135c7fb43f38e7b21218d5df7d87d69ddaee17dd9c57d392 + "@ledgerhq/hw-transport": 6.31.4 + checksum: d6982119c7dc8d150c8aba8b26509b5112e4b0bb299f290739b64fad1650e9d820d179d275da9d28cae7bc4fcc28c69865cd0952847cefa0277e08d35a9cfc87 languageName: node linkType: hard -"@zondax/ledger-substrate@npm:1.0.1": - version: 1.0.1 - resolution: "@zondax/ledger-substrate@npm:1.0.1" +"@zondax/ledger-substrate@npm:1.1.2": + version: 1.1.2 + resolution: "@zondax/ledger-substrate@npm:1.1.2" dependencies: - "@ledgerhq/hw-transport": 6.31.2 - "@zondax/ledger-js": ^0.11.0 - axios: ^1.7.4 - checksum: e5fd0743bb6a5638c7a633131a0f4db84e9e8eec720fa659f6f116a8fe6629125b6d472184683c1a64a88fadf89bb54c6e3ffa4fa063470b4395df87d7be0bb0 + "@ledgerhq/hw-transport": 6.31.4 + "@zondax/ledger-js": ^1.2.0 + axios: ^1.8.4 + checksum: 0f9e30f70c49a87496f07a4b074f6d130e21a1c937a455be4138a5e9568ec3f673c28a05bcf87ed9972804f429f0379d3ecf22464eb9dbc8937bbcc822133d55 languageName: node linkType: hard @@ -10353,14 +10353,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.7.4": - version: 1.7.9 - resolution: "axios@npm:1.7.9" +"axios@npm:^1.8.4": + version: 1.9.0 + resolution: "axios@npm:1.9.0" dependencies: follow-redirects: ^1.15.6 form-data: ^4.0.0 proxy-from-env: ^1.1.0 - checksum: cb8ce291818effda09240cb60f114d5625909b345e10f389a945320e06acf0bc949d0f8422d25720f5dd421362abee302c99f5e97edec4c156c8939814b23d19 + checksum: 631f02c9c279f2ae90637a4989cc9d75c1c27aefd16b6e8eb90f98a4d0bddaccfd1cb1387be12101d1ab0f9bbf0c47e2451b4de0cf2870462a7d9ed3de8da3f2 languageName: node linkType: hard