diff --git a/.env.example b/.env.example index ab9f2dad4b6..61e7fadaffe 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,4 @@ BITTENSOR_API_KEY_8=tao-abafdbad8 BITTENSOR_API_KEY_9=tao-abafdbad9 BITTENSOR_API_KEY_10=tao-abafdbad10 SIMPLE_SWAP_API_KEY=26c8c21 +SUBWALLET_API=localhost diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index a537161c924..39731cbe3bf 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -47,7 +47,7 @@ jobs: TRANSAK_API_KEY: ${{ secrets.TRANSAK_API_KEY }} COINBASE_PAY_ID: ${{ secrets.COINBASE_PAY_ID }} CHAINFLIP_BROKER_API: ${{ secrets.CHAINFLIP_BROKER_API }} - BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} + BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} BITTENSOR_API_KEY_2: ${{ secrets.BITTENSOR_API_KEY_2 }} BITTENSOR_API_KEY_3: ${{ secrets.BITTENSOR_API_KEY_3 }} BITTENSOR_API_KEY_4: ${{ secrets.BITTENSOR_API_KEY_4 }} @@ -57,6 +57,7 @@ jobs: BITTENSOR_API_KEY_8: ${{ secrets.BITTENSOR_API_KEY_8 }} BITTENSOR_API_KEY_9: ${{ secrets.BITTENSOR_API_KEY_9 }} BITTENSOR_API_KEY_10: ${{ secrets.BITTENSOR_API_KEY_10 }} + SUBWALLET_API: ${{ secrets.SUBWALLET_API }} SIMPLE_SWAP_API_KEY: ${{ secrets.SIMPLE_SWAP_API_KEY }} GH_RELEASE_FILES: master-build.zip,master-src.zip COMMIT_MESSAGE: ${{ github.event.head_commit.message }} diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index fa700048b3a..e057f1e8a0f 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -28,7 +28,7 @@ jobs: TRANSAK_API_KEY: ${{ secrets.TRANSAK_API_KEY }} COINBASE_PAY_ID: ${{ secrets.COINBASE_PAY_ID }} CHAINFLIP_BROKER_API: ${{ secrets.CHAINFLIP_BROKER_API }} - BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} + BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} BITTENSOR_API_KEY_2: ${{ secrets.BITTENSOR_API_KEY_2 }} BITTENSOR_API_KEY_3: ${{ secrets.BITTENSOR_API_KEY_3 }} BITTENSOR_API_KEY_4: ${{ secrets.BITTENSOR_API_KEY_4 }} @@ -39,6 +39,7 @@ jobs: BITTENSOR_API_KEY_9: ${{ secrets.BITTENSOR_API_KEY_9 }} BITTENSOR_API_KEY_10: ${{ secrets.BITTENSOR_API_KEY_10 }} SIMPLE_SWAP_API_KEY: ${{ secrets.SIMPLE_SWAP_API_KEY }} + SUBWALLET_API: ${{ secrets.SUBWALLET_API }} BRANCH_NAME: ${{ github.ref_name }} run: | yarn install --immutable | grep -v 'YN0013' diff --git a/.github/workflows/push-web-runner.yml b/.github/workflows/push-web-runner.yml index e27c73f5a49..a617d09f5b4 100644 --- a/.github/workflows/push-web-runner.yml +++ b/.github/workflows/push-web-runner.yml @@ -32,7 +32,7 @@ jobs: TRANSAK_API_KEY: ${{ secrets.TRANSAK_API_KEY }} COINBASE_PAY_ID: ${{ secrets.COINBASE_PAY_ID }} CHAINFLIP_BROKER_API: ${{ secrets.CHAINFLIP_BROKER_API }} - BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} + BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} BITTENSOR_API_KEY_2: ${{ secrets.BITTENSOR_API_KEY_2 }} BITTENSOR_API_KEY_3: ${{ secrets.BITTENSOR_API_KEY_3 }} BITTENSOR_API_KEY_4: ${{ secrets.BITTENSOR_API_KEY_4 }} @@ -43,6 +43,7 @@ jobs: BITTENSOR_API_KEY_9: ${{ secrets.BITTENSOR_API_KEY_9 }} BITTENSOR_API_KEY_10: ${{ secrets.BITTENSOR_API_KEY_10 }} SIMPLE_SWAP_API_KEY: ${{ secrets.SIMPLE_SWAP_API_KEY }} + SUBWALLET_API: ${{ secrets.SUBWALLET_API }} BRANCH_NAME: master run: | yarn install --immutable | grep -v 'YN0013' diff --git a/.github/workflows/push-webapp.yml b/.github/workflows/push-webapp.yml index 28568cf2fc9..0eda76b82e1 100644 --- a/.github/workflows/push-webapp.yml +++ b/.github/workflows/push-webapp.yml @@ -32,7 +32,7 @@ jobs: COINBASE_PAY_ID: ${{ secrets.COINBASE_PAY_ID }} NFT_MINTING_HOST: ${{ secrets.NFT_MINTING_HOST }} CHAINFLIP_BROKER_API: ${{ secrets.CHAINFLIP_BROKER_API }} - BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} + BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} BITTENSOR_API_KEY_2: ${{ secrets.BITTENSOR_API_KEY_2 }} BITTENSOR_API_KEY_3: ${{ secrets.BITTENSOR_API_KEY_3 }} BITTENSOR_API_KEY_4: ${{ secrets.BITTENSOR_API_KEY_4 }} @@ -43,6 +43,7 @@ jobs: BITTENSOR_API_KEY_9: ${{ secrets.BITTENSOR_API_KEY_9 }} BITTENSOR_API_KEY_10: ${{ secrets.BITTENSOR_API_KEY_10 }} SIMPLE_SWAP_API_KEY: ${{ secrets.SIMPLE_SWAP_API_KEY }} + SUBWALLET_API: ${{ secrets.SUBWALLET_API }} BRANCH_NAME: ${{ github.ref_name }} run: | yarn install --immutable | grep -v 'YN0013' diff --git a/package.json b/package.json index f2a4b6cf4c0..ab231b27817 100644 --- a/package.json +++ b/package.json @@ -105,10 +105,10 @@ "@polkadot/types-support": "^15.0.1", "@polkadot/util": "^13.2.3", "@polkadot/util-crypto": "^13.2.3", - "@subwallet/chain-list": "0.2.98", - "@subwallet/keyring": "^0.1.8-beta.0", + "@subwallet/chain-list": "file:../SubWallet-Chainlist/packages/chain-list/build/", + "@subwallet/keyring": "file:../SubWallet-Base/packages/keyring/build/", "@subwallet/react-ui": "5.1.2-b79", - "@subwallet/ui-keyring": "0.1.8-beta.0", + "@subwallet/ui-keyring": "file:../SubWallet-Base/packages/ui-keyring/build/", "@types/bn.js": "^5.1.6", "@zondax/ledger-substrate": "1.0.1", "babel-core": "^7.0.0-bridge.0", diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json index cbdf6bca56b..9a7d54f9ef0 100644 --- a/packages/extension-base/package.json +++ b/packages/extension-base/package.json @@ -24,6 +24,7 @@ "@apollo/client": "^3.7.14", "@azns/resolver-core": "^1.4.0", "@chainflip/sdk": "^1.6.0", + "@emurgo/cardano-serialization-lib-nodejs": "^13.2.0", "@equilab/api": "~1.14.25", "@ethereumjs/common": "^4.1.0", "@ethereumjs/tx": "^5.1.0", @@ -61,6 +62,7 @@ "@subwallet/extension-dapp": "^1.3.15-0", "@subwallet/extension-inject": "^1.3.15-0", "@subwallet/keyring": "^0.1.8-beta.0", + "@subwallet/subwallet-api-sdk": "^1.3.12-1", "@subwallet/ui-keyring": "^0.1.8-beta.0", "@ton/core": "^0.56.3", "@ton/crypto": "^3.2.0", diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 26778ad2670..93289efe184 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -7,6 +7,7 @@ import { Resolver } from '@subwallet/extension-base/background/handlers/State'; import { AccountAuthType, AuthorizeRequest, ConfirmationRequestBase, RequestAccountList, RequestAccountSubscribe, RequestAccountUnsubscribe, RequestAuthorizeCancel, RequestAuthorizeReject, RequestAuthorizeSubscribe, RequestAuthorizeTab, RequestCurrentAccountAddress, ResponseAuthorizeList } from '@subwallet/extension-base/background/types'; import { AppConfig, BrowserConfig, OSConfig } from '@subwallet/extension-base/constants'; import { RequestOptimalTransferProcess } from '@subwallet/extension-base/services/balance-service/helpers'; +import { CardanoTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/cardano-transfer'; import { TonTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _ChainState, _EvmApi, _NetworkUpsertParams, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse, EnableChainParams, EnableMultiChainParams } from '@subwallet/extension-base/services/chain-service/types'; @@ -16,7 +17,7 @@ import { AuthUrls } from '@subwallet/extension-base/services/request-service/typ import { CrowdloanContributionsResponse } from '@subwallet/extension-base/services/subscan-service/types'; import { SWTransactionResponse, SWTransactionResult } from '@subwallet/extension-base/services/transaction-service/types'; import { WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; -import { AccountJson, AccountsWithCurrentAddress, AddressJson, BalanceJson, BaseRequestSign, BuyServiceInfo, BuyTokenInfo, CommonOptimalPath, CurrentAccountInfo, EarningRewardHistoryItem, EarningRewardJson, EarningStatus, HandleYieldStepParams, InternalRequestSign, LeavePoolAdditionalData, NominationPoolInfo, OptimalYieldPath, OptimalYieldPathParams, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestAccountProxyEdit, RequestAccountProxyForget, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeTonWalletContractVersion, RequestCheckCrossChainTransfer, RequestCheckPublicAndSecretKey, RequestCheckTransfer, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseEarlyValidateYield, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, StorageDataInterface, SubmitYieldStepData, SwapPair, SwapQuoteResponse, SwapRequest, SwapRequestResult, SwapSubmitParams, SwapTxData, TokenSpendingApprovalParams, UnlockDotTransactionNft, UnstakingStatus, ValidateSwapProcessParams, ValidateYieldProcessParams, YieldPoolInfo, YieldPositionInfo } from '@subwallet/extension-base/types'; +import { AccountChainType, AccountJson, AccountsWithCurrentAddress, AddressJson, BalanceJson, BaseRequestSign, BuyServiceInfo, BuyTokenInfo, CommonOptimalPath, CurrentAccountInfo, EarningRewardHistoryItem, EarningRewardJson, EarningStatus, HandleYieldStepParams, InternalRequestSign, LeavePoolAdditionalData, NominationPoolInfo, OptimalYieldPath, OptimalYieldPathParams, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestAccountProxyEdit, RequestAccountProxyForget, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeTonWalletContractVersion, RequestCheckCrossChainTransfer, RequestCheckPublicAndSecretKey, RequestCheckTransfer, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseEarlyValidateYield, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, StorageDataInterface, SubmitYieldStepData, SwapPair, SwapQuoteResponse, SwapRequest, SwapRequestResult, SwapSubmitParams, SwapTxData, TokenSpendingApprovalParams, UnlockDotTransactionNft, UnstakingStatus, ValidateSwapProcessParams, ValidateYieldProcessParams, YieldPoolInfo, YieldPositionInfo } from '@subwallet/extension-base/types'; import { RequestClaimBridge } from '@subwallet/extension-base/types/bridge'; import { GetNotificationParams, RequestIsClaimedPolygonBridge, RequestSwitchStatusParams } from '@subwallet/extension-base/types/notification'; import { InjectedAccount, InjectedAccountWithMeta, MetadataDefBase } from '@subwallet/extension-inject/types'; @@ -435,6 +436,9 @@ export interface UiSettings { unlockType: WalletUnlockType; enableChainPatrol: boolean; notificationSetup: NotificationSetup; + isAcknowledgedUnifiedAccountMigration: boolean; + isUnifiedAccountMigrationInProgress: boolean; + isUnifiedAccountMigrationDone: boolean; // On-ramp service account reference walletReference: string; } @@ -478,7 +482,9 @@ export enum TransactionDirection { export enum ChainType { EVM = 'evm', SUBSTRATE = 'substrate', - TON = 'ton' + BITCOIN = 'bitcoin', + TON = 'ton', + CARDANO = 'cardano' } export enum ExtrinsicType { @@ -1091,6 +1097,12 @@ export interface TonSignRequest { canSign: boolean; } +export interface CardanoSignRequest { + account: AccountJson; + hashPayload: string; + canSign: boolean; +} + export interface ErrorValidation { message: string; name: string; @@ -1109,6 +1121,12 @@ export interface TonSignatureRequest extends TonSignRequest { payload: unknown; } +export interface CardanoSignatureRequest extends CardanoSignRequest { + id: string; + type: string; + payload: unknown +} + export interface EvmSendTransactionRequest extends TransactionConfig, EvmSignRequest { estimateGas: string; parseData: EvmTransactionData; @@ -1118,9 +1136,11 @@ export interface EvmSendTransactionRequest extends TransactionConfig, EvmSignReq // TODO: add account info + dataToSign export type TonSendTransactionRequest = TonTransactionConfig; +export type CardanoSendTransactionRequest = CardanoTransactionConfig; export type EvmWatchTransactionRequest = EvmSendTransactionRequest; export type TonWatchTransactionRequest = TonSendTransactionRequest; +export type CardanoWatchTransactionRequest = CardanoSendTransactionRequest; export interface ConfirmationsQueueItemOptions { requiredPassword?: boolean; @@ -1186,8 +1206,15 @@ export interface ConfirmationDefinitionsTon { tonWatchTransactionRequest: [ConfirmationsQueueItem<TonWatchTransactionRequest>, ConfirmationResult<string>] } +export interface ConfirmationDefinitionsCardano { + cardanoSignatureRequest: [ConfirmationsQueueItem<CardanoSignatureRequest>, ConfirmationResult<string>], + cardanoSendTransactionRequest: [ConfirmationsQueueItem<CardanoSendTransactionRequest>, ConfirmationResult<string>], + cardanoWatchTransactionRequest: [ConfirmationsQueueItem<CardanoWatchTransactionRequest>, ConfirmationResult<string>] +} + export type ConfirmationType = keyof ConfirmationDefinitions; export type ConfirmationTypeTon = keyof ConfirmationDefinitionsTon; +export type ConfirmationTypeCardano = keyof ConfirmationDefinitionsCardano; export type ConfirmationsQueue = { [CT in ConfirmationType]: Record<string, ConfirmationDefinitions[CT][0]>; @@ -1195,19 +1222,24 @@ export type ConfirmationsQueue = { export type ConfirmationsQueueTon = { [CT in ConfirmationTypeTon]: Record<string, ConfirmationDefinitionsTon[CT][0]>; } +export type ConfirmationsQueueCardano = { + [CT in ConfirmationTypeCardano]: Record<string, ConfirmationDefinitionsCardano[CT][0]>; +} export type RequestConfirmationsSubscribe = null; - export type RequestConfirmationsSubscribeTon = null; +export type RequestConfirmationsSubscribeCardano = null; // Design to use only one confirmation export type RequestConfirmationComplete = { [CT in ConfirmationType]?: ConfirmationDefinitions[CT][1]; } - export type RequestConfirmationCompleteTon = { [CT in ConfirmationTypeTon]?: ConfirmationDefinitionsTon[CT][1]; } +export type RequestConfirmationCompleteCardano = { + [CT in ConfirmationTypeCardano]?: ConfirmationDefinitionsCardano[CT][1]; +} export interface BondingOptionParams { chain: string; @@ -1904,6 +1936,47 @@ export interface ResponseNftImport { /* Campaign */ +/* Migrate Unified Account */ +export interface RequestSaveMigrationAcknowledgedStatus { + isAcknowledgedUnifiedAccountMigration: boolean; +} + +export interface RequestSaveUnifiedAccountMigrationInProgress { + isUnifiedAccountMigrationInProgress: boolean; +} + +export interface RequestMigrateUnifiedAndFetchEligibleSoloAccounts { + password: string +} + +export interface ResponseMigrateUnifiedAndFetchEligibleSoloAccounts { + migratedUnifiedAccountIds: string[], + soloAccounts: Record<string, SoloAccountToBeMigrated[]> + sessionId: string; // to keep linking to password in state +} + +export interface SoloAccountToBeMigrated { + upcomingProxyId: string, + proxyId: string, + address: string, + name: string, + chainType: AccountChainType +} + +export interface RequestMigrateSoloAccount { + soloAccounts: SoloAccountToBeMigrated[]; + accountName: string; + sessionId: string; +} + +export interface ResponseMigrateSoloAccount { + migratedUnifiedAccountId: string +} + +export interface RequestPingSession { + sessionId: string; +} + /* Core types */ export type _Address = string; export type _BalanceMetadata = unknown; @@ -2083,7 +2156,10 @@ export interface KoniRequestSignatures { 'pri(settings.saveAutoLockTime)': [RequestChangeTimeAutoLock, boolean]; 'pri(settings.saveUnlockType)': [RequestUnlockType, boolean]; 'pri(settings.saveEnableChainPatrol)': [RequestChangeEnableChainPatrol, boolean]; - 'pri(settings.saveNotificationSetup)': [NotificationSetup, boolean] + 'pri(settings.saveNotificationSetup)': [NotificationSetup, boolean]; + 'pri(settings.saveUnifiedAccountMigrationInProgress)': [RequestSaveUnifiedAccountMigrationInProgress, boolean]; + 'pri(settings.pingUnifiedAccountMigrationDone)': [null, boolean]; + 'pri(settings.saveMigrationAcknowledgedStatus)': [RequestSaveMigrationAcknowledgedStatus, boolean]; 'pri(settings.saveLanguage)': [RequestChangeLanguage, boolean]; 'pri(settings.savePriceCurrency)': [RequestChangePriceCurrency, boolean]; 'pri(settings.saveShowZeroBalance)': [RequestChangeShowZeroBalance, boolean]; @@ -2167,8 +2243,10 @@ export interface KoniRequestSignatures { // Confirmation Queues 'pri(confirmations.subscribe)': [RequestConfirmationsSubscribe, ConfirmationsQueue, ConfirmationsQueue]; 'pri(confirmationsTon.subscribe)': [RequestConfirmationsSubscribeTon, ConfirmationsQueueTon, ConfirmationsQueueTon]; + 'pri(confirmationsCardano.subscribe)': [RequestConfirmationsSubscribeCardano, ConfirmationsQueueCardano, ConfirmationsQueueCardano]; 'pri(confirmations.complete)': [RequestConfirmationComplete, boolean]; 'pri(confirmationsTon.complete)': [RequestConfirmationCompleteTon, boolean]; + 'pri(confirmationsCardano.complete)': [RequestConfirmationCompleteCardano, boolean]; 'pub(utils.getRandom)': [RandomTestRequest, number]; 'pub(accounts.listV2)': [RequestAccountList, InjectedAccount[]]; @@ -2308,6 +2386,11 @@ export interface KoniRequestSignatures { /* Ledger */ 'pri(ledger.generic.allow)': [null, string[], string[]]; + + /* Migrate Unified Account */ + 'pri(migrate.migrateUnifiedAndFetchEligibleSoloAccounts)': [RequestMigrateUnifiedAndFetchEligibleSoloAccounts, ResponseMigrateUnifiedAndFetchEligibleSoloAccounts]; + 'pri(migrate.migrateSoloAccount)': [RequestMigrateSoloAccount, ResponseMigrateSoloAccount]; + 'pri(migrate.pingSession)': [RequestPingSession, boolean]; } export interface ApplicationMetadataType { diff --git a/packages/extension-base/src/constants/signing.ts b/packages/extension-base/src/constants/signing.ts index f2eb61466f7..3d1b83eb608 100644 --- a/packages/extension-base/src/constants/signing.ts +++ b/packages/extension-base/src/constants/signing.ts @@ -7,11 +7,15 @@ import { AccountChainType } from '@subwallet/extension-base/types'; export const SIGNING_COMPATIBLE_MAP: Record<ChainType, AccountChainType[]> = { [ChainType.SUBSTRATE]: [AccountChainType.SUBSTRATE, AccountChainType.ETHEREUM], [ChainType.EVM]: [AccountChainType.ETHEREUM], - [ChainType.TON]: [AccountChainType.TON] + [ChainType.BITCOIN]: [AccountChainType.BITCOIN], + [ChainType.TON]: [AccountChainType.TON], + [ChainType.CARDANO]: [AccountChainType.CARDANO] }; export const LEDGER_SIGNING_COMPATIBLE_MAP: Record<ChainType, AccountChainType[]> = { [ChainType.SUBSTRATE]: [AccountChainType.SUBSTRATE], [ChainType.EVM]: [AccountChainType.ETHEREUM], - [ChainType.TON]: [AccountChainType.TON] + [ChainType.BITCOIN]: [AccountChainType.BITCOIN], + [ChainType.TON]: [AccountChainType.TON], + [ChainType.CARDANO]: [AccountChainType.CARDANO] }; diff --git a/packages/extension-base/src/core/logic-validation/recipientAddress.ts b/packages/extension-base/src/core/logic-validation/recipientAddress.ts index 91da1b52732..d4b04b1e034 100644 --- a/packages/extension-base/src/core/logic-validation/recipientAddress.ts +++ b/packages/extension-base/src/core/logic-validation/recipientAddress.ts @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { ActionType, ValidateRecipientParams, ValidationCondition } from '@subwallet/extension-base/core/types'; -import { _isAddress, _isNotDuplicateAddress, _isNotNull, _isSupportLedgerAccount, _isValidAddressForEcosystem, _isValidSubstrateAddressFormat, _isValidTonAddressFormat } from '@subwallet/extension-base/core/utils'; +import { _isAddress, _isNotDuplicateAddress, _isNotNull, _isSupportLedgerAccount, _isValidAddressForEcosystem, _isValidCardanoAddressFormat, _isValidSubstrateAddressFormat, _isValidTonAddressFormat } from '@subwallet/extension-base/core/utils'; import { AccountSignMode } from '@subwallet/extension-base/types'; import { detectTranslate } from '@subwallet/extension-base/utils'; -import { isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; +import { isCardanoAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; function getConditions (validateRecipientParams: ValidateRecipientParams): ValidationCondition[] { const { account, actionType, autoFormatValue, destChainInfo, srcChain, toAddress } = validateRecipientParams; @@ -25,7 +25,11 @@ function getConditions (validateRecipientParams: ValidateRecipientParams): Valid conditions.push(ValidationCondition.IS_VALID_TON_ADDRESS_FORMAT); } - if (srcChain === destChainInfo.slug && isSendAction && !destChainInfo.tonInfo) { + if (isCardanoAddress(toAddress)) { + conditions.push(ValidationCondition.IS_VALID_CARDANO_ADDRESS_FORMAT); + } + + if (srcChain === destChainInfo.slug && isSendAction && !destChainInfo.tonInfo && !destChainInfo.cardanoInfo) { conditions.push(ValidationCondition.IS_NOT_DUPLICATE_ADDRESS); } @@ -75,6 +79,12 @@ function getValidationFunctions (conditions: ValidationCondition[]): Array<(vali break; } + case ValidationCondition.IS_VALID_CARDANO_ADDRESS_FORMAT: { + validationFunctions.push(_isValidCardanoAddressFormat); + + break; + } + case ValidationCondition.IS_NOT_DUPLICATE_ADDRESS: { validationFunctions.push(_isNotDuplicateAddress); diff --git a/packages/extension-base/src/core/logic-validation/transfer.ts b/packages/extension-base/src/core/logic-validation/transfer.ts index 57be57f2cdc..5875ddd0208 100644 --- a/packages/extension-base/src/core/logic-validation/transfer.ts +++ b/packages/extension-base/src/core/logic-validation/transfer.ts @@ -8,16 +8,17 @@ import { TransactionWarning } from '@subwallet/extension-base/background/warning import { LEDGER_SIGNING_COMPATIBLE_MAP, SIGNING_COMPATIBLE_MAP, XCM_MIN_AMOUNT_RATIO } from '@subwallet/extension-base/constants'; import { _canAccountBeReaped, _isAccountActive } from '@subwallet/extension-base/core/substrate/system-pallet'; import { FrameSystemAccountInfo } from '@subwallet/extension-base/core/substrate/types'; +import { getCardanoAssetId } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/utils'; import { isBounceableAddress } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/utils'; import { _TRANSFER_CHAIN_GROUP } from '@subwallet/extension-base/services/chain-service/constants'; import { _EvmApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetDecimals, _getChainExistentialDeposit, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getTokenMinAmount, _isNativeToken, _isTokenEvmSmartContract, _isTokenTonSmartContract } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getChainExistentialDeposit, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getTokenMinAmount, _isCIP26Token, _isNativeToken, _isTokenEvmSmartContract, _isTokenTonSmartContract } from '@subwallet/extension-base/services/chain-service/utils'; import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; -import { isSubstrateTransaction, isTonTransaction } from '@subwallet/extension-base/services/transaction-service/helpers'; +import { isCardanoTransaction, isSubstrateTransaction, isTonTransaction } from '@subwallet/extension-base/services/transaction-service/helpers'; import { OptionalSWTransaction, SWTransactionInput, SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; import { AccountSignMode, BasicTxErrorType, BasicTxWarningCode, TransferTxErrorType } from '@subwallet/extension-base/types'; import { balanceFormatter, formatNumber, pairToAccount } from '@subwallet/extension-base/utils'; -import { isTonAddress } from '@subwallet/keyring'; +import { isCardanoAddress, isTonAddress } from '@subwallet/keyring'; import { KeyringPair } from '@subwallet/keyring/types'; import { keyring } from '@subwallet/ui-keyring'; import BigN from 'bignumber.js'; @@ -53,6 +54,10 @@ export function validateTransferRequest (tokenInfo: _ChainAsset, from: _Address, errors.push(new TransactionError(BasicTxErrorType.INVALID_PARAMS, t('Not found TEP74 address for this token'))); } + if (isCardanoAddress(from) && isCardanoAddress(to) && _isCIP26Token(tokenInfo) && getCardanoAssetId(tokenInfo).length === 0) { + errors.push(new TransactionError(BasicTxErrorType.INVALID_PARAMS, t('Not found policy id of this token'))); + } + return [errors, keypair, transferValue]; } @@ -388,6 +393,8 @@ export async function estimateFeeForTransaction (validationResponse: SWTransacti estimateFee.value = (await transaction.paymentInfo(validationResponse.address)).partialFee.toString(); } else if (isTonTransaction(transaction)) { estimateFee.value = transaction.estimateFee; // todo: might need to update logic estimate fee inside for future actions excluding normal transfer Ton and Jetton + } else if (isCardanoTransaction(transaction)) { + estimateFee.value = transaction.estimateCardanoFee; } else { const gasLimit = transaction.gas || await evmApi.api.eth.estimateGas(transaction); diff --git a/packages/extension-base/src/core/types.ts b/packages/extension-base/src/core/types.ts index 3543493e4b3..41d9a680f31 100644 --- a/packages/extension-base/src/core/types.ts +++ b/packages/extension-base/src/core/types.ts @@ -12,6 +12,7 @@ export enum ValidationCondition { IS_VALID_ADDRESS_FOR_ECOSYSTEM = 'IS_VALID_ADDRESS_FOR_ECOSYSTEM', IS_VALID_SUBSTRATE_ADDRESS_FORMAT = 'IS_VALID_SUBSTRATE_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', IS_SUPPORT_LEDGER_ACCOUNT = 'IS_SUPPORT_LEDGER_ACCOUNT' } diff --git a/packages/extension-base/src/core/utils.ts b/packages/extension-base/src/core/utils.ts index c3899a1e036..380f4552c61 100644 --- a/packages/extension-base/src/core/utils.ts +++ b/packages/extension-base/src/core/utils.ts @@ -5,10 +5,10 @@ import { ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { BalanceAccountType } from '@subwallet/extension-base/core/substrate/types'; import { LedgerMustCheckType, ValidateRecipientParams } from '@subwallet/extension-base/core/types'; import { tonAddressInfo } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/utils'; -import { _isChainEvmCompatible, _isChainSubstrateCompatible, _isChainTonCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { _isChainCardanoCompatible, _isChainEvmCompatible, _isChainSubstrateCompatible, _isChainTonCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountJson } from '@subwallet/extension-base/types'; import { isAddressAndChainCompatible, isSameAddress, reformatAddress } from '@subwallet/extension-base/utils'; -import { isAddress, isTonAddress } from '@subwallet/keyring'; +import { isAddress, isCardanoTestnetAddress, isTonAddress } from '@subwallet/keyring'; import { isEthereumAddress } from '@polkadot/util-crypto'; @@ -64,7 +64,8 @@ export function _isValidAddressForEcosystem (validateRecipientParams: ValidateRe if (!isAddressAndChainCompatible(toAddress, destChainInfo)) { if (_isChainEvmCompatible(destChainInfo) || _isChainSubstrateCompatible(destChainInfo) || - _isChainTonCompatible(destChainInfo)) { + _isChainTonCompatible(destChainInfo) || + _isChainCardanoCompatible(destChainInfo)) { return 'Recipient address must be the same type as sender address'; } @@ -98,6 +99,16 @@ export function _isValidTonAddressFormat (validateRecipientParams: ValidateRecip return ''; } +export function _isValidCardanoAddressFormat (validateRecipientParams: ValidateRecipientParams): string { + const { destChainInfo, toAddress } = validateRecipientParams; + + if (isCardanoTestnetAddress(toAddress) !== destChainInfo.isTestnet) { + return `Recipient address must be a valid ${destChainInfo.name} address`; + } + + return ''; +} + export function _isNotDuplicateAddress (validateRecipientParams: ValidateRecipientParams): string { const { fromAddress, toAddress } = validateRecipientParams; diff --git a/packages/extension-base/src/defaults.ts b/packages/extension-base/src/defaults.ts index 6af6bb847f8..14ca5580336 100644 --- a/packages/extension-base/src/defaults.ts +++ b/packages/extension-base/src/defaults.ts @@ -7,7 +7,8 @@ const ALLOWED_PATH = [ '/accounts/connect-ledger', '/accounts/restore-json', '/accounts/detail', - '/accounts/new-seed-phrase' + '/accounts/new-seed-phrase', + '/migrate-account' ] as const; const PHISHING_PAGE_REDIRECT = '/phishing-page-detected'; const EXTENSION_PREFIX = process.env.EXTENSION_PREFIX as string || ''; diff --git a/packages/extension-base/src/koni/api/dotsama/crowdloan.ts b/packages/extension-base/src/koni/api/dotsama/crowdloan.ts index f41a0bbe3bd..de7960e4db9 100644 --- a/packages/extension-base/src/koni/api/dotsama/crowdloan.ts +++ b/packages/extension-base/src/koni/api/dotsama/crowdloan.ts @@ -3,11 +3,11 @@ import { COMMON_CHAIN_SLUGS } from '@subwallet/chain-list'; import { _ChainInfo, _CrowdloanFund, _FundStatus } from '@subwallet/chain-list/types'; -import { APIItemState, CrowdloanItem, CrowdloanParaState } from '@subwallet/extension-base/background/KoniTypes'; +import { APIItemState, ChainType, CrowdloanItem, CrowdloanParaState } from '@subwallet/extension-base/background/KoniTypes'; import { ACALA_REFRESH_CROWDLOAN_INTERVAL } from '@subwallet/extension-base/constants'; import registry from '@subwallet/extension-base/koni/api/dotsama/typeRegistry'; import { _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; -import { categoryAddresses, fetchJson, reformatAddress } from '@subwallet/extension-base/utils'; +import { fetchJson, getAddressesByChainType, reformatAddress } from '@subwallet/extension-base/utils'; import { fetchStaticData } from '@subwallet/extension-base/utils/fetchStaticData'; import { DeriveOwnContributions } from '@polkadot/api-derive/types'; @@ -178,7 +178,7 @@ export async function subscribeCrowdloan (addresses: string[], substrateApiMap: const now = Date.now(); const polkadotAPI = await substrateApiMap[COMMON_CHAIN_SLUGS.POLKADOT].isReady; const kusamaAPI = await substrateApiMap[COMMON_CHAIN_SLUGS.KUSAMA].isReady; - const substrateAddresses = categoryAddresses(addresses).substrate; + const substrateAddresses = getAddressesByChainType(addresses, [ChainType.SUBSTRATE]); const hexAddresses = substrateAddresses.map((address) => { return registry.createType('AccountId', address).toHex(); }); diff --git a/packages/extension-base/src/koni/api/nft/index.ts b/packages/extension-base/src/koni/api/nft/index.ts index e6e03a0f1af..f6f1016303d 100644 --- a/packages/extension-base/src/koni/api/nft/index.ts +++ b/packages/extension-base/src/koni/api/nft/index.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; -import { NftCollection, NftItem } from '@subwallet/extension-base/background/KoniTypes'; +import { ChainType, NftCollection, NftItem } from '@subwallet/extension-base/background/KoniTypes'; import { AcalaNftApi } from '@subwallet/extension-base/koni/api/nft/acala_nft'; import AssetHubUniquesPalletApi from '@subwallet/extension-base/koni/api/nft/assethub_unique'; import { BitCountryNftApi } from '@subwallet/extension-base/koni/api/nft/bit.country'; @@ -13,13 +13,12 @@ import { BaseNftApi } from '@subwallet/extension-base/koni/api/nft/nft'; import OrdinalNftApi from '@subwallet/extension-base/koni/api/nft/ordinal_nft'; import { RmrkNftApi } from '@subwallet/extension-base/koni/api/nft/rmrk_nft'; import { UniqueNftApi } from '@subwallet/extension-base/koni/api/nft/unique_network_nft'; -// import UniqueNftApi from '@subwallet/extension-base/koni/api/nft/unique_nft'; import { VaraNftApi } from '@subwallet/extension-base/koni/api/nft/vara_nft'; import { WasmNftApi } from '@subwallet/extension-base/koni/api/nft/wasm_nft'; import { _NFT_CHAIN_GROUP } from '@subwallet/extension-base/services/chain-service/constants'; import { _EvmApi, _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; import { _isChainSupportEvmNft, _isChainSupportNativeNft, _isChainSupportWasmNft, _isSupportOrdinal } from '@subwallet/extension-base/services/chain-service/utils'; -import { categoryAddresses, targetIsWeb } from '@subwallet/extension-base/utils'; +import { getAddressesByChainType, targetIsWeb } from '@subwallet/extension-base/utils'; import AssetHubNftsPalletApi from './assethub_nft'; import { RariNftApi } from './rari'; @@ -27,7 +26,8 @@ import { OdysseyNftApi } from './story_odyssey_nft'; import { TernoaNftApi } from './ternoa_nft'; function createSubstrateNftApi (chain: string, substrateApi: _SubstrateApi | null, addresses: string[]): BaseNftApi[] | null { - const { evm: evmAddresses, substrate: substrateAddresses } = categoryAddresses(addresses); + const evmAddresses = getAddressesByChainType(addresses, [ChainType.EVM]); + const substrateAddresses = getAddressesByChainType(addresses, [ChainType.SUBSTRATE]); if (_NFT_CHAIN_GROUP.acala.includes(chain)) { return [new AcalaNftApi(substrateApi, substrateAddresses, chain)]; @@ -61,13 +61,13 @@ function createSubstrateNftApi (chain: string, substrateApi: _SubstrateApi | nul } function createWasmNftApi (chain: string, apiProps: _SubstrateApi | null, addresses: string[]): BaseNftApi | null { - const substrateAddresses = categoryAddresses(addresses).substrate; + const substrateAddresses = getAddressesByChainType(addresses, [ChainType.SUBSTRATE]); return new WasmNftApi(apiProps, substrateAddresses, chain); } function createWeb3NftApi (chain: string, evmApi: _EvmApi | null, addresses: string[]): BaseNftApi | null { - const evmAddresses = categoryAddresses(addresses).evm; + const evmAddresses = getAddressesByChainType(addresses, [ChainType.EVM]); return new EvmNftApi(evmApi, evmAddresses, chain); } @@ -109,7 +109,8 @@ export class NftHandler { setAddresses (addresses: string[]) { this.addresses = addresses; - const { evm: evmAddresses, substrate: substrateAddresses } = categoryAddresses(addresses); + const evmAddresses = getAddressesByChainType(addresses, [ChainType.EVM]); + const substrateAddresses = getAddressesByChainType(addresses, [ChainType.SUBSTRATE]); for (const handler of this.handlers) { const useAddresses = handler.isEthereum ? evmAddresses : substrateAddresses; @@ -140,7 +141,8 @@ export class NftHandler { try { if (this.needSetupApi) { // setup connections for first time use this.handlers = []; - const { evm: evmAddresses, substrate: substrateAddresses } = categoryAddresses(this.addresses); + const evmAddresses = getAddressesByChainType(this.addresses, [ChainType.EVM]); + const substrateAddresses = getAddressesByChainType(this.addresses, [ChainType.SUBSTRATE]); Object.entries(this.chainInfoMap).forEach(([chain, chainInfo]) => { if (_isChainSupportNativeNft(chainInfo)) { diff --git a/packages/extension-base/src/koni/api/staking/index.ts b/packages/extension-base/src/koni/api/staking/index.ts index 4055d1f65d2..b0cf0771f21 100644 --- a/packages/extension-base/src/koni/api/staking/index.ts +++ b/packages/extension-base/src/koni/api/staking/index.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _ChainInfo } from '@subwallet/chain-list/types'; -import { NominatorMetadata, StakingItem, StakingRewardItem } from '@subwallet/extension-base/background/KoniTypes'; +import { ChainType, NominatorMetadata, StakingItem, StakingRewardItem } from '@subwallet/extension-base/background/KoniTypes'; import { getAmplitudeStakingOnChain, getAstarStakingOnChain, getParaStakingOnChain } from '@subwallet/extension-base/koni/api/staking/paraChain'; import { getNominationPoolReward, getRelayPoolingOnChain, getRelayStakingOnChain } from '@subwallet/extension-base/koni/api/staking/relayChain'; import { getAllSubsquidStaking } from '@subwallet/extension-base/koni/api/staking/subsquidStaking'; @@ -10,7 +10,7 @@ import { _PURE_EVM_CHAINS } from '@subwallet/extension-base/services/chain-servi import { _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; import { _isChainEvmCompatible, _isChainSupportSubstrateStaking, _isSubstrateRelayChain } from '@subwallet/extension-base/services/chain-service/utils'; import { _STAKING_CHAIN_GROUP } from '@subwallet/extension-base/services/earning-service/constants'; -import { categoryAddresses } from '@subwallet/extension-base/utils'; +import { getAddressesByChainType } from '@subwallet/extension-base/utils'; interface PromiseMapping { api: _SubstrateApi, @@ -19,7 +19,8 @@ interface PromiseMapping { export function stakingOnChainApi (addresses: string[], substrateApiMap: Record<string, _SubstrateApi>, chainInfoMap: Record<string, _ChainInfo>, stakingCallback: (networkKey: string, rs: StakingItem) => void, nominatorStateCallback: (nominatorMetadata: NominatorMetadata) => void) { const filteredApiMap: PromiseMapping[] = []; - const { evm: evmAddresses, substrate: substrateAddresses } = categoryAddresses(addresses); + const evmAddresses = getAddressesByChainType(addresses, [ChainType.EVM]); + const substrateAddresses = getAddressesByChainType(addresses, [ChainType.SUBSTRATE]); Object.entries(chainInfoMap).forEach(([networkKey, chainInfo]) => { if (_PURE_EVM_CHAINS.indexOf(networkKey) < 0 && _isChainSupportSubstrateStaking(chainInfo)) { diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index ad8c22d3987..d975803eba8 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -8,7 +8,7 @@ import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _MultiChainAsset } from import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { createSubscription } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, MetadataItem, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, SufficientMetadata, ThemeNames, TransactionHistoryItem, TransactionResponse, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, MetadataItem, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestMigrateSoloAccount, RequestMigrateUnifiedAndFetchEligibleSoloAccounts, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestPingSession, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveMigrationAcknowledgedStatus, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSaveUnifiedAccountMigrationInProgress, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseMigrateSoloAccount, ResponseMigrateUnifiedAndFetchEligibleSoloAccounts, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, SufficientMetadata, ThemeNames, TransactionHistoryItem, TransactionResponse, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountExport, RequestAuthorizeCancel, RequestAuthorizeReject, RequestCurrentAccountAddress, RequestMetadataApprove, RequestMetadataReject, RequestSigningApproveSignature, RequestSigningCancel, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseType, SigningRequest, WindowOpenParams } from '@subwallet/extension-base/background/types'; import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning'; import { ALL_ACCOUNT_KEY, LATEST_SESSION, XCM_FEE_RATIO } from '@subwallet/extension-base/constants'; @@ -28,9 +28,11 @@ import { getPoolingBondingExtrinsic, getPoolingUnbondingExtrinsic, validatePoolB import { YIELD_EXTRINSIC_TYPES } from '@subwallet/extension-base/koni/api/yield/helper/utils'; import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; import { RequestOptimalTransferProcess } from '@subwallet/extension-base/services/balance-service/helpers/process'; +import { DEFAULT_CARDANO_TTL_OFFSET } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/consts'; import { isBounceableAddress } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/utils'; +import { createCardanoTransaction } from '@subwallet/extension-base/services/balance-service/transfer/cardano-transfer'; import { getERC20TransactionObject, getERC721Transaction, getEVMTransactionObject, getPSP34TransferExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/smart-contract'; -import { createTransferExtrinsic, getTransferMockTxFee } from '@subwallet/extension-base/services/balance-service/transfer/token'; +import { createSubstrateExtrinsic, getTransferMockTxFee } from '@subwallet/extension-base/services/balance-service/transfer/token'; import { createTonTransaction } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; import { createAvailBridgeExtrinsicFromAvail, createAvailBridgeTxFromEth, createPolygonBridgeExtrinsic, createSnowBridgeExtrinsic, createXcmExtrinsic, CreateXcmExtrinsicProps, FunctionCreateXcmExtrinsic, getXcmMockTxFee } from '@subwallet/extension-base/services/balance-service/transfer/xcm'; import { getClaimTxOnAvail, getClaimTxOnEthereum, isAvailChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/availBridge'; @@ -38,7 +40,7 @@ import { _isPolygonChainBridge, getClaimPolygonBridge, isClaimedPolygonBridge } import { _isPosChainBridge, getClaimPosBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/posBridge'; import { _API_OPTIONS_CHAIN_GROUP, _DEFAULT_MANTA_ZK_CHAIN, _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX, SUFFICIENT_CHAIN } from '@subwallet/extension-base/services/chain-service/constants'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _NetworkUpsertParams, _SubstrateAdapterQueryArgs, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse, EnableChainParams, EnableMultiChainParams } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getEvmChainId, _getTokenMinAmount, _getTokenOnChainAssetId, _getXcmAssetMultilocation, _isAssetSmartContractNft, _isBridgedToken, _isChainEvmCompatible, _isChainSubstrateCompatible, _isChainTonCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getEvmChainId, _getTokenMinAmount, _getTokenOnChainAssetId, _getXcmAssetMultilocation, _isAssetSmartContractNft, _isBridgedToken, _isChainEvmCompatible, _isChainSubstrateCompatible, _isChainTonCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByCardano, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; import { ClaimPolygonBridgeNotificationMetadata, NotificationSetup } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; import { AppBannerData, AppConfirmationData, AppPopupData } from '@subwallet/extension-base/services/mkt-campaign-service/types'; import { EXTENSION_REQUEST_URL } from '@subwallet/extension-base/services/request-service/constants'; @@ -49,16 +51,15 @@ import { isProposalExpired, isSupportWalletConnectChain, isSupportWalletConnectN import { ResultApproveWalletConnectSession, WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; import { SWStorage } from '@subwallet/extension-base/storage'; import { AccountsStore } from '@subwallet/extension-base/stores'; -import { AccountJson, AccountProxyMap, AccountsWithCurrentAddress, BalanceJson, BasicTxErrorType, BasicTxWarningCode, BuyServiceInfo, BuyTokenInfo, EarningRewardJson, NominationPoolInfo, OptimalYieldPathParams, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeTonWalletContractVersion, RequestCheckPublicAndSecretKey, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, StakingTxErrorType, StorageDataInterface, TokenSpendingApprovalParams, ValidateYieldProcessParams, YieldPoolType } from '@subwallet/extension-base/types'; +import { AccountJson, AccountProxyMap, AccountsWithCurrentAddress, BalanceJson, BasicTxErrorType, BasicTxWarningCode, BuyServiceInfo, BuyTokenInfo, EarningRewardJson, NominationPoolInfo, OptimalYieldPathParams, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeTonWalletContractVersion, RequestCheckPublicAndSecretKey, RequestClaimBridge, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, StakingTxErrorType, StorageDataInterface, TokenSpendingApprovalParams, ValidateYieldProcessParams, YieldPoolType } from '@subwallet/extension-base/types'; import { RequestAccountProxyEdit, RequestAccountProxyForget } from '@subwallet/extension-base/types/account/action/edit'; -import { RequestClaimBridge } from '@subwallet/extension-base/types/bridge'; import { GetNotificationParams, RequestIsClaimedPolygonBridge, RequestSwitchStatusParams } from '@subwallet/extension-base/types/notification'; import { CommonOptimalPath } from '@subwallet/extension-base/types/service-base'; import { SwapPair, SwapQuoteResponse, SwapRequest, SwapRequestResult, SwapSubmitParams, ValidateSwapProcessParams } from '@subwallet/extension-base/types/swap'; import { _analyzeAddress, BN_ZERO, combineAllAccountProxy, createTransactionFromRLP, isSameAddress, MODULE_SUPPORT, reformatAddress, signatureToHex, toBNString, Transaction as QrTransaction, transformAccounts, transformAddresses, uniqueStringArray } from '@subwallet/extension-base/utils'; import { parseContractInput, parseEvmRlp } from '@subwallet/extension-base/utils/eth/parseTransaction'; import { MetadataDef } from '@subwallet/extension-inject/types'; -import { getKeypairTypeByAddress, isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; +import { getKeypairTypeByAddress, isAddress, isCardanoAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; import { EthereumKeypairTypes, SubstrateKeypairTypes, TonKeypairTypes } from '@subwallet/keyring/types'; import { keyring } from '@subwallet/ui-keyring'; import { SubjectInfo } from '@subwallet/ui-keyring/observable/types'; @@ -911,6 +912,24 @@ export default class KoniExtension { return true; } + private saveMigrationAcknowledgedStatus ({ isAcknowledgedUnifiedAccountMigration }: RequestSaveMigrationAcknowledgedStatus) { + this.#koniState.updateSetting('isAcknowledgedUnifiedAccountMigration', isAcknowledgedUnifiedAccountMigration); + + return true; + } + + private saveUnifiedAccountMigrationInProgress ({ isUnifiedAccountMigrationInProgress }: RequestSaveUnifiedAccountMigrationInProgress) { + this.#koniState.updateSetting('isUnifiedAccountMigrationInProgress', isUnifiedAccountMigrationInProgress); + + return true; + } + + private pingUnifiedAccountMigrationDone (): boolean { + this.#koniState.updateSetting('isUnifiedAccountMigrationInProgress', false); + + return true; + } + private setShowZeroBalance ({ show }: RequestChangeShowZeroBalance) { this.#koniState.updateSetting('isShowZeroBalance', show); @@ -1362,10 +1381,25 @@ export default class KoniExtension { transferAll: !!transferAll, // currently not used tonApi }); + } else if (isCardanoAddress(from) && isCardanoAddress(to) && _isTokenTransferredByCardano(transferTokenInfo)) { + chainType = ChainType.CARDANO; + const cardanoApi = this.#koniState.getCardanoApi(networkKey); + + [transaction, transferAmount.value] = await createCardanoTransaction({ + tokenInfo: transferTokenInfo, + from, + to, + networkKey, + value: value || '0', + cardanoTtlOffset: DEFAULT_CARDANO_TTL_OFFSET, + transferAll: !!transferAll, + cardanoApi, + nativeTokenInfo + }); } else { const substrateApi = this.#koniState.getSubstrateApi(networkKey); - [transaction, transferAmount.value] = await createTransferExtrinsic({ + [transaction, transferAmount.value] = await createSubstrateExtrinsic({ transferAll: !!transferAll, value: value || '0', from: from, @@ -1972,6 +2006,20 @@ export default class KoniExtension { return this.#koniState.getConfirmationsQueueSubjectTon().getValue(); } + private subscribeConfirmationsCardano (id: string, port: chrome.runtime.Port) { + const cb = createSubscription<'pri(confirmationsCardano.subscribe)'>(id, port); + + const subscription = this.#koniState.getConfirmationsQueueSubjectCardano().subscribe(cb); + + this.createUnsubscriptionHandle(id, subscription.unsubscribe); + + port.onDisconnect.addListener((): void => { + this.cancelSubscription(id); + }); + + return this.#koniState.getConfirmationsQueueSubjectCardano().getValue(); + } + private async completeConfirmation (request: RequestConfirmationComplete) { return await this.#koniState.completeConfirmation(request); } @@ -1980,6 +2028,10 @@ export default class KoniExtension { return await this.#koniState.completeConfirmationTon(request); } + private async completeConfirmationCardano (request: RequestConfirmationCompleteCardano) { + return await this.#koniState.completeConfirmationCardano(request); + } + /// Sign Qr private getNetworkJsonByChainId (chainId?: number): _ChainInfo | null { @@ -3898,6 +3950,36 @@ export default class KoniExtension { /* Ledger */ + /* Migrate Unified Account */ + private async migrateUnifiedAndFetchEligibleSoloAccounts (request: RequestMigrateUnifiedAndFetchEligibleSoloAccounts): Promise<ResponseMigrateUnifiedAndFetchEligibleSoloAccounts> { + const setMigratingMode = () => { + this.saveUnifiedAccountMigrationInProgress({ isUnifiedAccountMigrationInProgress: true }); + }; + + return await this.#koniState.keyringService.context.migrateUnifiedAndFetchEligibleSoloAccounts(request, setMigratingMode); + } + + private async migrateSoloAccount (request: RequestMigrateSoloAccount): Promise<ResponseMigrateSoloAccount> { + const proxyIds = request.soloAccounts.map((account) => account.proxyId); + + const response = this.#koniState.keyringService.context.migrateSoloAccount(request); + const newProxyId = response.migratedUnifiedAccountId; // get from response to ensure account migration is done. + const newName = request.accountName; + + try { + this.#koniState.inappNotificationService.migrateNotificationProxyId(proxyIds, newProxyId, newName); + } catch (error) { + console.error('Error on migrating notification for unified account migration', error); + } + + return response; + } + + private pingSession (request: RequestPingSession): boolean { + return this.#koniState.keyringService.context.pingSession(request); + } + /* Migrate Unified Account */ + // -------------------------------------------------------------- // eslint-disable-next-line @typescript-eslint/require-await public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise<ResponseType<TMessageType>> { @@ -3994,6 +4076,12 @@ export default class KoniExtension { return this.setEnableChainPatrol(request as RequestChangeEnableChainPatrol); case 'pri(settings.saveNotificationSetup)': return this.saveNotificationSetup(request as NotificationSetup); + case 'pri(settings.saveMigrationAcknowledgedStatus)': + return this.saveMigrationAcknowledgedStatus(request as RequestSaveMigrationAcknowledgedStatus); + case 'pri(settings.saveUnifiedAccountMigrationInProgress)': + return this.saveUnifiedAccountMigrationInProgress(request as RequestSaveUnifiedAccountMigrationInProgress); + case 'pri(settings.pingUnifiedAccountMigrationDone)': + return this.pingUnifiedAccountMigrationDone(); case 'pri(settings.saveShowZeroBalance)': return this.setShowZeroBalance(request as RequestChangeShowZeroBalance); case 'pri(settings.saveLanguage)': @@ -4271,10 +4359,14 @@ export default class KoniExtension { return this.subscribeConfirmations(id, port); case 'pri(confirmationsTon.subscribe)': return this.subscribeConfirmationsTon(id, port); + case 'pri(confirmationsCardano.subscribe)': + return this.subscribeConfirmationsCardano(id, port); case 'pri(confirmations.complete)': return await this.completeConfirmation(request as RequestConfirmationComplete); case 'pri(confirmationsTon.complete)': return await this.completeConfirmationTon(request as RequestConfirmationCompleteTon); + case 'pri(confirmationsCardano.complete)': + return await this.completeConfirmationCardano(request as RequestConfirmationCompleteCardano); /// Stake case 'pri(bonding.getBondingOptions)': @@ -4508,6 +4600,14 @@ export default class KoniExtension { case 'pri(ledger.generic.allow)': return this.subscribeLedgerGenericAllowChains(id, port); /* Ledger */ + + /* Migrate Unified Account */ + case 'pri(migrate.migrateUnifiedAndFetchEligibleSoloAccounts)': + return await this.migrateUnifiedAndFetchEligibleSoloAccounts(request as RequestMigrateUnifiedAndFetchEligibleSoloAccounts); + case 'pri(migrate.migrateSoloAccount)': + return await this.migrateSoloAccount(request as RequestMigrateSoloAccount); + case 'pri(migrate.pingSession)': + return this.pingSession(request as RequestPingSession); // Default default: throw new Error(`Unable to handle message of type ${type}`); diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index 98a78580cd4..36885ac4d3d 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -6,7 +6,7 @@ import { EvmProviderError } from '@subwallet/extension-base/background/errors/Ev import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { isSubscriptionRunning, unsubscribe } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, ChainStakingMetadata, ChainType, ConfirmationsQueue, ConfirmationsQueueTon, CrowdloanItem, CrowdloanJson, CurrencyType, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestConfirmationComplete, RequestConfirmationCompleteTon, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ServiceInfo, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; +import { AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, ChainStakingMetadata, ChainType, ConfirmationsQueue, ConfirmationsQueueCardano, ConfirmationsQueueTon, CrowdloanItem, CrowdloanJson, CurrencyType, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestConfirmationComplete, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ServiceInfo, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; import { RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning } from '@subwallet/extension-base/background/types'; import { EnvConfig, MANTA_PAY_BALANCE_INTERVAL, REMIND_EXPORT_ACCOUNT } from '@subwallet/extension-base/constants'; import { convertErrorFormat, generateValidationProcess, PayloadValidated, ValidateStepFunction, validationAuthMiddleware, validationAuthWCMiddleware, validationConnectMiddleware, validationEvmDataTransactionMiddleware, validationEvmSignMessageMiddleware } from '@subwallet/extension-base/core/logic-validation'; @@ -46,6 +46,7 @@ import { BalanceItem, BasicTxErrorType, CurrentAccountInfo, EvmFeeInfo, RequestC import { isManifestV3, stripUrl, targetIsWeb } from '@subwallet/extension-base/utils'; import { createPromiseHandler } from '@subwallet/extension-base/utils/promise'; import { MetadataDef, ProviderMeta } from '@subwallet/extension-inject/types'; +import subwalletApiSdk from '@subwallet/subwallet-api-sdk'; import { keyring } from '@subwallet/ui-keyring'; import BN from 'bn.js'; import { t } from 'i18next'; @@ -141,6 +142,9 @@ export default class KoniState { private waitStarting: Promise<void> | null = null; constructor (providers: Providers = {}) { + // Init subwallet api sdk + subwalletApiSdk.init(process.env.SUBWALLET_API || ''); + this.providers = providers; this.eventService = new EventService(); @@ -925,6 +929,14 @@ export default class KoniState { return this.chainService.getTonApi(networkKey); } + public getCardanoApiMap () { + return this.chainService.getCardanoApiMap(); + } + + public getCardanoApi (networkKey: string) { + return this.chainService.getCardanoApi(networkKey); + } + public getApiMap () { return { substrate: this.chainService.getSubstrateApiMap(), @@ -1264,6 +1276,10 @@ export default class KoniState { return this.requestService.confirmationsQueueSubjectTon; } + public getConfirmationsQueueSubjectCardano (): BehaviorSubject<ConfirmationsQueueCardano> { + return this.requestService.confirmationsQueueSubjectCardano; + } + public async completeConfirmation (request: RequestConfirmationComplete) { return await this.requestService.completeConfirmation(request); } @@ -1272,6 +1288,10 @@ export default class KoniState { return await this.requestService.completeConfirmationTon(request); } + public async completeConfirmationCardano (request: RequestConfirmationCompleteCardano) { + return await this.requestService.completeConfirmationCardano(request); + } + private async onMV3Update () { const migrationStatus = await SWStorage.instance.getItem('mv3_migration'); diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/consts.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/consts.ts new file mode 100644 index 00000000000..70656418409 --- /dev/null +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/consts.ts @@ -0,0 +1,4 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export const DEFAULT_CARDANO_TTL_OFFSET = 2 * 60 * 60 * 1000; // 2 hours diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/index.ts new file mode 100644 index 00000000000..d159c0ea2a7 --- /dev/null +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/index.ts @@ -0,0 +1,65 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { _AssetType } from '@subwallet/chain-list/types'; +import { APIItemState } from '@subwallet/extension-base/background/KoniTypes'; +import { ASTAR_REFRESH_BALANCE_INTERVAL } from '@subwallet/extension-base/constants'; +import { CardanoBalanceItem } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/types'; +import { getCardanoAssetId } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/utils'; +import { _CardanoApi } from '@subwallet/extension-base/services/chain-service/types'; +import { BalanceItem, SusbcribeCardanoPalletBalance } from '@subwallet/extension-base/types'; +import { filterAssetsByChainAndType, reformatAddress } from '@subwallet/extension-base/utils'; + +async function getBalanceMap (addresses: string[], cardanoApi: _CardanoApi, isTestnet: boolean): Promise<Record<string, CardanoBalanceItem[]>> { + const addressBalanceMap: Record<string, CardanoBalanceItem[]> = {}; + + for (const address of addresses) { + addressBalanceMap[address] = await cardanoApi.getBalanceMap(isTestnet ? reformatAddress(address, 0) : address); + } + + return addressBalanceMap; +} + +export function subscribeCardanoBalance (params: SusbcribeCardanoPalletBalance) { + const { addresses, assetMap, callback, cardanoApi, chainInfo } = params; + const chain = chainInfo.slug; + const isTestnet = chainInfo.isTestnet; + const tokens = filterAssetsByChainAndType(assetMap, chain, [_AssetType.NATIVE, _AssetType.CIP26]); + + function getBalance () { + getBalanceMap(addresses, cardanoApi, isTestnet) + .then((addressBalanceMap) => { + Object.values(tokens).forEach((tokenInfo) => { + const id = getCardanoAssetId(tokenInfo); + const balances = addresses.map((address) => { + if (!addressBalanceMap[address]) { + return '0'; + } + + return addressBalanceMap[address].find((asset) => asset.unit === id)?.quantity || '0'; + }); + + const items: BalanceItem[] = balances.map((balance, index): BalanceItem => { + return { + address: addresses[index], + tokenSlug: tokenInfo.slug, + free: balance, + locked: '0', // todo: research cardano lock balance + state: APIItemState.READY + }; + }); + + callback(items); + }); + }) + .catch((e) => console.error('Error while fetching cardano balance', e)); + } + + const interval = setInterval(getBalance, ASTAR_REFRESH_BALANCE_INTERVAL); + + getBalance(); + + return () => { + clearInterval(interval); + }; +} diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/types.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/types.ts new file mode 100644 index 00000000000..14723354849 --- /dev/null +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/types.ts @@ -0,0 +1,40 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export interface CardanoAddressBalance { + address: string; + amount: CardanoBalanceItem[], + stake_address: string, + type: string, // todo: consider create interface for type + script: boolean +} + +export interface CardanoBalanceItem { + unit: string, + quantity: string +} + +export interface CardanoTxJson { + body: { + inputs: CardanoTxInput[], + outputs: CardanoTxOutput[], + fee: string, + ttl: string + } + witness_set: any, + is_valid: any, + auxiliary_data: any +} + +export interface CardanoTxOutput { + address: string, + amount: { + coin: string, + multiasset: Record<string, Record<string, string>>; + } +} + +interface CardanoTxInput { + transaction_id: string, + index: number +} diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/utils.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/utils.ts new file mode 100644 index 00000000000..1b6c68ff016 --- /dev/null +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/cardano/utils.ts @@ -0,0 +1,78 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { Transaction } from '@emurgo/cardano-serialization-lib-nodejs'; +import { _ChainAsset } from '@subwallet/chain-list/types'; +import { CardanoTxOutput } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/types'; + +export function getCardanoAssetId (chainAsset: _ChainAsset): string { + return chainAsset.metadata?.cardanoId as string; +} + +export function getCardanoTxFee (payload: string) { + return BigInt(Transaction.from_hex(payload).body().fee().to_str()); +} + +export function getAdaBelongUtxo (payload: string, receiverAddress: string) { + const txOutputsRaw = Transaction.from_hex(payload).body().outputs().to_json(); + const txOutputs = JSON.parse(txOutputsRaw) as CardanoTxOutput[]; + const receiverUtxo = txOutputs.find((utxo) => utxo.address === receiverAddress); // must has utxo to receiver + + // @ts-ignore + return BigInt(receiverUtxo.amount.coin); +} + +export const cborToBytes = (hex: string): Uint8Array => { + if (hex.length % 2 === 0 && /^[0-9A-F]*$/i.test(hex)) { + return Buffer.from(hex, 'hex'); + } + + return Buffer.from(hex, 'utf-8'); +}; + +export async function retryCardanoTxStatus (fn: () => Promise<boolean>, options: { retries: number, delay: number }): Promise<boolean> { + let lastError: Error | undefined; + + for (let i = 0; i < options.retries; i++) { + try { + return await fn(); + } catch (e) { + if (e instanceof Error) { + lastError = e; + } + + // todo: improve the timeout tx + await new Promise((resolve) => setTimeout(resolve, options.delay)); // wait for delay period, then recall the fn() + } + } + + console.error('Cardano transaction timeout', lastError); // throw only last error, in case no successful result from fn() + + return false; +} + +export interface CardanoAssetMetadata { + cardanoId: string; + policyId: string; + nameHex: string; +} + +export function splitCardanoId (id: string): CardanoAssetMetadata { + if (id === 'lovelace') { + return { + cardanoId: id, + policyId: '', + nameHex: '' + }; + } + + if (!id || id.length < 56) { + throw new Error('The cardano native asset policy id must has 28 bytes in length.'); + } else { + return { + cardanoId: id, + policyId: id.slice(0, 56), + nameHex: id.slice(56) + }; + } +} diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index b9c96ec8810..71eb63b1953 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -3,10 +3,11 @@ import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; import { APIItemState, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; -import { _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureEvmChain, _isPureTonChain } from '@subwallet/extension-base/services/chain-service/utils'; +import { subscribeCardanoBalance } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano'; +import { _CardanoApi, _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; +import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountJson, BalanceItem } from '@subwallet/extension-base/types'; -import { categoryAddresses, filterAssetsByChainAndType, pairToAccount } from '@subwallet/extension-base/utils'; +import { filterAssetsByChainAndType, getAddressesByChainTypeMap, pairToAccount } from '@subwallet/extension-base/utils'; import keyring from '@subwallet/ui-keyring'; import { subscribeTonBalance } from './ton/ton'; @@ -40,14 +41,16 @@ export const getAccountJsonByAddress = (address: string): AccountJson | null => /** Filter addresses to subscribe by chain info */ const filterAddress = (addresses: string[], chainInfo: _ChainInfo): [string[], string[]] => { - const { bitcoin, evm, substrate, ton } = categoryAddresses(addresses); + const { bitcoin, cardano, evm, substrate, ton } = getAddressesByChainTypeMap(addresses); if (_isChainEvmCompatible(chainInfo)) { - return [evm, [...bitcoin, ...substrate, ...ton]]; + return [evm, [...bitcoin, ...substrate, ...ton, ...cardano]]; } else if (_isChainBitcoinCompatible(chainInfo)) { - return [bitcoin, [...evm, ...substrate, ...ton]]; + return [bitcoin, [...evm, ...substrate, ...ton, ...cardano]]; } else if (_isChainTonCompatible(chainInfo)) { - return [ton, [...bitcoin, ...evm, ...substrate]]; + return [ton, [...bitcoin, ...evm, ...substrate, ...cardano]]; + } else if (_isChainCardanoCompatible(chainInfo)) { + return [cardano, [...bitcoin, ...evm, ...substrate, ...ton]]; } else { const fetchList: string[] = []; const unfetchList: string[] = []; @@ -77,7 +80,7 @@ const filterAddress = (addresses: string[], chainInfo: _ChainInfo): [string[], s } }); - return [fetchList, [...unfetchList, ...bitcoin, ...evm, ...ton]]; + return [fetchList, [...unfetchList, ...bitcoin, ...evm, ...ton, ...cardano]]; } }; @@ -94,7 +97,9 @@ const handleUnsupportedOrPendingAddresses = ( _AssetType.PSP22, _AssetType.LOCAL, _AssetType.GRC20, - _AssetType.VFT + _AssetType.VFT, + _AssetType.TEP74, + _AssetType.CIP26 ]); const now = new Date().getTime(); @@ -123,6 +128,7 @@ export function subscribeBalance ( substrateApiMap: Record<string, _SubstrateApi>, evmApiMap: Record<string, _EvmApi>, tonApiMap: Record<string, _TonApi>, + cardanoApiMap: Record<string, _CardanoApi>, callback: (rs: BalanceItem[]) => void, extrinsicType?: ExtrinsicType ) { @@ -169,6 +175,18 @@ export function subscribeBalance ( }); } + const cardanoApi = cardanoApiMap[chainSlug]; + + if (_isPureCardanoChain(chainInfo)) { + return subscribeCardanoBalance({ + addresses: useAddresses, + assetMap: chainAssetMap, + callback, + chainInfo, + cardanoApi + }); + } + // If the chain is not ready, return pending state if (!substrateApiMap[chainSlug].isApiReady) { handleUnsupportedOrPendingAddresses( diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/ton/utils.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/ton/utils.ts index 40dd534eaa9..935d1218557 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/ton/utils.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/ton/utils.ts @@ -41,7 +41,7 @@ export function externalMessage (contract: TonWalletContract, seqno: number, bod .endCell(); } -export async function retry<T> (fn: () => Promise<T>, options: { retries: number, delay: number }): Promise<T> { +export async function retryTonTxStatus<T> (fn: () => Promise<T>, options: { retries: number, delay: number }): Promise<T> { let lastError: Error | undefined; for (let i = 0; i < options.retries; i++) { diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 9db8df64e1b..2d3a81f9407 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -220,10 +220,11 @@ export class BalanceService implements StoppableServiceInterface { const evmApiMap = this.state.chainService.getEvmApiMap(); const substrateApiMap = this.state.chainService.getSubstrateApiMap(); const tonApiMap = this.state.chainService.getTonApiMap(); + const cardanoApiMap = this.state.chainService.getCardanoApiMap(); let unsub = noop; - unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, (result) => { + unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, (result) => { const rs = result[0]; let value: string; @@ -398,6 +399,7 @@ export class BalanceService implements StoppableServiceInterface { const evmApiMap = this.state.chainService.getEvmApiMap(); const substrateApiMap = this.state.chainService.getSubstrateApiMap(); const tonApiMap = this.state.chainService.getTonApiMap(); + const cardanoApiMap = this.state.chainService.getCardanoApiMap(); const activeChainSlugs = Object.keys(this.state.getActiveChainInfoMap()); const assetState = this.state.chainService.subscribeAssetSettings().value; @@ -407,7 +409,7 @@ export class BalanceService implements StoppableServiceInterface { }) .map((asset) => asset.slug); - const unsub = subscribeBalance(addresses, activeChainSlugs, assets, assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, (result) => { + const unsub = subscribeBalance(addresses, activeChainSlugs, assets, assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, (result) => { !cancel && this.setBalanceItem(result); }, ExtrinsicType.TRANSFER_BALANCE); diff --git a/packages/extension-base/src/services/balance-service/transfer/cardano-transfer.ts b/packages/extension-base/src/services/balance-service/transfer/cardano-transfer.ts new file mode 100644 index 00000000000..ab7feac3922 --- /dev/null +++ b/packages/extension-base/src/services/balance-service/transfer/cardano-transfer.ts @@ -0,0 +1,159 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import * as csl from '@emurgo/cardano-serialization-lib-nodejs'; +import { _AssetType, _ChainAsset } from '@subwallet/chain-list/types'; +import { CardanoTxJson, CardanoTxOutput } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/types'; +import { CardanoAssetMetadata, getAdaBelongUtxo, getCardanoTxFee, splitCardanoId } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/utils'; +import { _CardanoApi } from '@subwallet/extension-base/services/chain-service/types'; +import { subwalletApiSdk } from '@subwallet/subwallet-api-sdk'; + +export interface CardanoTransactionConfigProps { + tokenInfo: _ChainAsset; + nativeTokenInfo: _ChainAsset; + from: string, + to: string, + networkKey: string, + value: string, + transferAll: boolean, + cardanoTtlOffset: number, + cardanoApi: _CardanoApi +} + +export interface CardanoTransactionConfig { + from: string, + to: string, + networkKey: string, + value: string, + transferAll: boolean, + cardanoTtlOffset: number, + estimateCardanoFee: string, + cardanoPayload: string // hex unsigned tx +} + +export async function createCardanoTransaction (params: CardanoTransactionConfigProps): Promise<[CardanoTransactionConfig | null, string]> { + const { cardanoTtlOffset, from, networkKey, to, tokenInfo, transferAll, value } = params; + + const cardanoId = tokenInfo.metadata?.cardanoId; + const isNativeTransfer = tokenInfo.assetType === _AssetType.NATIVE; + const isSelfTransfer = from === to; + + if (!cardanoId) { + throw new Error('Missing token policy id metadata'); + } + + const payload = await subwalletApiSdk.fetchUnsignedPayload({ + tokenDecimals: params.tokenInfo.decimals || 0, + nativeTokenSymbol: params.nativeTokenInfo.symbol, + cardanoId, + from: params.from, + to: params.to, + value: params.value, + cardanoTtlOffset: params.cardanoTtlOffset + }); + + console.log('Build cardano payload successfully!', payload); + + validatePayload(payload, params); + + const fee = getCardanoTxFee(payload); + const adaBelongToCnaUtxo = isNativeTransfer || isSelfTransfer ? BigInt(0) : getAdaBelongUtxo(payload, to); + + const tx: CardanoTransactionConfig = { + from, + to, + networkKey, + value, + transferAll, + cardanoTtlOffset, + estimateCardanoFee: (fee + adaBelongToCnaUtxo).toString(), + cardanoPayload: payload + }; + + return [tx, value]; +} + +function validatePayload (payload: string, params: CardanoTransactionConfigProps) { + const txInfo = JSON.parse(csl.Transaction.from_hex(payload).to_json()) as CardanoTxJson; + const outputs = txInfo.body.outputs; + const cardanoId = params.tokenInfo.metadata?.cardanoId; + const assetType = params.tokenInfo.assetType; + const isSendSameAddress = params.from === params.to; + + if (!cardanoId) { + throw new Error('Missing cardano id metadata'); + } + + const cardanoAssetMetadata = splitCardanoId(cardanoId); + + if (isSendSameAddress) { + validateAllOutputsBelongToAddress(params.from, outputs); + validateExistOutputWithAmountSend(params.value, outputs, assetType, cardanoAssetMetadata); + } else { + const [outputsBelongToReceiver, outputsNotBelongToReceiver] = [ + outputs.filter((output) => output.address === params.to), + outputs.filter((output) => output.address !== params.to) + ]; + + validateReceiverOutputsWithAmountSend(params.value, outputsBelongToReceiver, assetType, cardanoAssetMetadata); + validateAllOutputsBelongToAddress(params.from, outputsNotBelongToReceiver); + } +} + +function validateAllOutputsBelongToAddress (address: string, outputs: CardanoTxOutput[]) { + const found = outputs.find((output) => output.address !== address); + + if (found) { + throw new Error('Transaction has invalid address information'); + } +} + +function validateExistOutputWithAmountSend (amount: string, outputs: CardanoTxOutput[], assetType: _AssetType, cardanoAssetMetadata: CardanoAssetMetadata) { + if (assetType === _AssetType.NATIVE) { + const found = outputs.find((output) => output.amount.coin === amount); + + if (found) { + return; + } + + throw new Error('Transaction has invalid transfer amount information'); + } + + if (assetType === _AssetType.CIP26) { + const found = outputs.find((output) => amount === output.amount.multiasset[cardanoAssetMetadata.policyId]?.[cardanoAssetMetadata.nameHex]); + + if (found) { + return; + } + + throw new Error('Transaction has invalid transfer amount information'); + } + + throw new Error('Invalid asset type!'); +} + +function validateReceiverOutputsWithAmountSend (amount: string, outputs: CardanoTxOutput[], assetType: _AssetType, cardanoAssetMetadata: CardanoAssetMetadata) { + if (outputs.length !== 1) { + throw new Error('Transaction has invalid transfer amount information'); + } + + const receiverOutput = outputs[0]; + + if (assetType === _AssetType.NATIVE) { + if (receiverOutput.amount.coin === amount) { + return; + } + + throw new Error('Transaction has invalid transfer amount information'); + } + + if (assetType === _AssetType.CIP26) { + if (receiverOutput.amount.multiasset[cardanoAssetMetadata.policyId][cardanoAssetMetadata.nameHex] === amount) { + return; + } + + throw new Error('Transaction has invalid transfer amount information'); + } + + throw new Error('Invalid asset type!'); +} diff --git a/packages/extension-base/src/services/balance-service/transfer/token.ts b/packages/extension-base/src/services/balance-service/transfer/token.ts index 79da9c13c41..9286156669a 100644 --- a/packages/extension-base/src/services/balance-service/transfer/token.ts +++ b/packages/extension-base/src/services/balance-service/transfer/token.ts @@ -31,7 +31,7 @@ interface CreateTransferExtrinsicProps { tokenInfo: _ChainAsset, } -export const createTransferExtrinsic = async ({ from, networkKey, substrateApi, to, tokenInfo, transferAll, value }: CreateTransferExtrinsicProps): Promise<[SubmittableExtrinsic | null, string]> => { +export const createSubstrateExtrinsic = async ({ from, networkKey, substrateApi, to, tokenInfo, transferAll, value }: CreateTransferExtrinsicProps): Promise<[SubmittableExtrinsic | null, string]> => { const api = substrateApi.api; const isDisableTransfer = tokenInfo.metadata?.isDisableTransfer as boolean; @@ -161,7 +161,7 @@ export const getTransferMockTxFee = async (address: string, chainInfo: _ChainInf } else { const substrateApi = api as _SubstrateApi; const chainApi = await substrateApi.isReady; - const [mockTx] = await createTransferExtrinsic({ + const [mockTx] = await createSubstrateExtrinsic({ from: address, networkKey: chainInfo.slug, substrateApi: chainApi, diff --git a/packages/extension-base/src/services/chain-service/handler/CardanoApi.ts b/packages/extension-base/src/services/chain-service/handler/CardanoApi.ts new file mode 100644 index 00000000000..91384840d6c --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/CardanoApi.ts @@ -0,0 +1,213 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { CardanoAddressBalance, CardanoBalanceItem } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/types'; +import { cborToBytes, retryCardanoTxStatus } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/utils'; +import { _ApiOptions } from '@subwallet/extension-base/services/chain-service/handler/types'; +import { _CardanoApi, _ChainConnectionStatus } from '@subwallet/extension-base/services/chain-service/types'; +import { createPromiseHandler, PromiseHandler } from '@subwallet/extension-base/utils'; +import { BehaviorSubject } from 'rxjs'; + +import { hexAddPrefix, isHex } from '@polkadot/util'; + +export const API_KEY = { // todo: move to env. + mainnet: 'mainnet6uE9JH3zGYquaxRKA7IMhEuzRUB58uGK', + testnet: 'preprodcnP5RADcrWMlf2cQe4ZKm4cjRvrBQFXM' +}; + +export class CardanoApi implements _CardanoApi { + chainSlug: string; + // private api: BlockFrostAPI; + apiUrl: string; + apiError?: string; + apiRetry = 0; + public readonly isApiConnectedSubject = new BehaviorSubject(false); + public readonly connectionStatusSubject = new BehaviorSubject(_ChainConnectionStatus.DISCONNECTED); + isApiReady = false; + isApiReadyOnce = false; + isReadyHandler: PromiseHandler<_CardanoApi>; + isTestnet: boolean; // todo: add api with interface BlockFrostAPI to remove isTestnet check + private projectId: string; + + providerName: string; + + constructor (chainSlug: string, apiUrl: string, { isTestnet, providerName }: _ApiOptions) { + this.chainSlug = chainSlug; + this.apiUrl = apiUrl; + this.isTestnet = isTestnet ?? true; + this.projectId = isTestnet ? API_KEY.testnet : API_KEY.mainnet; + this.providerName = providerName || 'unknown'; + // this.api = this.createProvider(isTestnet); + this.isReadyHandler = createPromiseHandler<_CardanoApi>(); + + this.connect(); + } + + get isApiConnected (): boolean { + return this.isApiConnectedSubject.getValue(); + } + + get connectionStatus (): _ChainConnectionStatus { + return this.connectionStatusSubject.getValue(); + } + + private updateConnectionStatus (status: _ChainConnectionStatus): void { + const isConnected = status === _ChainConnectionStatus.CONNECTED; + + if (isConnected !== this.isApiConnectedSubject.value) { + this.isApiConnectedSubject.next(isConnected); + } + + if (status !== this.connectionStatusSubject.value) { + this.connectionStatusSubject.next(status); + } + } + + get isReady (): Promise<_CardanoApi> { + return this.isReadyHandler.promise; + } + + async updateApiUrl (apiUrl: string) { + if (this.apiUrl === apiUrl) { + return; + } + + await this.disconnect(); + + this.apiUrl = apiUrl; + // this.api = this.createProvider(); + } + + async recoverConnect () { + await this.disconnect(); + this.connect(); + + await this.isReadyHandler.promise; + } + + // private createProvider (isTestnet = true): BlockFrostAPI { + // const projectId = isTestnet ? API_KEY.testnet : API_KEY.mainnet; + // + // return new BlockFrostAPI({ + // projectId + // }); + // } + + connect (): void { + this.updateConnectionStatus(_ChainConnectionStatus.CONNECTING); + // There isn't a persistent network connection underlying TonClient. Cant check connection status. + // this.isApiReadyOnce = true; + this.onConnect(); + } + + async disconnect () { + this.onDisconnect(); + this.updateConnectionStatus(_ChainConnectionStatus.DISCONNECTED); + + return Promise.resolve(); + } + + destroy () { + // Todo: implement this in the future + return this.disconnect(); + } + + onConnect (): void { + if (!this.isApiConnected) { + console.log(`Connected to ${this.chainSlug} at ${this.apiUrl}`); + this.isApiReady = true; + + if (this.isApiReadyOnce) { + this.isReadyHandler.resolve(this); + } + } + + this.updateConnectionStatus(_ChainConnectionStatus.CONNECTED); + } + + onDisconnect (): void { + this.updateConnectionStatus(_ChainConnectionStatus.DISCONNECTED); + + if (this.isApiConnected) { + console.warn(`Disconnected from ${this.chainSlug} of ${this.apiUrl}`); + this.isApiReady = false; + this.isReadyHandler = createPromiseHandler<_CardanoApi>(); + } + } + + async getBalanceMap (address: string): Promise<CardanoBalanceItem[]> { + try { + const url = this.isTestnet ? `https://cardano-preprod.blockfrost.io/api/v0/addresses/${address}` : `https://cardano-mainnet.blockfrost.io/api/v0/addresses/${address}`; + const response = await fetch( + url, { + method: 'GET', + headers: { + Project_id: this.projectId + } + } + ); + + const addressBalance = await response.json() as CardanoAddressBalance; + + return addressBalance.amount; + } catch (e) { + console.error('Error on getting account balance', e); + + return []; + } + } + + async sendCardanoTxReturnHash (tx: string): Promise<string> { + try { + const url = this.isTestnet ? 'https://cardano-preprod.blockfrost.io/api/v0/tx/submit' : 'https://cardano-mainnet.blockfrost.io/api/v0/tx/submit'; + const response = await fetch( + url, { + method: 'POST', + headers: { + Project_id: this.projectId, + 'Content-Type': 'application/cbor' + }, + body: cborToBytes(tx) + } + ); + + const hash = (await response.text()).replace(/^"|"$/g, ''); + + if (isHex(hexAddPrefix(hash))) { + return hash; + } else { + console.error('Error on submitting cardano tx'); + + return ''; + } + } catch (e) { + console.error('Error on submitting cardano tx', e); + + return ''; + } + } + + async getStatusByTxHash (txHash: string, ttl: number): Promise<boolean> { + const cronTime = 30000; + + return retryCardanoTxStatus(async () => { + const url = this.isTestnet ? `https://cardano-preprod.blockfrost.io/api/v0/txs/${txHash}` : `https://cardano-mainnet.blockfrost.io/api/v0/txs/${txHash}`; + const response = await fetch( + url, { + method: 'GET', + headers: { + Project_id: this.projectId + } + } + ); + + const txInfo = await response.json() as { hash: string, block: string, index: number }; + + if (txInfo.block && txInfo.hash && txInfo.index >= 0) { + return true; + } + + throw new Error('Transaction not found'); + }, { retries: ttl / cronTime, delay: cronTime }); + } +} diff --git a/packages/extension-base/src/services/chain-service/handler/CardanoChainHandler.ts b/packages/extension-base/src/services/chain-service/handler/CardanoChainHandler.ts new file mode 100644 index 00000000000..4544e9b9c7e --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/CardanoChainHandler.ts @@ -0,0 +1,93 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ChainService } from '@subwallet/extension-base/services/chain-service'; +import { AbstractChainHandler } from '@subwallet/extension-base/services/chain-service/handler/AbstractChainHandler'; +import { CardanoApi } from '@subwallet/extension-base/services/chain-service/handler/CardanoApi'; +import { _ApiOptions } from '@subwallet/extension-base/services/chain-service/handler/types'; + +export class CardanoChainHandler extends AbstractChainHandler { + private cardanoApiMap: Record<string, CardanoApi> = {}; + + // eslint-disable-next-line no-useless-constructor + constructor (parent?: ChainService) { + super(parent); + } + + public getCardanoApiMap () { + return this.cardanoApiMap; + } + + public getCardanoApiByChain (chain: string) { + return this.cardanoApiMap[chain]; + } + + public getApiByChain (chain: string) { + return this.getCardanoApiByChain(chain); + } + + public setCardanoApi (chain: string, cardanoApi: CardanoApi) { + this.cardanoApiMap[chain] = cardanoApi; + } + + public async initApi (chainSlug: string, apiUrl: string, { isTestnet, onUpdateStatus, providerName }: Omit<_ApiOptions, 'metadata'> = {}) { + const existed = this.getCardanoApiByChain(chainSlug); + + if (existed) { + existed.connect(); + + if (apiUrl !== existed.apiUrl) { + existed.updateApiUrl(apiUrl).catch(console.error); + } + + return existed; + } + + const apiObject = new CardanoApi(chainSlug, apiUrl, { isTestnet, providerName }); + + apiObject.connectionStatusSubject.subscribe(this.handleConnection.bind(this, chainSlug)); + apiObject.connectionStatusSubject.subscribe(onUpdateStatus); + + return Promise.resolve(apiObject); + } + + public async recoverApi (chain: string): Promise<void> { + const existed = this.getCardanoApiByChain(chain); + + if (existed && !existed.isApiReadyOnce) { + console.log(`Reconnect ${existed.providerName || existed.chainSlug} at ${existed.apiUrl}`); + + return existed.recoverConnect(); + } + } + + destroyCardanoApi (chain: string) { + const cardanoApi = this.getCardanoApiByChain(chain); + + cardanoApi?.destroy().catch(console.error); + } + + async sleep () { + this.isSleeping = true; + this.cancelAllRecover(); + + await Promise.all(Object.values(this.getCardanoApiMap()).map((cardanoApi) => { + return cardanoApi.disconnect().catch(console.error); + })); + + return Promise.resolve(); + } + + wakeUp () { + this.isSleeping = false; + const activeChains = this.parent?.getActiveChains() || []; + + for (const chain of activeChains) { + const cardanoApi = this.getCardanoApiByChain(chain); + + cardanoApi?.connect(); + } + + return Promise.resolve(); + } +} diff --git a/packages/extension-base/src/services/chain-service/handler/TonApi.ts b/packages/extension-base/src/services/chain-service/handler/TonApi.ts index 3ce683aef15..009a75a0ade 100644 --- a/packages/extension-base/src/services/chain-service/handler/TonApi.ts +++ b/packages/extension-base/src/services/chain-service/handler/TonApi.ts @@ -4,7 +4,7 @@ import { ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { TON_CENTER_API_KEY, TON_OPCODES } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/consts'; import { AccountState, TxByMsgResponse } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/types'; -import { getJettonTxStatus, retry } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/utils'; +import { getJettonTxStatus, retryTonTxStatus } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/utils'; import { _ApiOptions } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _ChainConnectionStatus, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; import { createPromiseHandler, PromiseHandler } from '@subwallet/extension-base/utils'; @@ -70,10 +70,7 @@ export class TonApi implements _TonApi { // Create new provider and api this.apiUrl = apiUrl; - this.api = new TonClient({ - endpoint: this.getJsonRpc(this.apiUrl), - apiKey: TON_CENTER_API_KEY - }); + this.api = this.createProvider(apiUrl); } async recoverConnect () { @@ -204,7 +201,7 @@ export class TonApi implements _TonApi { } async getStatusByExtMsgHash (extMsgHash: string, extrinsicType?: ExtrinsicType): Promise<[boolean, string]> { - return retry<[boolean, string]>(async () => { // retry many times to get transaction status and transaction hex + return retryTonTxStatus<[boolean, string]>(async () => { // retry many times to get transaction status and transaction hex const externalTxInfoRaw = await this.getTxByInMsg(extMsgHash); const externalTxInfo = externalTxInfoRaw.transactions[0]; const isExternalTxCompute = externalTxInfo.description.compute_ph.success; diff --git a/packages/extension-base/src/services/chain-service/handler/types.ts b/packages/extension-base/src/services/chain-service/handler/types.ts index 8f95d2b6224..485d740bc03 100644 --- a/packages/extension-base/src/services/chain-service/handler/types.ts +++ b/packages/extension-base/src/services/chain-service/handler/types.ts @@ -29,6 +29,7 @@ export interface _ApiOptions { metadata?: MetadataItem, onUpdateStatus?: (status: _ChainConnectionStatus) => void, externalApiPromise?: ApiPromise + isTestnet?: boolean } export enum _CHAIN_VALIDATION_ERROR { diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 4b836160aa6..e8db247a8af 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { AssetLogoMap, AssetRefMap, ChainAssetMap, ChainInfoMap, ChainLogoMap, MultiChainAssetMap } from '@subwallet/chain-list'; -import { _AssetRef, _AssetRefPath, _AssetType, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo } from '@subwallet/chain-list/types'; +import { _AssetRef, _AssetRefPath, _AssetType, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo } from '@subwallet/chain-list/types'; import { AssetSetting, MetadataItem, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes'; import { _DEFAULT_ACTIVE_CHAINS, _ZK_ASSET_PREFIX, LATEST_CHAIN_DATA_FETCHING_INTERVAL } from '@subwallet/extension-base/services/chain-service/constants'; +import { CardanoChainHandler } from '@subwallet/extension-base/services/chain-service/handler/CardanoChainHandler'; import { EvmChainHandler } from '@subwallet/extension-base/services/chain-service/handler/EvmChainHandler'; import { MantaPrivateHandler } from '@subwallet/extension-base/services/chain-service/handler/manta/MantaPrivateHandler'; import { SubstrateChainHandler } from '@subwallet/extension-base/services/chain-service/handler/SubstrateChainHandler'; @@ -73,6 +74,7 @@ export class ChainService { private substrateChainHandler: SubstrateChainHandler; private evmChainHandler: EvmChainHandler; private tonChainHandler: TonChainHandler; + private cardanoChainHandler: CardanoChainHandler; private mantaChainHandler: MantaPrivateHandler | undefined; refreshLatestChainDataTimeOut: NodeJS.Timer | undefined; @@ -117,6 +119,7 @@ export class ChainService { this.substrateChainHandler = new SubstrateChainHandler(this); this.evmChainHandler = new EvmChainHandler(this); this.tonChainHandler = new TonChainHandler(this); + this.cardanoChainHandler = new CardanoChainHandler(this); this.logger = createLogger('chain-service'); } @@ -202,6 +205,14 @@ export class ChainService { return this.tonChainHandler.getTonApiMap(); } + public getCardanoApi (slug: string) { + return this.cardanoChainHandler.getCardanoApiByChain(slug); + } + + public getCardanoApiMap () { + return this.cardanoChainHandler.getCardanoApiMap(); + } + public getChainCurrentProviderByKey (slug: string) { const providerName = this.getChainStateByKey(slug).currentProvider; const providerMap = this.getChainInfoByKey(slug).providers; @@ -896,6 +907,14 @@ export class ChainService { this.tonChainHandler.setTonApi(chainInfo.slug, chainApi); } + + if (chainInfo.cardanoInfo !== null && chainInfo.cardanoInfo !== undefined) { + const isTestnet = chainInfo.isTestnet; + + const chainApi = await this.cardanoChainHandler.initApi(chainInfo.slug, endpoint, { isTestnet, providerName, onUpdateStatus }); + + this.cardanoChainHandler.setCardanoApi(chainInfo.slug, chainApi); + } } private destroyApiForChain (chainInfo: _ChainInfo) { @@ -910,6 +929,10 @@ export class ChainService { if (chainInfo.tonInfo !== null) { this.tonChainHandler.destroyTonApi(chainInfo.slug); } + + if (chainInfo.cardanoInfo !== null) { + this.cardanoChainHandler.destroyCardanoApi(chainInfo.slug); + } } public async enableChain (chainSlug: string) { @@ -1223,6 +1246,7 @@ export class ChainService { substrateInfo: storedChainInfo.substrateInfo, bitcoinInfo: storedChainInfo.bitcoinInfo ?? null, tonInfo: storedChainInfo.tonInfo, + cardanoInfo: storedChainInfo.cardanoInfo ?? null, isTestnet: storedChainInfo.isTestnet, chainStatus: storedChainInfo.chainStatus, icon: storedChainInfo.icon, @@ -1251,6 +1275,7 @@ export class ChainService { substrateInfo: storedChainInfo.substrateInfo, bitcoinInfo: storedChainInfo.bitcoinInfo ?? null, tonInfo: storedChainInfo.tonInfo, + cardanoInfo: storedChainInfo.cardanoInfo ?? null, isTestnet: storedChainInfo.isTestnet, chainStatus: storedChainInfo.chainStatus, icon: storedChainInfo.icon, @@ -1455,6 +1480,7 @@ export class ChainService { let substrateInfo: _SubstrateInfo | null = null; let evmInfo: _EvmInfo | null = null; const tonInfo: _TonInfo | null = null; + const cardanoInfo: _CardanoInfo | null = null; if (params.chainSpec.genesisHash !== '') { substrateInfo = { @@ -1494,6 +1520,7 @@ export class ChainService { evmInfo, bitcoinInfo: null, tonInfo, + cardanoInfo, isTestnet: false, chainStatus: _ChainStatus.ACTIVE, icon: '', // Todo: Allow update with custom chain, @@ -1615,6 +1642,7 @@ export class ChainService { // TODO: EVM chain might have WS provider if (provider.startsWith('http')) { // todo: handle validate ton provider + // todo: handle validate cardano provider // HTTP provider is EVM by default api = await this.evmChainHandler.initApi('custom', provider); @@ -1853,15 +1881,12 @@ export class ChainService { this.evmChainHandler.recoverApi(slug).catch(console.error); } - public refreshTonApi (slug: string) { - this.tonChainHandler.recoverApi(slug).catch(console.error); - } - public async stopAllChainApis () { await Promise.all([ this.substrateChainHandler.sleep(), this.evmChainHandler.sleep(), - this.tonChainHandler.sleep() + this.tonChainHandler.sleep(), + this.cardanoChainHandler.sleep() ]); this.stopCheckLatestChainData(); @@ -1871,7 +1896,8 @@ export class ChainService { await Promise.all([ this.substrateChainHandler.wakeUp(), this.evmChainHandler.wakeUp(), - this.tonChainHandler.wakeUp() + this.tonChainHandler.wakeUp(), + this.cardanoChainHandler.wakeUp() ]); this.checkLatestData(); diff --git a/packages/extension-base/src/services/chain-service/types.ts b/packages/extension-base/src/services/chain-service/types.ts index 5d0b84b84da..390f36fcb40 100644 --- a/packages/extension-base/src/services/chain-service/types.ts +++ b/packages/extension-base/src/services/chain-service/types.ts @@ -6,6 +6,7 @@ import type { ApiInterfaceRx } from '@polkadot/api/types'; import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _CrowdloanFund } from '@subwallet/chain-list/types'; +import { CardanoBalanceItem } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/types'; import { AccountState, TxByMsgResponse } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/types'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { TonWalletContract } from '@subwallet/keyring/types'; @@ -135,7 +136,7 @@ export interface _TonApi extends _ChainBaseApi, _TonUtilsApi { isReady: Promise<_TonApi>; } -export interface _TonUtilsApi { +interface _TonUtilsApi { getBalance (address: Address): Promise<bigint>; open<T extends Contract>(src: T): OpenedContract<T>; estimateExternalMessageFee (walletContract: TonWalletContract, body: Cell, isInit: boolean, ignoreSignature?: boolean): Promise<EstimateExternalMessageFee>; @@ -145,6 +146,14 @@ export interface _TonUtilsApi { getAccountState (address: string): Promise<AccountState>; } +export interface _CardanoApi extends _ChainBaseApi, _CardanoUtilsApi { + isReady: Promise<_CardanoApi>; +} + +interface _CardanoUtilsApi { + getBalanceMap (address: string): Promise<CardanoBalanceItem[]> +} + export interface EstimateExternalMessageFee { source_fees: { in_fwd_fee: number, 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 5aeb6637a8f..c1c9694d1d5 100644 --- a/packages/extension-base/src/services/chain-service/utils/index.ts +++ b/packages/extension-base/src/services/chain-service/utils/index.ts @@ -62,15 +62,19 @@ export function _isEqualSmartContractAsset (asset1: _ChainAsset, asset2: _ChainA } export function _isPureEvmChain (chainInfo: _ChainInfo) { - return (!!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo); + return (!!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo); } export function _isPureSubstrateChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !!chainInfo.substrateInfo && !chainInfo.tonInfo); + return (!chainInfo.evmInfo && !!chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo); } export function _isPureTonChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.tonInfo); + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.tonInfo && !chainInfo.cardanoInfo); +} + +export function _isPureCardanoChain (chainInfo: _ChainInfo) { + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !!chainInfo.cardanoInfo); } export function _getOriginChainOfAsset (assetSlug: string) { @@ -130,6 +134,10 @@ export function _isTokenTransferredByTon (tokenInfo: _ChainAsset) { return _isJettonToken(tokenInfo) || _isNativeToken(tokenInfo); } +export function _isTokenTransferredByCardano (tokenInfo: _ChainAsset) { + return _isCIP26Token(tokenInfo) || _isNativeToken(tokenInfo); +} + // Utils for balance functions export function _getTokenOnChainAssetId (tokenInfo: _ChainAsset): string { return tokenInfo.metadata?.assetId as string || '-1'; @@ -159,6 +167,10 @@ export function _isChainTonCompatible (chainInfo: _ChainInfo) { return !!chainInfo.tonInfo; } +export function _isChainCardanoCompatible (chainInfo: _ChainInfo) { + return !!chainInfo.cardanoInfo; +} + export function _isNativeToken (tokenInfo: _ChainAsset) { return tokenInfo.assetType === _AssetType.NATIVE; } @@ -294,11 +306,13 @@ export function _getTokenTypesSupportedByChain (chainInfo: _ChainInfo): _AssetTy } export function _getChainNativeTokenBasicInfo (chainInfo: _ChainInfo): BasicTokenInfo { + const defaultTokenInfo = { + symbol: '', + decimals: -1 + }; + if (!chainInfo) { - return { - symbol: '', - decimals: -1 - }; + return defaultTokenInfo; } if (chainInfo.substrateInfo) { // substrate by default @@ -316,12 +330,14 @@ export function _getChainNativeTokenBasicInfo (chainInfo: _ChainInfo): BasicToke symbol: chainInfo.tonInfo.symbol, decimals: chainInfo.tonInfo.decimals }; + } else if (chainInfo.cardanoInfo) { + return { + symbol: chainInfo.cardanoInfo.symbol, + decimals: chainInfo.cardanoInfo.decimals + }; } - return { - symbol: '', - decimals: -1 - }; + return defaultTokenInfo; } export function _getChainNativeTokenSlug (chainInfo: _ChainInfo) { @@ -344,6 +360,10 @@ export function _isTokenTonSmartContract (tokenInfo: _ChainAsset) { return [_AssetType.TEP74].includes(tokenInfo.assetType); // add TEP-62 when supporting } +export function _isCIP26Token (tokenInfo: _ChainAsset) { + return [_AssetType.CIP26].includes(tokenInfo.assetType); +} + export function _isTokenWasmSmartContract (tokenInfo: _ChainAsset) { return [_AssetType.PSP22, _AssetType.PSP34].includes(tokenInfo.assetType); } @@ -649,6 +669,10 @@ export const _chainInfoToChainType = (chainInfo: _ChainInfo): AccountChainType = return AccountChainType.TON; } + if (_isChainCardanoCompatible(chainInfo)) { + return AccountChainType.CARDANO; + } + if (_isChainBitcoinCompatible(chainInfo)) { return AccountChainType.BITCOIN; } diff --git a/packages/extension-base/src/services/earning-service/service.ts b/packages/extension-base/src/services/earning-service/service.ts index 7906ca44a88..f5a5bf7b3f6 100644 --- a/packages/extension-base/src/services/earning-service/service.ts +++ b/packages/extension-base/src/services/earning-service/service.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; -import { ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { ChainType, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { CRON_REFRESH_CHAIN_STAKING_METADATA, CRON_REFRESH_EARNING_REWARD_HISTORY_INTERVAL, CRON_REFRESH_STAKING_REWARD_FAST_INTERVAL } from '@subwallet/extension-base/constants'; import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; import { PersistDataServiceInterface, ServiceStatus, StoppableServiceInterface } from '@subwallet/extension-base/services/base/types'; @@ -13,7 +13,7 @@ import { EventService } from '@subwallet/extension-base/services/event-service'; import DatabaseService from '@subwallet/extension-base/services/storage-service/DatabaseService'; import { SWTransaction } from '@subwallet/extension-base/services/transaction-service/types'; import { BasicTxErrorType, EarningRewardHistoryItem, EarningRewardItem, EarningRewardJson, HandleYieldStepData, HandleYieldStepParams, OptimalYieldPath, OptimalYieldPathParams, RequestEarlyValidateYield, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestYieldLeave, RequestYieldWithdrawal, ResponseEarlyValidateYield, TransactionData, ValidateYieldProcessParams, YieldPoolInfo, YieldPoolTarget, YieldPoolType, YieldPositionInfo } from '@subwallet/extension-base/types'; -import { addLazy, categoryAddresses, createPromiseHandler, PromiseHandler, removeLazy } from '@subwallet/extension-base/utils'; +import { addLazy, createPromiseHandler, getAddressesByChainType, PromiseHandler, removeLazy } from '@subwallet/extension-base/utils'; import { fetchStaticCache } from '@subwallet/extension-base/utils/fetchStaticCache'; import { BehaviorSubject } from 'rxjs'; @@ -477,7 +477,8 @@ export default class EarningService implements StoppableServiceInterface, Persis await this.eventService.waitChainReady; - const { evm: evmAddresses, substrate: substrateAddresses } = categoryAddresses(addresses); + const evmAddresses = getAddressesByChainType(addresses, [ChainType.EVM]); + const substrateAddresses = getAddressesByChainType(addresses, [ChainType.SUBSTRATE]); const activeChains = this.state.activeChainSlugs; const unsubList: Array<VoidFunction> = []; @@ -662,7 +663,8 @@ export default class EarningService implements StoppableServiceInterface, Persis await this.eventService.waitChainReady; - const { evm: evmAddresses, substrate: substrateAddresses } = categoryAddresses(addresses); + const evmAddresses = getAddressesByChainType(addresses, [ChainType.EVM]); + const substrateAddresses = getAddressesByChainType(addresses, [ChainType.SUBSTRATE]); const activeChains = this.state.activeChainSlugs; const unsubList: Array<VoidFunction> = []; @@ -735,7 +737,8 @@ export default class EarningService implements StoppableServiceInterface, Persis await this.eventService.waitChainReady; - const { evm: evmAddresses, substrate: substrateAddresses } = categoryAddresses(addresses); + const evmAddresses = getAddressesByChainType(addresses, [ChainType.EVM]); + const substrateAddresses = getAddressesByChainType(addresses, [ChainType.SUBSTRATE]); const activeChains = this.state.activeChainSlugs; const unsubList: Array<VoidFunction> = []; diff --git a/packages/extension-base/src/services/history-service/index.ts b/packages/extension-base/src/services/history-service/index.ts index acf51dc4b04..4855c9a3059 100644 --- a/packages/extension-base/src/services/history-service/index.ts +++ b/packages/extension-base/src/services/history-service/index.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { ExtrinsicStatus, TransactionHistoryItem } from '@subwallet/extension-base/background/KoniTypes'; +import { ChainType, ExtrinsicStatus, TransactionHistoryItem } from '@subwallet/extension-base/background/KoniTypes'; import { CRON_RECOVER_HISTORY_INTERVAL } from '@subwallet/extension-base/constants'; import { PersistDataServiceInterface, ServiceStatus, StoppableServiceInterface } from '@subwallet/extension-base/services/base/types'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; @@ -13,7 +13,7 @@ import { parseSubscanExtrinsicData, parseSubscanTransferData } from '@subwallet/ import { KeyringService } from '@subwallet/extension-base/services/keyring-service'; import DatabaseService from '@subwallet/extension-base/services/storage-service/DatabaseService'; import { SubscanService } from '@subwallet/extension-base/services/subscan-service'; -import { categoryAddresses } from '@subwallet/extension-base/utils'; +import { getAddressesByChainType } from '@subwallet/extension-base/utils'; import { createPromiseHandler } from '@subwallet/extension-base/utils/promise'; import { keyring } from '@subwallet/ui-keyring'; import { BehaviorSubject } from 'rxjs'; @@ -181,7 +181,8 @@ export class HistoryService implements StoppableServiceInterface, PersistDataSer subscribeHistories (chain: string, proxyId: string, cb: (items: TransactionHistoryItem[]) => void) { const addresses = this.keyringService.context.getDecodedAddresses(proxyId, false); - const { evm, substrate } = categoryAddresses(addresses); + const evmAddresses = getAddressesByChainType(addresses, [ChainType.EVM]); + const substrateAddresses = getAddressesByChainType(addresses, [ChainType.SUBSTRATE]); const subscription = this.historySubject.subscribe((items) => { cb(items.filter(filterHistoryItemByAddressAndChain(chain, addresses))); @@ -191,9 +192,9 @@ export class HistoryService implements StoppableServiceInterface, PersistDataSer if (_isChainSubstrateCompatible(chainInfo)) { if (_isChainEvmCompatible(chainInfo)) { - this.fetchSubscanTransactionHistory(chain, evm); + this.fetchSubscanTransactionHistory(chain, evmAddresses); } else { - this.fetchSubscanTransactionHistory(chain, substrate); + this.fetchSubscanTransactionHistory(chain, substrateAddresses); } } diff --git a/packages/extension-base/src/services/inapp-notification-service/index.ts b/packages/extension-base/src/services/inapp-notification-service/index.ts index dc8095d503b..dd2b6230152 100644 --- a/packages/extension-base/src/services/inapp-notification-service/index.ts +++ b/packages/extension-base/src/services/inapp-notification-service/index.ts @@ -3,7 +3,7 @@ import { COMMON_ASSETS, COMMON_CHAIN_SLUGS } from '@subwallet/chain-list'; import { _ChainAsset } from '@subwallet/chain-list/types'; -import { ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { ChainType, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { CRON_LISTEN_AVAIL_BRIDGE_CLAIM } from '@subwallet/extension-base/constants'; import { fetchLastestRemindNotificationTime } from '@subwallet/extension-base/constants/remind-notification-time'; import { CronServiceInterface, ServiceStatus } from '@subwallet/extension-base/services/base/types'; @@ -15,7 +15,7 @@ import { AvailBridgeSourceChain, AvailBridgeTransaction, fetchAllAvailBridgeClai import { KeyringService } from '@subwallet/extension-base/services/keyring-service'; import DatabaseService from '@subwallet/extension-base/services/storage-service/DatabaseService'; import { GetNotificationParams, RequestSwitchStatusParams } from '@subwallet/extension-base/types/notification'; -import { categoryAddresses, formatNumber } from '@subwallet/extension-base/utils'; +import { formatNumber, getAddressesByChainType } from '@subwallet/extension-base/utils'; import { isSubstrateAddress } from '@subwallet/keyring'; export class InappNotificationService implements CronServiceInterface { @@ -201,12 +201,14 @@ export class InappNotificationService implements CronServiceInterface { getCategorizedAddresses () { const addresses = this.keyringService.context.getAllAddresses(); + const evmAddresses = getAddressesByChainType(addresses, [ChainType.EVM]); + const substrateAddresses = getAddressesByChainType(addresses, [ChainType.SUBSTRATE]); - return categoryAddresses(addresses); + return { evmAddresses: evmAddresses, substrateAddresses: substrateAddresses }; } createAvailBridgeClaimNotification () { - const { evm: evmAddresses, substrate: substrateAddresses } = this.getCategorizedAddresses(); + const { evmAddresses, substrateAddresses } = this.getCategorizedAddresses(); const chainAssets = this.chainService.getAssetRegistry(); @@ -299,7 +301,7 @@ export class InappNotificationService implements CronServiceInterface { // Polygon Claimable Handle async createPolygonClaimableTransactions () { - const { evm: evmAddresses } = this.getCategorizedAddresses(); + const { evmAddresses } = this.getCategorizedAddresses(); const etherChains = [COMMON_ASSETS.ETH, COMMON_ASSETS.ETH_SEPOLIA]; const polygonAssets = Object.values(this.chainService.getAssetRegistry()).filter( @@ -411,4 +413,8 @@ export class InappNotificationService implements CronServiceInterface { removeAccountNotifications (proxyId: string) { this.dbService.removeAccountNotifications(proxyId).catch(console.error); } + + migrateNotificationProxyId (proxyIds: string[], newProxyId: string, newName: string) { + this.dbService.updateNotificationProxyId(proxyIds, newProxyId, newName); + } } diff --git a/packages/extension-base/src/services/keyring-service/context/account-context.ts b/packages/extension-base/src/services/keyring-service/context/account-context.ts index ff997e5df1a..1747ff12d79 100644 --- a/packages/extension-base/src/services/keyring-service/context/account-context.ts +++ b/packages/extension-base/src/services/keyring-service/context/account-context.ts @@ -1,9 +1,10 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { AccountExternalError, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestChangeMasterPassword, RequestMigratePassword, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseMigratePassword } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountExternalError, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestChangeMasterPassword, RequestMigratePassword, RequestMigrateSoloAccount, RequestMigrateUnifiedAndFetchEligibleSoloAccounts, RequestPingSession, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseMigratePassword } from '@subwallet/extension-base/background/KoniTypes'; import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; import { KeyringService } from '@subwallet/extension-base/services/keyring-service'; +import { AccountMigrationHandler } from '@subwallet/extension-base/services/keyring-service/context/handlers/Migration'; import { AccountProxyMap, CurrentAccountInfo, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestAccountProxyEdit, RequestAccountProxyForget, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestChangeTonWalletContractVersion, RequestCheckPublicAndSecretKey, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseJsonGetAccountInfo, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2 } from '@subwallet/extension-base/types'; import { InjectedAccountWithMeta } from '@subwallet/extension-inject/types'; import { SubjectInfo } from '@subwallet/ui-keyring/observable/types'; @@ -20,6 +21,7 @@ export class AccountContext { private readonly ledgerHandler: AccountLedgerHandler; private readonly modifyHandler: AccountModifyHandler; private readonly secretHandler: AccountSecretHandler; + private readonly migrationHandler: AccountMigrationHandler; constructor (private readonly koniState: KoniState, private readonly parentService: KeyringService) { this.state = new AccountState(this.koniState); @@ -30,6 +32,7 @@ export class AccountContext { this.ledgerHandler = new AccountLedgerHandler(this.parentService, this.state); this.modifyHandler = new AccountModifyHandler(this.parentService, this.state); this.secretHandler = new AccountSecretHandler(this.parentService, this.state); + this.migrationHandler = new AccountMigrationHandler(this.parentService, this.state); } // TODO: Merge to value @@ -253,8 +256,8 @@ export class AccountContext { } /* Derive account proxy */ - public derivationAccountProxyCreate (request: RequestDeriveCreateV3): boolean { - return this.deriveHandler.derivationAccountProxyCreate(request); + public derivationAccountProxyCreate (request: RequestDeriveCreateV3, isMigration?: boolean): boolean { + return this.deriveHandler.derivationAccountProxyCreate(request, isMigration); } /* Derive */ @@ -271,6 +274,21 @@ export class AccountContext { /* Inject */ + /* Migration */ + public async migrateUnifiedAndFetchEligibleSoloAccounts (request: RequestMigrateUnifiedAndFetchEligibleSoloAccounts, setMigratingModeFn: () => void) { + return await this.migrationHandler.migrateUnifiedAndFetchEligibleSoloAccounts(request, setMigratingModeFn); + } + + public migrateSoloAccount (request: RequestMigrateSoloAccount) { + return this.migrationHandler.migrateSoloToUnifiedAccount(request); + } + + public pingSession (request: RequestPingSession) { + return this.migrationHandler.pingSession(request); + } + + /* Migration */ + /* Others */ public removeNoneHardwareGenesisHash () { diff --git a/packages/extension-base/src/services/keyring-service/context/handlers/Derive.ts b/packages/extension-base/src/services/keyring-service/context/handlers/Derive.ts index ab01fa6d66f..69fbfce229e 100644 --- a/packages/extension-base/src/services/keyring-service/context/handlers/Derive.ts +++ b/packages/extension-base/src/services/keyring-service/context/handlers/Derive.ts @@ -12,7 +12,7 @@ import { assert } from '@polkadot/util'; import { AccountBaseHandler } from './Base'; -const validDeriveKeypairTypes: KeypairType[] = [...SubstrateKeypairTypes, ...EthereumKeypairTypes, 'ton']; +const validDeriveKeypairTypes: KeypairType[] = [...SubstrateKeypairTypes, ...EthereumKeypairTypes, 'ton', 'cardano']; /** * @class AccountDeriveHandler @@ -227,7 +227,7 @@ export class AccountDeriveHandler extends AccountBaseHandler { /** * Derive account proxy * */ - public derivationAccountProxyCreate (request: RequestDeriveCreateV3): boolean { + public derivationAccountProxyCreate (request: RequestDeriveCreateV3, isMigration = false): boolean { const { name, proxyId: deriveId, suri } = request; const isUnified = this.state.isUnifiedAccount(deriveId); @@ -237,7 +237,7 @@ export class AccountDeriveHandler extends AccountBaseHandler { const nameExists = this.state.checkNameExists(name); - if (nameExists) { + if (nameExists && !isMigration) { throw new SWCommonAccountError(CommonAccountErrorType.ACCOUNT_NAME_EXISTED); } @@ -366,7 +366,9 @@ export class AccountDeriveHandler extends AccountBaseHandler { const addresses = pairs.map((pair) => pair.address); const exists = this.state.checkAddressExists(addresses); - assert(!exists, t('Account already exists under the name "{{name}}"', { replace: { name: exists?.name || exists?.address || '' } })); + if (!isMigration) { + assert(!exists, t('Account already exists under the name "{{name}}"', { replace: { name: exists?.name || exists?.address || '' } })); + } childAccountProxy && this.state.upsertAccountProxyByKey(childAccountProxy); this.state.upsertModifyPairs(modifyPairs); diff --git a/packages/extension-base/src/services/keyring-service/context/handlers/Migration.ts b/packages/extension-base/src/services/keyring-service/context/handlers/Migration.ts new file mode 100644 index 00000000000..d64bcfcb9eb --- /dev/null +++ b/packages/extension-base/src/services/keyring-service/context/handlers/Migration.ts @@ -0,0 +1,286 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { RequestMigrateSoloAccount, RequestMigrateUnifiedAndFetchEligibleSoloAccounts, RequestPingSession, ResponseMigrateSoloAccount, ResponseMigrateUnifiedAndFetchEligibleSoloAccounts, SoloAccountToBeMigrated } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountBaseHandler } from '@subwallet/extension-base/services/keyring-service/context/handlers/Base'; +import { AccountChainType, AccountProxy, SUPPORTED_ACCOUNT_CHAIN_TYPES } from '@subwallet/extension-base/types'; +import { createAccountProxyId, getDefaultKeypairTypeFromAccountChainType, getSuri } from '@subwallet/extension-base/utils'; +import { generateRandomString } from '@subwallet/extension-base/utils/getId'; +import { keyring } from '@subwallet/ui-keyring'; + +import { keyExtractSuri } from '@polkadot/util-crypto'; + +export const SESSION_TIMEOUT = 10000; + +interface SessionInfo { + password: string, + timeoutId: NodeJS.Timeout +} + +interface UnifiedAccountGroup { + derivedUnifiedAccounts: AccountProxy[], + masterUnifiedAccounts: AccountProxy[] +} + +export class AccountMigrationHandler extends AccountBaseHandler { + private sessionIdToPassword: Record<string, SessionInfo> = {}; + + public pingSession ({ sessionId }: RequestPingSession) { + if (!this.sessionIdToPassword[sessionId]) { // todo: if no persistent sessionId, should we jump to enter password again? + throw Error(`Session ID ${sessionId} not found.`); + } + + clearTimeout(this.sessionIdToPassword[sessionId].timeoutId); + this.sessionIdToPassword[sessionId].timeoutId = setTimeout(() => { + delete this.sessionIdToPassword[sessionId]; + }, SESSION_TIMEOUT); + + return true; + } + + public async migrateUnifiedAndFetchEligibleSoloAccounts (request: RequestMigrateUnifiedAndFetchEligibleSoloAccounts, setMigratingModeFn: () => void): Promise<ResponseMigrateUnifiedAndFetchEligibleSoloAccounts> { + // Migrate unified -> unified + const password = request.password; + const allAccountProxies = Object.values(this.state.accounts); + const UACanBeMigrated = this.getUACanBeMigrated(allAccountProxies); + const UACanBeMigratedSortedByParent = this.sortUAByParent(UACanBeMigrated); // master account must be migrated before derived account + const migratedUnifiedAccountIds = await this.migrateUnifiedToUnifiedAccount(password, UACanBeMigratedSortedByParent, setMigratingModeFn); + + // Get solo accounts can be migrated + const soloAccountsNeedToBeMigrated = this.getSoloAccountsNeedToBeMigrated(allAccountProxies); + const soloAccountsNeedToBeMigratedGroup = this.groupSoloAccountByMnemonic(password, soloAccountsNeedToBeMigrated); + const eligibleSoloAccountMap = this.accountProxiesToEligibleSoloAccountMap(soloAccountsNeedToBeMigratedGroup); + + // Create persistent mapping sessionId <-> password + const uniqueId = Date.now().toString(); + const timeoutId = setTimeout(() => delete this.sessionIdToPassword[uniqueId], SESSION_TIMEOUT * 2); + + this.sessionIdToPassword[uniqueId] = { + password, + timeoutId + }; + + return { + migratedUnifiedAccountIds, + soloAccounts: eligibleSoloAccountMap, + sessionId: uniqueId + }; + } + + public async migrateUnifiedToUnifiedAccount (password: string, accountProxies: AccountProxy[], setMigratingModeFn: () => void): Promise<string[]> { + keyring.unlockKeyring(password); + this.parentService.updateKeyringState(); + setMigratingModeFn(); + + const unifiedAccountIds: string[] = []; + const modifiedPairs = structuredClone(this.state.modifyPairs); + + const { derivedUnifiedAccounts, masterUnifiedAccounts } = accountProxies.reduce((accountInfo, account: AccountProxy) => { + const isDerivedAccount = !!account.parentId; + + isDerivedAccount ? accountInfo.derivedUnifiedAccounts.push(account) : accountInfo.masterUnifiedAccounts.push(account); + + return accountInfo; + }, { derivedUnifiedAccounts: [], masterUnifiedAccounts: [] } as unknown as UnifiedAccountGroup); + + try { + for (const unifiedAccount of masterUnifiedAccounts) { + const proxyId = unifiedAccount.id; + const mnemonic = this.parentService.context.exportAccountProxyMnemonic({ + password, + proxyId + }).result; + + const newChainTypes = Object.values(AccountChainType).filter((type) => !unifiedAccount.chainTypes.includes(type) && SUPPORTED_ACCOUNT_CHAIN_TYPES.includes(type)); + const keypairTypes = newChainTypes.map((chainType) => getDefaultKeypairTypeFromAccountChainType(chainType)); + + keypairTypes.forEach((type) => { + const suri = getSuri(mnemonic, type); + const pair = keyring.createFromUri(suri, {}, type); + const address = pair.address; + + modifiedPairs[address] = { accountProxyId: proxyId, migrated: true, key: address }; + }); + + keypairTypes.forEach((type) => { + const suri = getSuri(mnemonic, type); + const { derivePath } = keyExtractSuri(suri); + const metadata = { + name: unifiedAccount.name, + derivationPath: derivePath ? derivePath.substring(1) : undefined + }; + + const rs = keyring.addUri(suri, metadata, type); + const address = rs.pair.address; + + this.state._addAddressToAuthList(address, true); + }); + + this.state.upsertModifyPairs(modifiedPairs); + + unifiedAccountIds.push(proxyId); + } + + await new Promise((resolve) => setTimeout(resolve, 1800)); // Wait last master unified account migrated. // todo: can be optimized later by await a promise resolve if master account is migrating + + for (const unifiedAccount of derivedUnifiedAccounts) { + this.parentService.context.derivationAccountProxyCreate({ + name: unifiedAccount.name, + suri: unifiedAccount.suri || '', + proxyId: unifiedAccount.parentId || '' + }, true); + unifiedAccountIds.push(unifiedAccount.id); + } + } catch (error) { + console.error('Migration unified account failed with error:', error); + } finally { + keyring.lockAll(false); + this.parentService.updateKeyringState(); + } + + return unifiedAccountIds; + } + + public getUACanBeMigrated (accountProxies: AccountProxy[]): AccountProxy[] { + return accountProxies.filter((account) => this.state.isUnifiedAccount(account.id) && account.isNeedMigrateUnifiedAccount); + } + + public getSoloAccountsNeedToBeMigrated (accountProxies: AccountProxy[]): AccountProxy[] { + return accountProxies.filter((account) => !this.state.isUnifiedAccount(account.id) && account.isNeedMigrateUnifiedAccount); + } + + public groupSoloAccountByMnemonic (password: string, accountProxies: AccountProxy[]) { + const parentService = this.parentService; + + return accountProxies.reduce(function (rs: Record<string, AccountProxy[]>, item) { + const oldProxyId = item.id; + const mnemonic = parentService.context.exportAccountProxyMnemonic({ + password, + proxyId: oldProxyId + }).result; + const upcomingProxyId = createAccountProxyId(mnemonic); + + if (!rs[upcomingProxyId]) { + rs[upcomingProxyId] = []; + } + + rs[upcomingProxyId].push(item); + + return rs; + }, {}); + } + + public accountProxiesToEligibleSoloAccountMap (accountProxyMap: Record<string, AccountProxy[]>): Record<string, SoloAccountToBeMigrated[]> { + const eligibleSoloAccountMap: Record<string, SoloAccountToBeMigrated[]> = {}; + + Object.entries(accountProxyMap).forEach(([upcomingProxyId, accounts]) => { + eligibleSoloAccountMap[upcomingProxyId] = accounts.map((account) => { + return { + upcomingProxyId, + proxyId: account.accounts[0].proxyId, + address: account.accounts[0].address, + name: account.name, + chainType: account.chainTypes[0] + } as SoloAccountToBeMigrated; + }); + }); + + return eligibleSoloAccountMap; + } + + public sortUAByParent (accountProxies: AccountProxy[]): AccountProxy[] { + const undefinedToStr = (str: string | undefined) => str ?? ''; + + return accountProxies.sort((a, b) => undefinedToStr(a.parentId) < undefinedToStr(b.parentId) ? -1 : undefinedToStr(a.parentId) > undefinedToStr(b.parentId) ? 1 : 0); + } + + public migrateSoloToUnifiedAccount (request: RequestMigrateSoloAccount): ResponseMigrateSoloAccount { + const { accountName, sessionId, soloAccounts } = request; + const password = this.sessionIdToPassword[sessionId].password; + + keyring.unlockKeyring(password); + this.parentService.updateKeyringState(); + const modifiedPairs = structuredClone(this.state.modifyPairs); + const firstAccountInfo = soloAccounts[0]; + const upcomingProxyId = firstAccountInfo.upcomingProxyId; + const firstAccountOldProxyId = firstAccountInfo.proxyId; + + try { + const mnemonic = this.parentService.context.exportAccountProxyMnemonic({ password, proxyId: firstAccountOldProxyId }).result; + + const keypairTypes = SUPPORTED_ACCOUNT_CHAIN_TYPES.map((chainType) => getDefaultKeypairTypeFromAccountChainType(chainType as AccountChainType)); + + keypairTypes.forEach((type) => { + const suri = getSuri(mnemonic, type); + const pair = keyring.createFromUri(suri, {}, type); + const address = pair.address; + + modifiedPairs[address] = { accountProxyId: upcomingProxyId, migrated: true, key: address }; + }); + + this.state.upsertAccountProxyByKey({ id: upcomingProxyId, name: accountName, isMigrationDone: false }); + + const soloAccountProxyIds: string[] = []; + + keypairTypes.forEach((type) => { + const suri = getSuri(mnemonic, type); + const { derivePath } = keyExtractSuri(suri); + const metadata = { + name: accountName.concat(' - ').concat(generateRandomString()), + derivationPath: derivePath ? derivePath.substring(1) : undefined + }; + + const rs = keyring.addUri(suri, metadata, type); + soloAccountProxyIds.push(rs.json.address); + const address = rs.pair.address; + + this.state._addAddressToAuthList(address, true); + }); + + this.state.upsertModifyPairs(modifiedPairs); + this.migrateDerivedSoloAccountRelationship(soloAccounts); + this.state.upsertAccountProxyByKey({ id: upcomingProxyId, name: accountName, isMigrationDone: true }); + + // Re-update account name + soloAccountProxyIds.forEach((oldProxyId) => { + const pair = keyring.getPair(oldProxyId); + + keyring.saveAccountMeta(pair, { ...pair.meta, name: accountName }); + }); + + // Update current account after migrating + const currentAccountProxyId = this.state.currentAccount.proxyId; + + if (soloAccountProxyIds.includes(currentAccountProxyId)) { + this.state.saveCurrentAccountProxyId(upcomingProxyId); + } + } catch (error) { + console.error('Migration solo account failed with error', error); + } finally { + keyring.lockAll(false); + this.parentService.updateKeyringState(); + } + + return { + migratedUnifiedAccountId: upcomingProxyId + }; + } + + public migrateDerivedSoloAccountRelationship (soloAccounts: SoloAccountToBeMigrated[]) { + const accountProxies = this.state.accountProxies; + + // Use Set.has & Map.get to optimize search performance + const proxyIdsSet = new Set(soloAccounts.map((account) => account.proxyId)); + const proxyIdToUpcomingProxyIdMap = new Map(soloAccounts.map((account) => [account.proxyId, account.upcomingProxyId])); + + for (const account of Object.values(accountProxies)) { + const currentParent = account.parentId; + + if (currentParent && proxyIdsSet.has(currentParent)) { + accountProxies[account.id].parentId = proxyIdToUpcomingProxyIdMap.get(currentParent); + } + } + + this.state.upsertAccountProxy(accountProxies); + } +} diff --git a/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts b/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts index 98d0ba96851..402321ea543 100644 --- a/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts +++ b/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts @@ -27,7 +27,7 @@ export class AccountMnemonicHandler extends AccountBaseHandler { /* Create seed */ public async mnemonicCreateV2 ({ length = SEED_DEFAULT_LENGTH, mnemonic: _seed, type = 'general' }: RequestMnemonicCreateV2): Promise<ResponseMnemonicCreateV2> { - const types: KeypairType[] = type === 'general' ? ['sr25519', 'ethereum', 'ton'] : ['ton-native']; + const types: KeypairType[] = type === 'general' ? ['sr25519', 'ethereum', 'ton', 'cardano'] : ['ton-native']; const seed = _seed || type === 'general' ? mnemonicGenerate(length) @@ -89,7 +89,7 @@ export class AccountMnemonicHandler extends AccountBaseHandler { const addressDict = {} as Record<KeypairType, string>; let changedAccount = false; const hasMasterPassword = keyring.keyring.hasMasterPassword; - const types: KeypairType[] = type ? [type] : ['sr25519', 'ethereum', 'ton']; + const types: KeypairType[] = type ? [type] : ['sr25519', 'ethereum', 'ton', 'cardano']; if (!hasMasterPassword) { if (!password) { diff --git a/packages/extension-base/src/services/keyring-service/context/handlers/Secret.ts b/packages/extension-base/src/services/keyring-service/context/handlers/Secret.ts index aa4cc208cfc..b066271d54d 100644 --- a/packages/extension-base/src/services/keyring-service/context/handlers/Secret.ts +++ b/packages/extension-base/src/services/keyring-service/context/handlers/Secret.ts @@ -5,7 +5,7 @@ import { AccountExternalError, AccountExternalErrorCode, RequestAccountCreateExt import { AccountChainType, CommonAccountErrorType, RequestCheckPublicAndSecretKey, RequestPrivateKeyValidateV2, ResponseCheckPublicAndSecretKey, ResponsePrivateKeyValidateV2, SWCommonAccountError } from '@subwallet/extension-base/types'; import { getKeypairTypeByAddress } from '@subwallet/keyring'; import { decodePair } from '@subwallet/keyring/pair/decode'; -import { BitcoinKeypairTypes, KeypairType, KeyringPair, KeyringPair$Meta, TonKeypairTypes } from '@subwallet/keyring/types'; +import { BitcoinKeypairTypes, CardanoKeypairTypes, KeypairType, KeyringPair, KeyringPair$Meta, TonKeypairTypes } from '@subwallet/keyring/types'; import keyring from '@subwallet/ui-keyring'; import { t } from 'i18next'; @@ -51,7 +51,7 @@ export class AccountSecretHandler extends AccountBaseHandler { genesisHash: '' }; - if ([...BitcoinKeypairTypes, ...TonKeypairTypes].includes(type) && isReadOnly) { + if ([...BitcoinKeypairTypes, ...TonKeypairTypes, ...CardanoKeypairTypes].includes(type) && isReadOnly) { meta.noPublicKey = true; } diff --git a/packages/extension-base/src/services/keyring-service/utils.ts b/packages/extension-base/src/services/keyring-service/utils.ts new file mode 100644 index 00000000000..a20d08b95a2 --- /dev/null +++ b/packages/extension-base/src/services/keyring-service/utils.ts @@ -0,0 +1,14 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { AccountProxy } from '@subwallet/extension-base/types'; + +export const hasAnyAccountForMigration = (allAccountProxies: AccountProxy[]) => { + for (const account of allAccountProxies) { + if (account.isNeedMigrateUnifiedAccount) { + return true; + } + } + + return false; +}; diff --git a/packages/extension-base/src/services/request-service/handler/CardanoRequestHandler.ts b/packages/extension-base/src/services/request-service/handler/CardanoRequestHandler.ts new file mode 100644 index 00000000000..2c2d5c04ad3 --- /dev/null +++ b/packages/extension-base/src/services/request-service/handler/CardanoRequestHandler.ts @@ -0,0 +1,196 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ConfirmationDefinitionsCardano, ConfirmationsQueueCardano, ConfirmationsQueueItemOptions, ConfirmationTypeCardano, RequestConfirmationCompleteCardano } from '@subwallet/extension-base/background/KoniTypes'; +import { ConfirmationRequestBase, Resolver } from '@subwallet/extension-base/background/types'; +import RequestService from '@subwallet/extension-base/services/request-service'; +import { isInternalRequest } from '@subwallet/extension-base/utils/request'; +import { keyring } from '@subwallet/ui-keyring'; +import { t } from 'i18next'; +import { BehaviorSubject } from 'rxjs'; + +import { logger as createLogger } from '@polkadot/util/logger'; +import { Logger } from '@polkadot/util/types'; + +export default class CardanoRequestHandler { + readonly #requestService: RequestService; + readonly #logger: Logger; + + private readonly confirmationsQueueSubjectCardano = new BehaviorSubject<ConfirmationsQueueCardano>({ + cardanoSignatureRequest: {}, + cardanoSendTransactionRequest: {}, + cardanoWatchTransactionRequest: {} + }); + + private readonly confirmationsPromiseMap: Record<string, { resolver: Resolver<any>, validator?: (rs: any) => Error | undefined }> = {}; + + constructor (requestService: RequestService) { + this.#requestService = requestService; + this.#logger = createLogger('CardanoRequestHandler'); + } + + public get numCardanoRequests (): number { + let count = 0; + + Object.values(this.confirmationsQueueSubjectCardano.getValue()).forEach((x) => { + count += Object.keys(x).length; + }); + + return count; + } + + public getConfirmationsQueueSubjectCardano (): BehaviorSubject<ConfirmationsQueueCardano> { + return this.confirmationsQueueSubjectCardano; + } + + public async addConfirmationCardano<CT extends ConfirmationTypeCardano> ( + id: string, + url: string, + type: CT, + payload: ConfirmationDefinitionsCardano[CT][0]['payload'], + options: ConfirmationsQueueItemOptions = {}, + validator?: (input: ConfirmationDefinitionsCardano[CT][1]) => Error | undefined + ): Promise<ConfirmationDefinitionsCardano[CT][1]> { + const confirmations = this.confirmationsQueueSubjectCardano.getValue(); + const confirmationType = confirmations[type] as Record<string, ConfirmationDefinitionsCardano[CT][0]>; + const payloadJson = JSON.stringify({}); + const isInternal = isInternalRequest(url); + + if (['cardanoSendTransactionRequest', 'cardanoSignatureRequest'].includes(type)) { + const isAlwaysRequired = await this.#requestService.settingService.isAlwaysRequired; + + if (isAlwaysRequired) { + this.#requestService.keyringService.lock(); + } + } + + // Check duplicate request + const duplicated = Object.values(confirmationType).find((c) => (c.url === url) && (c.payloadJson === payloadJson)); + + if (duplicated) { + throw new Error('Cardano duplicate request'); // update this message. + } + + confirmationType[id] = { + id, + url, + isInternal, + payload, + payloadJson, + ...options + } as ConfirmationDefinitionsCardano[CT][0]; + + const promise = new Promise<ConfirmationDefinitionsCardano[CT][1]>((resolve, reject) => { + this.confirmationsPromiseMap[id] = { + validator: validator, + resolver: { + resolve: resolve, + reject: reject + } + }; + }); + + this.confirmationsQueueSubjectCardano.next(confirmations); + + if (!isInternal) { + this.#requestService.popupOpen(); + } + + this.#requestService.updateIconV2(); + + return promise; + } + + public async completeConfirmationCardano (request: RequestConfirmationCompleteCardano): Promise<boolean> { + const confirmations = this.confirmationsQueueSubjectCardano.getValue(); + + for (const ct in request) { + const type = ct as ConfirmationTypeCardano; + const result = request[type] as ConfirmationDefinitionsCardano[typeof type][1]; + + const { id } = result; + const { resolver, validator } = this.confirmationsPromiseMap[id]; + const confirmation = confirmations[type][id]; + + if (!resolver || !confirmation) { + this.#logger.error(t('Unable to proceed. Please try again'), type, id); + throw new Error(t('Unable to proceed. Please try again')); + } + + // Fill signature for some special type + await this.decorateResult(type, confirmation, result); + + // Validate response from confirmation popup some info like password, response format.... + const error = validator && validator(result); + + if (error) { + resolver.reject(error); + } + + // Delete confirmations from queue + delete this.confirmationsPromiseMap[id]; + delete confirmations[type][id]; + this.confirmationsQueueSubjectCardano.next(confirmations); + + // Update icon, and close queue + this.#requestService.updateIconV2(this.#requestService.numAllRequests === 0); + resolver.resolve(result); + } + + // TODO: Review later + return true; + } + + private async decorateResult<T extends ConfirmationTypeCardano> (t: T, request: ConfirmationDefinitionsCardano[T][0], result: ConfirmationDefinitionsCardano[T][1]) { + if (result.payload === '') { + if (t === 'cardanoSignatureRequest') { + // result.payload = await this.signMessage(request as ConfirmationDefinitions['evmSignatureRequest'][0]); + } else if (t === 'cardanoSendTransactionRequest') { + result.payload = this.signTransactionCardano(request as ConfirmationDefinitionsCardano['cardanoSendTransactionRequest'][0]); + } + + if (t === 'cardanoSignatureRequest' || t === 'cardanoSendTransactionRequest') { + const isAlwaysRequired = await this.#requestService.settingService.isAlwaysRequired; + + if (isAlwaysRequired) { + this.#requestService.keyringService.lock(); + } + } + } + } + + private signTransactionCardano (confirmation: ConfirmationDefinitionsCardano['cardanoSendTransactionRequest'][0]): string { // alibaba + const transaction = confirmation.payload; + const { cardanoPayload, from } = transaction; + + const pair = keyring.getPair(from); + + if (pair.isLocked) { + keyring.unlockPair(pair.address); + } + + return pair.cardano.sign(cardanoPayload); + } + + public resetWallet () { + const confirmations = this.confirmationsQueueSubjectCardano.getValue(); + + for (const [type, requests] of Object.entries(confirmations)) { + for (const confirmation of Object.values(requests)) { + const { id } = confirmation as ConfirmationRequestBase; + const { resolver } = this.confirmationsPromiseMap[id]; + + if (!resolver || !confirmation) { + console.error('Not found confirmation', type, id); + } else { + resolver.reject(new Error('Reset wallet')); + } + + delete this.confirmationsPromiseMap[id]; + delete confirmations[type as ConfirmationTypeCardano][id]; + } + } + + this.confirmationsQueueSubjectCardano.next(confirmations); + } +} diff --git a/packages/extension-base/src/services/request-service/index.ts b/packages/extension-base/src/services/request-service/index.ts index a9f9f312e17..8aee8c3cf2f 100644 --- a/packages/extension-base/src/services/request-service/index.ts +++ b/packages/extension-base/src/services/request-service/index.ts @@ -1,10 +1,11 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AuthRequestV2, ConfirmationDefinitions, ConfirmationDefinitionsTon, ConfirmationsQueue, ConfirmationsQueueItemOptions, ConfirmationsQueueTon, ConfirmationType, ConfirmationTypeTon, RequestConfirmationComplete, RequestConfirmationCompleteTon } from '@subwallet/extension-base/background/KoniTypes'; +import { AuthRequestV2, ConfirmationDefinitions, ConfirmationDefinitionsCardano, ConfirmationDefinitionsTon, ConfirmationsQueue, ConfirmationsQueueCardano, ConfirmationsQueueItemOptions, ConfirmationsQueueTon, ConfirmationType, ConfirmationTypeCardano, ConfirmationTypeTon, RequestConfirmationComplete, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType, AuthorizeRequest, MetadataRequest, RequestAuthorizeTab, RequestSign, ResponseSigning, SigningRequest } from '@subwallet/extension-base/background/types'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; import { KeyringService } from '@subwallet/extension-base/services/keyring-service'; +import CardanoRequestHandler from '@subwallet/extension-base/services/request-service/handler/CardanoRequestHandler'; import SettingService from '@subwallet/extension-base/services/setting-service/SettingService'; import { WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; import { MetadataDef } from '@subwallet/extension-inject/types'; @@ -27,6 +28,7 @@ export default class RequestService { readonly #substrateRequestHandler: SubstrateRequestHandler; readonly #evmRequestHandler: EvmRequestHandler; readonly #tonRequestHandler: TonRequestHandler; + readonly #cardanoRequestHandler: CardanoRequestHandler; readonly #connectWCRequestHandler: ConnectWCRequestHandler; readonly #notSupportWCRequestHandler: NotSupportWCRequestHandler; @@ -41,6 +43,7 @@ export default class RequestService { this.#substrateRequestHandler = new SubstrateRequestHandler(this); this.#evmRequestHandler = new EvmRequestHandler(this); this.#tonRequestHandler = new TonRequestHandler(this); + this.#cardanoRequestHandler = new CardanoRequestHandler(this); this.#connectWCRequestHandler = new ConnectWCRequestHandler(this); this.#notSupportWCRequestHandler = new NotSupportWCRequestHandler(this); @@ -49,7 +52,7 @@ export default class RequestService { } public get numAllRequests () { - return this.allSubstrateRequests.length + this.numEvmRequests + this.numTonRequests; + return this.allSubstrateRequests.length + this.numEvmRequests + this.numTonRequests + this.numCardanoRequests; } public updateIconV2 (shouldClose?: boolean): void { @@ -178,6 +181,10 @@ export default class RequestService { return this.#tonRequestHandler.numTonRequests; } + public get numCardanoRequests (): number { + return this.#cardanoRequestHandler.numCardanoRequests; + } + public get confirmationsQueueSubject (): BehaviorSubject<ConfirmationsQueue> { return this.#evmRequestHandler.getConfirmationsQueueSubject(); } @@ -186,6 +193,10 @@ export default class RequestService { return this.#tonRequestHandler.getConfirmationsQueueSubjectTon(); } + public get confirmationsQueueSubjectCardano (): BehaviorSubject<ConfirmationsQueueCardano> { + return this.#cardanoRequestHandler.getConfirmationsQueueSubjectCardano(); + } + public getSignRequest (id: string) { return this.#substrateRequestHandler.getSignRequest(id); } @@ -209,13 +220,24 @@ export default class RequestService { id: string, url: string, type: CT, - payload: ConfirmationDefinitionsTon[CT][0]['payload'], // todo: messages <-> payload + payload: ConfirmationDefinitionsTon[CT][0]['payload'], options: ConfirmationsQueueItemOptions = {}, validator?: (input: ConfirmationDefinitionsTon[CT][1]) => Error | undefined ): Promise<ConfirmationDefinitionsTon[CT][1]> { return this.#tonRequestHandler.addConfirmationTon(id, url, type, payload, options, validator); } + public addConfirmationCardano<CT extends ConfirmationTypeCardano> ( + id: string, + url: string, + type: CT, + payload: ConfirmationDefinitionsCardano[CT][0]['payload'], + options: ConfirmationsQueueItemOptions = {}, + validator?: (input: ConfirmationDefinitionsCardano[CT][1]) => Error | undefined + ): Promise<ConfirmationDefinitionsCardano[CT][1]> { + return this.#cardanoRequestHandler.addConfirmationCardano(id, url, type, payload, options, validator); + } + public async completeConfirmation (request: RequestConfirmationComplete): Promise<boolean> { return await this.#evmRequestHandler.completeConfirmation(request); } @@ -224,6 +246,10 @@ export default class RequestService { return await this.#tonRequestHandler.completeConfirmationTon(request); } + public async completeConfirmationCardano (request: RequestConfirmationCompleteCardano) { + return await this.#cardanoRequestHandler.completeConfirmationCardano(request); + } + public updateConfirmation<CT extends ConfirmationType> ( id: string, type: CT, @@ -278,7 +304,7 @@ export default class RequestService { // General methods public get numRequests (): number { - return this.numMetaRequests + this.numAuthRequests + this.numSubstrateRequests + this.numEvmRequests + this.numConnectWCRequests + this.numNotSupportWCRequests + this.numTonRequests; + return this.numMetaRequests + this.numAuthRequests + this.numSubstrateRequests + this.numEvmRequests + this.numConnectWCRequests + this.numNotSupportWCRequests + this.numTonRequests + this.numCardanoRequests; } public resetWallet (): void { @@ -286,6 +312,7 @@ export default class RequestService { this.#substrateRequestHandler.resetWallet(); this.#evmRequestHandler.resetWallet(); this.#tonRequestHandler.resetWallet(); + this.#cardanoRequestHandler.resetWallet(); this.#metadataRequestHandler.resetWallet(); this.#connectWCRequestHandler.resetWallet(); this.#notSupportWCRequestHandler.resetWallet(); diff --git a/packages/extension-base/src/services/setting-service/constants.ts b/packages/extension-base/src/services/setting-service/constants.ts index fd0ea463768..f6041fca012 100644 --- a/packages/extension-base/src/services/setting-service/constants.ts +++ b/packages/extension-base/src/services/setting-service/constants.ts @@ -30,6 +30,9 @@ export const DEFAULT_NOTIFICATION_SETUP: NotificationSetup = { // isHideAnnouncement: false } }; +export const DEFAULT_ACKNOWLEDGED_MIGRATION_STATUS = false; +export const DEFAULT_UNIFIED_ACCOUNT_MIGRATION_IN_PROGRESS = false; +export const DEFAULT_UNIFIED_ACCOUNT_MIGRATION_IN_DONE = false; export const DEFAULT_SETTING: UiSettings = { language: DEFAULT_LANGUAGE, @@ -44,5 +47,8 @@ export const DEFAULT_SETTING: UiSettings = { timeAutoLock: DEFAULT_AUTO_LOCK_TIME, enableChainPatrol: DEFAULT_CHAIN_PATROL_ENABLE, notificationSetup: DEFAULT_NOTIFICATION_SETUP, + isAcknowledgedUnifiedAccountMigration: DEFAULT_ACKNOWLEDGED_MIGRATION_STATUS, + isUnifiedAccountMigrationInProgress: DEFAULT_UNIFIED_ACCOUNT_MIGRATION_IN_PROGRESS, + isUnifiedAccountMigrationDone: DEFAULT_UNIFIED_ACCOUNT_MIGRATION_IN_DONE, walletReference: '' }; diff --git a/packages/extension-base/src/services/storage-service/DatabaseService.ts b/packages/extension-base/src/services/storage-service/DatabaseService.ts index a91949bf059..ed42683340a 100644 --- a/packages/extension-base/src/services/storage-service/DatabaseService.ts +++ b/packages/extension-base/src/services/storage-service/DatabaseService.ts @@ -650,6 +650,10 @@ export default class DatabaseService { return this.stores.inappNotification.removeAccountNotifications(proxyId); } + public updateNotificationProxyId (proxyIds: string[], newProxyId: string, newName: string) { + return this.stores.inappNotification.updateNotificationProxyId(proxyIds, newProxyId, newName); + } + async exportDB () { const blob = await exportDB(this._db, { filter: (table, value, key) => { diff --git a/packages/extension-base/src/services/storage-service/db-stores/InappNotification.ts b/packages/extension-base/src/services/storage-service/db-stores/InappNotification.ts index c953f2e0010..258d30a3817 100644 --- a/packages/extension-base/src/services/storage-service/db-stores/InappNotification.ts +++ b/packages/extension-base/src/services/storage-service/db-stores/InappNotification.ts @@ -44,6 +44,15 @@ export default class InappNotificationStore extends BaseStore<_NotificationInfo> return filteredTable.toArray(); } + updateNotificationProxyId (proxyIds: string[], newProxyId: string, newName: string) { + this.table.where('proxyId') + .anyOfIgnoreCase(proxyIds) + .modify((item) => { + item.proxyId = newProxyId; + item.title = item.title.replace(/\[.*?\]/, `[${newName}]`); + }); + } + async cleanUpOldNotifications (overdueTime: number) { const currentTimestamp = Date.now(); diff --git a/packages/extension-base/src/services/swap-service/handler/chainflip-handler.ts b/packages/extension-base/src/services/swap-service/handler/chainflip-handler.ts index fc9d21f1a03..86b9c6c1ae4 100644 --- a/packages/extension-base/src/services/swap-service/handler/chainflip-handler.ts +++ b/packages/extension-base/src/services/swap-service/handler/chainflip-handler.ts @@ -10,7 +10,7 @@ import { ChainType, ExtrinsicType } from '@subwallet/extension-base/background/K import { _getChainflipEarlyValidationError } from '@subwallet/extension-base/core/logic-validation/swap'; import { BalanceService } from '@subwallet/extension-base/services/balance-service'; import { getERC20TransactionObject, getEVMTransactionObject } from '@subwallet/extension-base/services/balance-service/transfer/smart-contract'; -import { createTransferExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/token'; +import { createSubstrateExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/token'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenSlug, _getContractAddressOfToken, _isChainSubstrateCompatible, _isNativeToken, _isSmartContractToken } from '@subwallet/extension-base/services/chain-service/utils'; import { SwapBaseHandler, SwapBaseInterface } from '@subwallet/extension-base/services/swap-service/handler/base-handler'; @@ -430,7 +430,7 @@ export class ChainflipSwapHandler implements SwapBaseInterface { const substrateApi = await chainApi.isReady; - const [submittableExtrinsic] = await createTransferExtrinsic({ + const [submittableExtrinsic] = await createSubstrateExtrinsic({ from: address, networkKey: chainInfo.slug, substrateApi, diff --git a/packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts b/packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts index 58c8ba2180f..7c81bcab2fb 100644 --- a/packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts +++ b/packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts @@ -15,7 +15,7 @@ import { SubmittableExtrinsic } from '@polkadot/api/types'; import { BalanceService } from '../../balance-service'; import { getERC20TransactionObject, getEVMTransactionObject } from '../../balance-service/transfer/smart-contract'; -import { createTransferExtrinsic, getTransferMockTxFee } from '../../balance-service/transfer/token'; +import { createSubstrateExtrinsic, getTransferMockTxFee } from '../../balance-service/transfer/token'; import { ChainService } from '../../chain-service'; import { EvmApi } from '../../chain-service/handler/EvmApi'; import { _SubstrateApi } from '../../chain-service/types'; @@ -464,7 +464,7 @@ export class SimpleSwapHandler implements SwapBaseInterface { const chainApi = this.chainService.getSubstrateApi(chainInfo.slug); const substrateApi = await chainApi.isReady; - const [submittableExtrinsic] = await createTransferExtrinsic({ + const [submittableExtrinsic] = await createSubstrateExtrinsic({ from: address, networkKey: chainInfo.slug, substrateApi, diff --git a/packages/extension-base/src/services/transaction-service/balance.spec.ts b/packages/extension-base/src/services/transaction-service/balance.spec.ts index a3d229b4daa..db52f847297 100644 --- a/packages/extension-base/src/services/transaction-service/balance.spec.ts +++ b/packages/extension-base/src/services/transaction-service/balance.spec.ts @@ -4,7 +4,7 @@ import { ChainAssetMap, ChainInfoMap } from '@subwallet/chain-list'; import { _AssetType, _ChainAsset } from '@subwallet/chain-list/types'; import { getERC20TransactionObject, getEVMTransactionObject } from '@subwallet/extension-base/services/balance-service/transfer/smart-contract'; -import { createTransferExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/token'; +import { createSubstrateExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/token'; import { EvmChainHandler } from '@subwallet/extension-base/services/chain-service/handler/EvmChainHandler'; import { SubstrateChainHandler } from '@subwallet/extension-base/services/chain-service/handler/SubstrateChainHandler'; import { _getContractAddressOfToken, _isLocalToken, _isTokenEvmSmartContract } from '@subwallet/extension-base/services/chain-service/utils'; @@ -49,7 +49,7 @@ describe('test token transfer', () => { for (const asset of assets) { try { - const [extrinsic] = await createTransferExtrinsic({ + const [extrinsic] = await createSubstrateExtrinsic({ from: '5DnokDpMdNEH8cApsZoWQnjsggADXQmGWUb6q8ZhHeEwvncL', networkKey: networkKey, to: '5EhSb8uHkbPRF869wynQ4gh5V7B62YgkEQvMdk6tzHD9bK7b', diff --git a/packages/extension-base/src/services/transaction-service/helpers/index.ts b/packages/extension-base/src/services/transaction-service/helpers/index.ts index 427d24b7aee..06103db8e78 100644 --- a/packages/extension-base/src/services/transaction-service/helpers/index.ts +++ b/packages/extension-base/src/services/transaction-service/helpers/index.ts @@ -3,6 +3,7 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { CardanoTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/cardano-transfer'; import { TonTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; import { SWTransaction } from '@subwallet/extension-base/services/transaction-service/types'; @@ -29,6 +30,12 @@ export const isTonTransaction = (tx: SWTransaction['transaction']): tx is TonTra return Boolean(tonTransactionConfig.messagePayload) && tonTransactionConfig.seqno >= 0; }; +export const isCardanoTransaction = (tx: SWTransaction['transaction']): tx is CardanoTransactionConfig => { + const cardanoTransactionConfig = tx as CardanoTransactionConfig; + + return cardanoTransactionConfig.cardanoPayload !== null && cardanoTransactionConfig.cardanoPayload !== undefined; +}; + const typeName = (type: SWTransaction['extrinsicType']) => { switch (type) { case ExtrinsicType.TRANSFER_BALANCE: diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 2249f181183..0493d55560a 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -8,6 +8,7 @@ import { ALL_ACCOUNT_KEY, fetchBlockedConfigObjects, fetchLastestBlockedActionsA import { checkBalanceWithTransactionFee, checkSigningAccountForTransaction, checkSupportForAction, checkSupportForFeature, checkSupportForTransaction, estimateFeeForTransaction } from '@subwallet/extension-base/core/logic-validation/transfer'; import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; import { cellToBase64Str, externalMessage, getTransferCellPromise } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/utils'; +import { CardanoTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/cardano-transfer'; import { TonTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; import { _isPolygonChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/polygonBridge'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; @@ -18,7 +19,7 @@ import { ClaimAvailBridgeNotificationMetadata } from '@subwallet/extension-base/ import { EXTENSION_REQUEST_URL } from '@subwallet/extension-base/services/request-service/constants'; import { TRANSACTION_TIMEOUT } from '@subwallet/extension-base/services/transaction-service/constants'; import { parseLiquidStakingEvents, parseLiquidStakingFastUnstakeEvents, parseTransferEventLogs, parseXcmEventLogs } from '@subwallet/extension-base/services/transaction-service/event-parser'; -import { getBaseTransactionInfo, getTransactionId, isSubstrateTransaction, isTonTransaction } from '@subwallet/extension-base/services/transaction-service/helpers'; +import { getBaseTransactionInfo, getTransactionId, isCardanoTransaction, isSubstrateTransaction, isTonTransaction } from '@subwallet/extension-base/services/transaction-service/helpers'; import { SWTransaction, SWTransactionInput, SWTransactionResponse, TransactionEmitter, TransactionEventMap, TransactionEventResponse, ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types'; import { getExplorerLink, parseTransactionData } from '@subwallet/extension-base/services/transaction-service/utils'; import { isWalletConnectRequest } from '@subwallet/extension-base/services/wallet-connect-service/helpers'; @@ -130,10 +131,14 @@ export default class TransactionService { const evmApi = this.state.chainService.getEvmApi(chainInfo.slug); const tonApi = this.state.chainService.getTonApi(chainInfo.slug); - const isNoEvmApi = transaction && !isSubstrateTransaction(transaction) && !isTonTransaction(transaction) && !evmApi; // todo: should split isEvmTx && isNoEvmApi. Because other chains type also has no Evm Api + const cardanoApi = this.state.chainService.getCardanoApi(chainInfo.slug); + // todo: should split into isEvmTx && isNoEvmApi. Because other chains type also has no Evm Api. Same to all blockchain. + // todo: refactor check evmTransaction. + const isNoEvmApi = transaction && !isSubstrateTransaction(transaction) && !isTonTransaction(transaction) && !isCardanoTransaction(transaction) && !evmApi; const isNoTonApi = transaction && isTonTransaction(transaction) && !tonApi; + const isNoCardanoApi = transaction && isCardanoTransaction(transaction) && !cardanoApi; - if (isNoEvmApi || isNoTonApi) { + if (isNoEvmApi || isNoTonApi || isNoCardanoApi) { validationResponse.errors.push(new TransactionError(BasicTxErrorType.CHAIN_DISCONNECTED, undefined)); } @@ -264,7 +269,13 @@ export default class TransactionService { private async sendTransaction (transaction: SWTransaction): Promise<TransactionEmitter> { // Send Transaction - const emitter = await (transaction.chainType === 'substrate' ? this.signAndSendSubstrateTransaction(transaction) : transaction.chainType === 'evm' ? this.signAndSendEvmTransaction(transaction) : this.signAndSendTonTransaction(transaction)); + const emitter = await (transaction.chainType === 'substrate' + ? this.signAndSendSubstrateTransaction(transaction) + : transaction.chainType === 'evm' + ? this.signAndSendEvmTransaction(transaction) + : transaction.chainType === 'cardano' + ? this.signAndSendCardanoTransaction(transaction) + : this.signAndSendTonTransaction(transaction)); const { eventsHandler } = transaction; @@ -1228,7 +1239,7 @@ export default class TransactionService { }); }; - const tonTransactionConfig = transaction as TonTransactionConfig; + const tonTransactionConfig = transaction as TonTransactionConfig; // todo: is this same as payload? const seqno = tonTransactionConfig.seqno; const messages = tonTransactionConfig.messages; @@ -1281,6 +1292,77 @@ export default class TransactionService { return emitter; } + private signAndSendCardanoTransaction ({ chain, id, transaction, url }: SWTransaction): TransactionEmitter { + const emitter = new EventEmitter<TransactionEventMap>(); + const eventData: TransactionEventResponse = { + id, + errors: [], + warnings: [], + extrinsicHash: id + }; + + const transactionConfig = transaction as CardanoTransactionConfig; + const cardanoApi = this.state.chainService.getCardanoApi(chain); + + this.state.requestService.addConfirmationCardano(id, url || EXTENSION_REQUEST_URL, 'cardanoSendTransactionRequest', transactionConfig, {}) + .then(({ isApproved, payload }) => { + if (!isApproved) { + this.removeTransaction(id); + eventData.errors.push(new TransactionError(BasicTxErrorType.USER_REJECT_REQUEST)); + emitter.emit('error', eventData); + } else { + if (!payload) { + throw new Error('Failed to sign'); + } + + // Emit signed event + emitter.emit('signed', eventData); + + // Send transaction + this.handleTransactionTimeout(emitter, eventData); + emitter.emit('send', eventData); + + // send qua api + cardanoApi.sendCardanoTxReturnHash(payload) + .then((txHash) => { + if (!txHash) { + eventData.errors.push(new TransactionError(BasicTxErrorType.SEND_TRANSACTION_FAILED)); + emitter.emit('error', eventData); + } + + eventData.extrinsicHash = txHash; + emitter.emit('extrinsicHash', eventData); + + // todo: wait transaction by fetch txHash by API + cardanoApi.getStatusByTxHash(txHash, transactionConfig.cardanoTtlOffset).then((status) => { + if (!status) { + eventData.errors.push(new TransactionError(BasicTxErrorType.SEND_TRANSACTION_FAILED)); + emitter.emit('error', eventData); + } + + emitter.emit('success', eventData); + }) + .catch((e: Error) => { + eventData.errors.push(new TransactionError(BasicTxErrorType.SEND_TRANSACTION_FAILED, e.message)); + emitter.emit('error', eventData); + }); + }) + .catch((e: Error) => { + eventData.errors.push(new TransactionError(BasicTxErrorType.SEND_TRANSACTION_FAILED, e.message)); + emitter.emit('error', eventData); + }); + } + }) + .catch((e: Error) => { + this.removeTransaction(id); + eventData.errors.push(new TransactionError(BasicTxErrorType.UNABLE_TO_SIGN, t(e.message))); + + emitter.emit('error', eventData); + }); + + return emitter; + } + private handleTransactionTimeout (emitter: EventEmitter<TransactionEventMap>, eventData: TransactionEventResponse): void { const timeout = setTimeout(() => { const transaction = this.getTransaction(eventData.id); diff --git a/packages/extension-base/src/types/account/info/keyring.ts b/packages/extension-base/src/types/account/info/keyring.ts index 5891a41ca93..a092645719d 100644 --- a/packages/extension-base/src/types/account/info/keyring.ts +++ b/packages/extension-base/src/types/account/info/keyring.ts @@ -123,9 +123,20 @@ export enum AccountChainType { SUBSTRATE = 'substrate', ETHEREUM = 'ethereum', BITCOIN = 'bitcoin', - TON = 'ton' + TON = 'ton', + CARDANO = 'cardano' } +export const ACCOUNT_CHAIN_TYPE_ORDINAL_MAP: Record<string, number> = { + [AccountChainType.SUBSTRATE]: 1, + [AccountChainType.ETHEREUM]: 2, + [AccountChainType.TON]: 3, + [AccountChainType.CARDANO]: 4, + [AccountChainType.BITCOIN]: 5 +}; + +export const SUPPORTED_ACCOUNT_CHAIN_TYPES = ['substrate', 'ethereum', 'ton', 'cardano']; + export enum AccountActions { DERIVE = 'DERIVE', EXPORT_MNEMONIC = 'EXPORT_MNEMONIC', diff --git a/packages/extension-base/src/types/account/info/proxy.ts b/packages/extension-base/src/types/account/info/proxy.ts index b1757572368..12d905fc3df 100644 --- a/packages/extension-base/src/types/account/info/proxy.ts +++ b/packages/extension-base/src/types/account/info/proxy.ts @@ -20,6 +20,7 @@ export interface AccountProxyData { name: string; parentId?: string; suri?: string; + isMigrationDone?: boolean; } /** @@ -63,6 +64,7 @@ export interface AccountProxy extends AccountProxyData { children?: string[]; tokenTypes: _AssetType[]; accountActions: AccountActions[]; + isNeedMigrateUnifiedAccount?: boolean; } export type AccountProxyMap = Record<string, AccountProxy> diff --git a/packages/extension-base/src/types/balance/index.ts b/packages/extension-base/src/types/balance/index.ts index aa28f78d33b..586591362b3 100644 --- a/packages/extension-base/src/types/balance/index.ts +++ b/packages/extension-base/src/types/balance/index.ts @@ -3,7 +3,7 @@ import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; import { _BalanceMetadata, APIItemState, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; -import { _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; +import { _CardanoApi, _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; import { BN } from '@polkadot/util'; @@ -68,3 +68,7 @@ export interface SubscribeEvmPalletBalance extends SubscribeBasePalletBalance { export interface SubscribeTonPalletBalance extends SubscribeBasePalletBalance { tonApi: _TonApi; } + +export interface SusbcribeCardanoPalletBalance extends SubscribeBasePalletBalance { + cardanoApi: _CardanoApi; +} diff --git a/packages/extension-base/src/utils/account/analyze.ts b/packages/extension-base/src/utils/account/analyze.ts index b4dd0da10be..a9f18ea6937 100644 --- a/packages/extension-base/src/utils/account/analyze.ts +++ b/packages/extension-base/src/utils/account/analyze.ts @@ -27,7 +27,7 @@ const isStrValidWithAddress = (str: string, account: AddressDataJson, chainInfo: } else if (account.address.toLowerCase().includes(str) || reformated.toLowerCase().includes(str)) { return 'valid'; } - } else if (account.chainType === AccountChainType.TON) { + } else if (account.chainType === AccountChainType.TON || account.chainType === AccountChainType.CARDANO) { // todo: recheck for Cardano const isTestnet = chainInfo.isTestnet; const reformated = reformatAddress(account.address, isTestnet ? 0 : 1); diff --git a/packages/extension-base/src/utils/account/common.ts b/packages/extension-base/src/utils/account/common.ts index e871bf39a51..9e1d66f8ef4 100644 --- a/packages/extension-base/src/utils/account/common.ts +++ b/packages/extension-base/src/utils/account/common.ts @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { _ChainInfo } from '@subwallet/chain-list/types'; +import { ChainType } from '@subwallet/extension-base/background/KoniTypes'; import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; import { _chainInfoToChainType, _getChainSubstrateAddressPrefix } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountChainType } from '@subwallet/extension-base/types'; -import { getAccountChainType } from '@subwallet/extension-base/utils'; -import { decodeAddress, encodeAddress, getKeypairTypeByAddress, isAddress, isBitcoinAddress, isTonAddress } from '@subwallet/keyring'; +import { getAccountChainTypeFromKeypairType } from '@subwallet/extension-base/utils'; +import { decodeAddress, encodeAddress, getKeypairTypeByAddress, isAddress, isBitcoinAddress, isCardanoAddress, isTonAddress } from '@subwallet/keyring'; import { KeypairType } from '@subwallet/keyring/types'; import { ethereumEncode, isEthereumAddress } from '@polkadot/util-crypto'; @@ -49,12 +50,12 @@ export function reformatAddress (address: string, networkPrefix = 42, isEthereum } } -export const _reformatAddressWithChain = (address: string, chainInfo: _ChainInfo): string => { +export const _reformatAddressWithChain = (address: string, chainInfo: _ChainInfo): string => { // todo: check for cardano const chainType = _chainInfoToChainType(chainInfo); if (chainType === AccountChainType.SUBSTRATE) { return reformatAddress(address, _getChainSubstrateAddressPrefix(chainInfo)); - } else if (chainType === AccountChainType.TON) { + } else if (chainType === AccountChainType.TON || chainType === AccountChainType.CARDANO) { const isTestnet = chainInfo.isTestnet; return reformatAddress(address, isTestnet ? 0 : 1); @@ -66,38 +67,49 @@ export const _reformatAddressWithChain = (address: string, chainInfo: _ChainInfo export const getAccountChainTypeForAddress = (address: string): AccountChainType => { const type = getKeypairTypeByAddress(address); - return getAccountChainType(type); + return getAccountChainTypeFromKeypairType(type); }; -export function categoryAddresses (addresses: string[]): { - substrate: string[], - evm: string[], - ton: string[], - bitcoin: string[] -} { - const substrate: string[] = []; - const evm: string[] = []; - const ton: string[] = []; - const bitcoin: string[] = []; +interface AddressesByChainType { + [ChainType.SUBSTRATE]: string[], + [ChainType.EVM]: string[], + [ChainType.BITCOIN]: string[], + [ChainType.TON]: string[], + [ChainType.CARDANO]: string[] +} + +export function getAddressesByChainType (addresses: string[], chainTypes: ChainType[]): string[] { + const addressByChainTypeMap = getAddressesByChainTypeMap(addresses); + + return chainTypes.map((chainType) => { + return addressByChainTypeMap[chainType]; + }).flat(); // todo: recheck +} + +export function getAddressesByChainTypeMap (addresses: string[]): AddressesByChainType { + const addressByChainType: AddressesByChainType = { + substrate: [], + evm: [], + bitcoin: [], + ton: [], + cardano: [] + }; addresses.forEach((address) => { if (isEthereumAddress(address)) { - evm.push(address); + addressByChainType.evm.push(address); } else if (isTonAddress(address)) { - ton.push(address); + addressByChainType.ton.push(address); } else if (isBitcoinAddress(address)) { - bitcoin.push(address); + addressByChainType.bitcoin.push(address); + } else if (isCardanoAddress(address)) { + addressByChainType.cardano.push(address); } else { - substrate.push(address); + addressByChainType.substrate.push(address); } }); - return { - bitcoin, - evm, - substrate, - ton - }; + return addressByChainType; } export function quickFormatAddressToCompare (address?: string) { diff --git a/packages/extension-base/src/utils/account/derive/info/solo.ts b/packages/extension-base/src/utils/account/derive/info/solo.ts index dc6523a5ceb..6ec8a1d258a 100644 --- a/packages/extension-base/src/utils/account/derive/info/solo.ts +++ b/packages/extension-base/src/utils/account/derive/info/solo.ts @@ -9,7 +9,7 @@ import { t } from 'i18next'; import { assert } from '@polkadot/util'; -import { validateEvmDerivationPath, validateOtherSubstrateDerivationPath, validateSr25519DerivationPath, validateTonDerivationPath, validateUnifiedDerivationPath } from '../validate'; +import { validateCardanoDerivationPath, validateEvmDerivationPath, validateOtherSubstrateDerivationPath, validateSr25519DerivationPath, validateTonDerivationPath, validateUnifiedDerivationPath } from '../validate'; export const parseUnifiedSuriToDerivationPath = (suri: string, type: KeypairType): string => { const reg = /^\/\/(\d+)(\/\/\d+)?$/; @@ -25,12 +25,16 @@ export const parseUnifiedSuriToDerivationPath = (suri: string, type: KeypairType return `m/44'/60'/0'/0/${first}/${secondIndex}`; } else if (type === 'ton') { return `m/44'/607'/${first}'/${secondIndex}'`; + } else if (type === 'cardano') { + return `m/1852'/1815'/${first}'/${secondIndex}'`; } } else { if (type === 'ethereum') { return `m/44'/60'/0'/0/${first}`; } else if (type === 'ton') { return `m/44'/607'/${first}'`; + } else if (type === 'cardano') { + return `m/1852'/1815'/${first}'`; } } @@ -51,7 +55,9 @@ export const getSoloDerivationInfo = (type: KeypairType, metadata: AccountDerive ? validateEvmDerivationPath : type === 'ton' ? validateTonDerivationPath - : () => undefined; + : type === 'cardano' + ? validateCardanoDerivationPath + : () => undefined; const validateTypeRs = validateTypeFunc(derivePath); if (validateTypeRs) { @@ -114,7 +120,9 @@ export const getSoloDerivationInfo = (type: KeypairType, metadata: AccountDerive ? validateEvmDerivationPath : type === 'ton' ? validateTonDerivationPath - : () => undefined; + : type === 'cardano' + ? validateCardanoDerivationPath + : () => undefined; const validateTypeRs = validateTypeFunc(derivePath); if (validateTypeRs) { @@ -242,6 +250,7 @@ export const derivePair = (parentPair: KeyringPair, name: string, suri: string, const isEvm = EthereumKeypairTypes.includes(parentPair.type); const isTon = parentPair.type === 'ton'; + const isCardano = parentPair.type === 'cardano'; const meta = { name, @@ -255,8 +264,8 @@ export const derivePair = (parentPair: KeyringPair, name: string, suri: string, meta.tonContractVersion = parentPair.ton.contractVersion; } - if (derivationPath && (isEvm || isTon)) { - return isEvm ? parentPair.evm.deriveCustom(derivationPath, meta) : parentPair.ton.deriveCustom(derivationPath, meta); + if (derivationPath && (isEvm || isTon || isCardano)) { + return isEvm ? parentPair.evm.deriveCustom(derivationPath, meta) : isTon ? parentPair.ton.deriveCustom(derivationPath, meta) : parentPair.cardano.deriveCustom(derivationPath, meta); } else { return parentPair.substrate.derive(suri, meta); } diff --git a/packages/extension-base/src/utils/account/derive/validate.ts b/packages/extension-base/src/utils/account/derive/validate.ts index 9185f917cdc..04c36d9d666 100644 --- a/packages/extension-base/src/utils/account/derive/validate.ts +++ b/packages/extension-base/src/utils/account/derive/validate.ts @@ -125,6 +125,46 @@ export const validateTonDerivationPath = (raw: string): IDerivePathInfo_ | undef } }; +export const validateCardanoDerivationPath = (raw: string): IDerivePathInfo_ | undefined => { + const reg = /^m\/1852'\/1815'\/(\d+)'(\/\d+')?$/; + + if (raw.match(reg)) { + const [, firstIndex, secondData] = raw.match(reg) as string[]; + + const first = parseInt(firstIndex, 10); + const autoIndexes: number[] = [first]; + + let depth: number; + let suri = `//${first}`; + + if (first === 0) { + depth = 0; + } else { + depth = 1; + } + + if (secondData) { + const [, secondIndex] = secondData.match(/\/(\d+)/) as string[]; + + const second = parseInt(secondIndex, 10); + + autoIndexes.push(second); + depth = 2; + suri += `//${second}`; + } + + return { + depth, + type: 'cardano', + suri, + derivationPath: raw, + autoIndexes + }; + } else { + return undefined; + } +}; + export const validateSr25519DerivationPath = (raw: string): IDerivePathInfo_ | undefined => { const reg = /\/(\/?)([^/]+)/g; const parts = raw.match(reg); @@ -197,10 +237,12 @@ export const validateDerivationPath = (raw: string, type?: KeypairType): DeriveP return validateSr25519DerivationPath(raw); } else if (type === 'ed25519' || type === 'ecdsa') { return validateOtherSubstrateDerivationPath(raw, type); + } else if (type === 'cardano') { + return validateCardanoDerivationPath(raw); } else { return undefined; } } else { - return validateUnifiedDerivationPath(raw) || validateEvmDerivationPath(raw) || validateTonDerivationPath(raw) || validateSr25519DerivationPath(raw); + return validateUnifiedDerivationPath(raw) || validateEvmDerivationPath(raw) || validateTonDerivationPath(raw) || validateSr25519DerivationPath(raw) || validateCardanoDerivationPath(raw); } }; diff --git a/packages/extension-base/src/utils/account/transform.ts b/packages/extension-base/src/utils/account/transform.ts index d26f22c7977..6f1d6637bb3 100644 --- a/packages/extension-base/src/utils/account/transform.ts +++ b/packages/extension-base/src/utils/account/transform.ts @@ -5,9 +5,9 @@ import { _AssetType, _ChainInfo } from '@subwallet/chain-list/types'; import { ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { ALL_ACCOUNT_KEY, isProductionMode } from '@subwallet/extension-base/constants'; import { _getSubstrateGenesisHash } from '@subwallet/extension-base/services/chain-service/utils'; -import { AccountActions, AccountChainType, AccountJson, AccountMetadataData, AccountProxy, AccountProxyMap, AccountProxyStoreData, AccountProxyType, AccountSignMode, AddressJson, ModifyPairStoreData } from '@subwallet/extension-base/types'; +import { AccountActions, AccountChainType, AccountJson, AccountMetadataData, AccountProxy, AccountProxyMap, AccountProxyStoreData, AccountProxyType, AccountSignMode, AddressJson, ModifyPairStoreData, SUPPORTED_ACCOUNT_CHAIN_TYPES } from '@subwallet/extension-base/types'; import { getKeypairTypeByAddress, tonMnemonicToEntropy } from '@subwallet/keyring'; -import { BitcoinKeypairTypes, EthereumKeypairTypes, KeypairType, KeyringPair, KeyringPair$Meta, TonKeypairTypes } from '@subwallet/keyring/types'; +import { BitcoinKeypairTypes, CardanoKeypairTypes, EthereumKeypairTypes, KeypairType, KeyringPair, KeyringPair$Meta, TonKeypairTypes } from '@subwallet/keyring/types'; import { tonMnemonicValidate } from '@subwallet/keyring/utils'; import { SingleAddress, SubjectInfo } from '@subwallet/ui-keyring/observable/types'; @@ -44,7 +44,7 @@ export const createAccountProxyId = (_suri: string, derivationPath?: string) => return blake2AsHex(data, 256); }; -export const getAccountChainType = (type: KeypairType): AccountChainType => { +export const getAccountChainTypeFromKeypairType = (type: KeypairType): AccountChainType => { return type ? EthereumKeypairTypes.includes(type) ? AccountChainType.ETHEREUM @@ -52,10 +52,26 @@ export const getAccountChainType = (type: KeypairType): AccountChainType => { ? AccountChainType.TON : BitcoinKeypairTypes.includes(type) ? AccountChainType.BITCOIN - : AccountChainType.SUBSTRATE + : CardanoKeypairTypes.includes(type) + ? AccountChainType.CARDANO + : AccountChainType.SUBSTRATE : AccountChainType.SUBSTRATE; }; +export const getDefaultKeypairTypeFromAccountChainType = (type: AccountChainType): KeypairType => { + if (type === AccountChainType.ETHEREUM) { + return 'ethereum'; + } else if (type === AccountChainType.TON) { + return 'ton'; + } else if (type === AccountChainType.BITCOIN) { + return 'bitcoin-84'; + } else if (type === AccountChainType.CARDANO) { + return 'cardano'; + } else { + return 'sr25519'; + } +}; + export const getAccountSignMode = (address: string, _meta?: KeyringPair$Meta): AccountSignMode => { const meta = _meta as AccountMetadataData; @@ -92,6 +108,7 @@ export const getAccountActions = (signMode: AccountSignMode, networkType: Accoun const result: AccountActions[] = []; const meta = _meta as AccountMetadataData; + // todo: check this function for Cardano // JSON if (signMode === AccountSignMode.PASSWORD) { result.push(AccountActions.EXPORT_JSON); @@ -253,6 +270,10 @@ export const getAccountTransactionActions = (signMode: AccountSignMode, networkT return [ ...BASE_TRANSFER_ACTIONS ]; + case AccountChainType.CARDANO: + return [ + ...BASE_TRANSFER_ACTIONS + ]; } } else if (signMode === AccountSignMode.QR) { switch (networkType) { @@ -287,6 +308,8 @@ export const getAccountTransactionActions = (signMode: AccountSignMode, networkT ]; case AccountChainType.TON: return []; + case AccountChainType.CARDANO: + return []; } } else if (signMode === AccountSignMode.GENERIC_LEDGER) { switch (networkType) { @@ -316,6 +339,8 @@ export const getAccountTransactionActions = (signMode: AccountSignMode, networkT return [ ...BASE_TRANSFER_ACTIONS ]; + case AccountChainType.CARDANO: + return []; } } else if (signMode === AccountSignMode.LEGACY_LEDGER) { // Only for Substrate const result: ExtrinsicType[] = []; @@ -379,6 +404,8 @@ export const getAccountTokenTypes = (type: KeypairType): _AssetType[] => { case 'bitcoin-86': case 'bittest-86': return [_AssetType.NATIVE, _AssetType.RUNE, _AssetType.BRC20]; + case 'cardano': + return [_AssetType.NATIVE, _AssetType.CIP26]; default: return []; } @@ -403,7 +430,7 @@ export const getAccountTokenTypes = (type: KeypairType): _AssetType[] => { export const transformAccount = (address: string, _type?: KeypairType, meta?: KeyringPair$Meta, chainInfoMap?: Record<string, _ChainInfo>, parentAccount?: AccountJson): AccountJson => { const signMode = getAccountSignMode(address, meta); const type = _type || getKeypairTypeByAddress(address); - const chainType: AccountChainType = getAccountChainType(type); + const chainType: AccountChainType = getAccountChainTypeFromKeypairType(type); let specialChain: string | undefined; if (!chainInfoMap) { @@ -464,7 +491,7 @@ export const transformAccounts = (accounts: SubjectInfo): AccountJson[] => Objec export const transformAddress = (address: string, meta?: KeyringPair$Meta): AddressJson => { const type = getKeypairTypeByAddress(address); - const chainType: AccountChainType = getAccountChainType(type); + const chainType: AccountChainType = getAccountChainTypeFromKeypairType(type); return { address, @@ -539,6 +566,7 @@ export const _combineAccounts = (accounts: AccountJson[], modifyPairs: ModifyPai let tokenTypes: _AssetType[] = []; let accountActions: AccountActions[] = []; let specialChain: string | undefined; + let isNeedMigrateUnifiedAccount: boolean | undefined; if (value.accounts.length > 1) { accountType = AccountProxyType.UNIFIED; @@ -551,6 +579,10 @@ export const _combineAccounts = (accounts: AccountJson[], modifyPairs: ModifyPai return rs; }, new Set())); + if (chainTypes.length < SUPPORTED_ACCOUNT_CHAIN_TYPES.length) { + isNeedMigrateUnifiedAccount = true; + } + /* Account actions */ // Mnemonic @@ -584,6 +616,10 @@ export const _combineAccounts = (accounts: AccountJson[], modifyPairs: ModifyPai accountActions = accountActions.filter((action) => action !== AccountActions.DERIVE); } + if (chainTypes.length === 1 && accountActions.includes(AccountActions.EXPORT_MNEMONIC) && account.isMasterAccount && account.type !== 'ton-native') { + isNeedMigrateUnifiedAccount = true; + } + switch (account.signMode) { case AccountSignMode.GENERIC_LEDGER: case AccountSignMode.LEGACY_LEDGER: @@ -592,7 +628,7 @@ export const _combineAccounts = (accounts: AccountJson[], modifyPairs: ModifyPai } } - return [key, { ...value, accountType, chainTypes, specialChain, tokenTypes, accountActions }]; + return [key, { ...value, accountType, chainTypes, specialChain, tokenTypes, accountActions, isNeedMigrateUnifiedAccount }]; }) ); @@ -687,17 +723,10 @@ export const combineAllAccountProxy = (accountProxies: AccountProxy[]): AccountP const specialChain: string | undefined = accountProxies.length === 1 ? accountProxies[0].specialChain : undefined; for (const accountProxy of accountProxies) { - // Have 4 network types, but at the moment, we only support 3 network types - if (chainTypes.size === 3) { - break; - } - for (const chainType of accountProxy.chainTypes) { chainTypes.add(chainType); } - } - for (const accountProxy of accountProxies) { for (const tokenType of accountProxy.tokenTypes) { tokenTypes.add(tokenType); } diff --git a/packages/extension-base/src/utils/index.ts b/packages/extension-base/src/utils/index.ts index 63cccdd65da..eb209962ff9 100644 --- a/packages/extension-base/src/utils/index.ts +++ b/packages/extension-base/src/utils/index.ts @@ -5,10 +5,10 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { CrowdloanParaState, NetworkJson } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType } from '@subwallet/extension-base/background/types'; import { getRandomIpfsGateway, SUBWALLET_IPFS } from '@subwallet/extension-base/koni/api/nft/config'; -import { _isChainEvmCompatible, _isChainSubstrateCompatible, _isChainTonCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { _isChainCardanoCompatible, _isChainEvmCompatible, _isChainSubstrateCompatible, _isChainTonCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountJson } from '@subwallet/extension-base/types'; import { reformatAddress } from '@subwallet/extension-base/utils/account'; -import { decodeAddress, encodeAddress, isTonAddress } from '@subwallet/keyring'; +import { decodeAddress, encodeAddress, isCardanoAddress, isTonAddress } from '@subwallet/keyring'; import { t } from 'i18next'; import { assert, BN, hexToU8a, isHex } from '@polkadot/util'; @@ -304,8 +304,9 @@ export function isAddressAndChainCompatible (address: string, chain: _ChainInfo) const isEvmCompatible = isEthereumAddress(address) && _isChainEvmCompatible(chain); const isTonCompatible = isTonAddress(address) && _isChainTonCompatible(chain); const isSubstrateCompatible = !isEthereumAddress(address) && !isTonAddress(address) && _isChainSubstrateCompatible(chain); // todo: need isSubstrateAddress util function to check exactly + const isCardanoCompatible = isCardanoAddress(address) && _isChainCardanoCompatible(chain); - return isEvmCompatible || isSubstrateCompatible || isTonCompatible; + return isEvmCompatible || isSubstrateCompatible || isTonCompatible || isCardanoCompatible; } export function getDomainFromUrl (url: string): string { diff --git a/packages/extension-base/tsconfig.build.json b/packages/extension-base/tsconfig.build.json index 8d1ebf4e39d..b60a1d9756d 100644 --- a/packages/extension-base/tsconfig.build.json +++ b/packages/extension-base/tsconfig.build.json @@ -11,6 +11,8 @@ { "path": "../extension-dapp/tsconfig.build.json" }, { "path": "../extension-inject/tsconfig.build.json" + }, + { "path": "../subwallet-api-sdk/tsconfig.build.json" } ] } diff --git a/packages/extension-base/tsconfig.json b/packages/extension-base/tsconfig.json index 25e08522878..4451ed53483 100644 --- a/packages/extension-base/tsconfig.json +++ b/packages/extension-base/tsconfig.json @@ -8,6 +8,7 @@ "references": [ { "path": "../extension-chains" }, { "path": "../extension-dapp" }, - { "path": "../extension-inject" } + { "path": "../extension-inject" }, + { "path": "../subwallet-api-sdk" } ] } diff --git a/packages/extension-koni-ui/src/Popup/Account/ImportSeedPhrase.tsx b/packages/extension-koni-ui/src/Popup/Account/ImportSeedPhrase.tsx index 9efc5158ad8..f86b6f53350 100644 --- a/packages/extension-koni-ui/src/Popup/Account/ImportSeedPhrase.tsx +++ b/packages/extension-koni-ui/src/Popup/Account/ImportSeedPhrase.tsx @@ -36,7 +36,7 @@ interface FormState extends Record<`seed-phrase-${number}`, string> { } const words = wordlists.english; -const phraseNumberOptions = [12, 24]; +const phraseNumberOptions = [12, 15, 24]; const Component: React.FC<Props> = ({ className }: Props) => { useAutoNavigateToCreatePassword(); diff --git a/packages/extension-koni-ui/src/Popup/Account/RestoreJson/AccountRestoreJsonItem.tsx b/packages/extension-koni-ui/src/Popup/Account/RestoreJson/AccountRestoreJsonItem.tsx index febc2ac6623..22ae43cee0a 100644 --- a/packages/extension-koni-ui/src/Popup/Account/RestoreJson/AccountRestoreJsonItem.tsx +++ b/packages/extension-koni-ui/src/Popup/Account/RestoreJson/AccountRestoreJsonItem.tsx @@ -1,8 +1,8 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AccountChainType, AccountProxyExtra, AccountProxyType } from '@subwallet/extension-base/types'; -import { AccountProxyAvatar } from '@subwallet/extension-koni-ui/components'; +import { AccountProxyExtra, AccountProxyType } from '@subwallet/extension-base/types'; +import { AccountChainTypeLogos, AccountProxyAvatar } from '@subwallet/extension-koni-ui/components'; import { useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { Theme } from '@subwallet/extension-koni-ui/themes'; import { PhosphorIcon } from '@subwallet/extension-koni-ui/types'; @@ -47,16 +47,7 @@ function Component (props: _AccountCardItem): React.ReactElement<_AccountCardIte const { accountType, chainTypes, id: accountProxyId, name: accountName } = useMemo(() => accountProxy, [accountProxy]); const { t } = useTranslation(); const token = useContext<Theme>(ThemeContext as Context<Theme>).token; - const logoMap = useContext<Theme>(ThemeContext as Context<Theme>).logoMap; const disabledItem = useMemo(() => disabled || accountProxy.isExistAccount, [accountProxy.isExistAccount, disabled]); - const chainTypeLogoMap = useMemo(() => { - return { - [AccountChainType.SUBSTRATE]: logoMap.network.polkadot as string, - [AccountChainType.ETHEREUM]: logoMap.network.ethereum as string, - [AccountChainType.BITCOIN]: logoMap.network.bitcoin as string, - [AccountChainType.TON]: logoMap.network.ton as string - }; - }, [logoMap.network.bitcoin, logoMap.network.ethereum, logoMap.network.polkadot, logoMap.network.ton]); const _onSelect = useCallback(() => { onClick && onClick(accountProxyId || ''); }, @@ -150,20 +141,11 @@ function Component (props: _AccountCardItem): React.ReactElement<_AccountCardIte <div className='__item-center-part'> <div className={'middle-item__name-wrapper'}> <div className='__item-name'>{accountName}</div> - <div className='__item-chain-types'> - { - chainTypes.map((nt) => { - return ( - <img - alt='Network type' - className={'__item-chain-type-item'} - key={nt} - src={chainTypeLogoMap[nt]} - /> - ); - }) - } - </div> + + <AccountChainTypeLogos + chainTypes={chainTypes} + className={'__item-chain-type-logos'} + /> </div> </div> @@ -268,22 +250,8 @@ const AccountRestoreJsonItem = styled(Component)<_AccountCardItem>(({ theme }) = overflow: 'hidden', 'white-space': 'nowrap' }, - '.__item-chain-types': { - display: 'flex', - paddingTop: 2, - - '.__item-chain-type-item': { - display: 'block', - boxShadow: '-4px 0px 4px 0px rgba(0, 0, 0, 0.40)', - width: token.size, - height: token.size, - borderRadius: '100%', - marginLeft: -token.marginXXS - }, - - '.__item-chain-type-item:first-of-type': { - marginLeft: 0 - } + '.__item-chain-type-logos': { + minHeight: 20 }, '.__item-right-part': { marginLeft: 'auto', diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Cardano.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Cardano.tsx new file mode 100644 index 00000000000..e9efc5ed827 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Cardano.tsx @@ -0,0 +1,148 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ConfirmationDefinitionsCardano, ConfirmationResult, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { useNotification } from '@subwallet/extension-koni-ui/hooks'; +import useUnlockChecker from '@subwallet/extension-koni-ui/hooks/common/useUnlockChecker'; +import { completeConfirmationCardano } from '@subwallet/extension-koni-ui/messaging'; +import { CardanoSignatureSupportType, PhosphorIcon, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { removeTransactionPersist } from '@subwallet/extension-koni-ui/utils'; +import { Button, Icon } from '@subwallet/react-ui'; +import CN from 'classnames'; +import { CheckCircle, XCircle } from 'phosphor-react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +interface Props extends ThemeProps { + id: string; + type: CardanoSignatureSupportType; + payload: ConfirmationDefinitionsCardano[CardanoSignatureSupportType][0]; + extrinsicType?: ExtrinsicType; + txExpirationTime?: number; +} + +const handleConfirm = async (type: CardanoSignatureSupportType, id: string, payload: string) => { + return await completeConfirmationCardano(type, { + id, + isApproved: true, + payload + } as ConfirmationResult<string>); +}; + +const handleCancel = async (type: CardanoSignatureSupportType, id: string) => { + return await completeConfirmationCardano(type, { + id, + isApproved: false + } as ConfirmationResult<string>); +}; + +const Component: React.FC<Props> = (props: Props) => { + const { className, extrinsicType, id, txExpirationTime, type } = props; + + const { t } = useTranslation(); + const notify = useNotification(); + + const checkUnlock = useUnlockChecker(); + + const [showQuoteExpired, setShowQuoteExpired] = useState<boolean>(false); + + const [loading, setLoading] = useState(false); + + const approveIcon = useMemo((): PhosphorIcon => { + return CheckCircle; + }, []); + + // Handle buttons actions + const onCancel = useCallback(() => { + setLoading(true); + handleCancel(type, id).finally(() => { + setLoading(false); + }); + }, [id, type]); + + const onApprovePassword = useCallback(() => { + setLoading(true); + setTimeout(() => { + handleConfirm(type, id, '').finally(() => { + setLoading(false); + }); + }, 1000); + }, [id, type]); + + const onConfirm = useCallback(() => { + removeTransactionPersist(extrinsicType); + + if (txExpirationTime) { + const currentTime = +Date.now(); + + if (currentTime >= txExpirationTime) { + notify({ + message: t('Transaction expired'), + type: 'error' + }); + onCancel(); + } + } + + checkUnlock().then(() => { + onApprovePassword(); + }).catch(() => { + // Unlock is cancelled + }); + }, [extrinsicType, txExpirationTime, notify, t, onCancel, checkUnlock, onApprovePassword]); + + useEffect(() => { + let timer: NodeJS.Timer; + + if (txExpirationTime) { + timer = setInterval(() => { + if (Date.now() >= txExpirationTime) { + setShowQuoteExpired(true); + clearInterval(timer); + } + }, 1000); + } + + return () => { + clearInterval(timer); + }; + }, [txExpirationTime]); + + return ( + <div className={CN(className, 'confirmation-footer')}> + <Button + disabled={loading} + icon={( + <Icon + phosphorIcon={XCircle} + weight='fill' + /> + )} + onClick={onCancel} + schema={'secondary'} + > + {t('Cancel')} + </Button> + <Button + disabled={showQuoteExpired} + icon={( + <Icon + phosphorIcon={approveIcon} + weight='fill' + /> + )} + loading={loading} + onClick={onConfirm} + > + {t('Approve')} + </Button> + </div> + ); +}; + +const CardanoSignArea = styled(Component)<Props>(({ theme: { token } }: Props) => { + return {}; +}); + +export default CardanoSignArea; diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/index.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/index.tsx index b9ca17b77da..342f3039b65 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/index.tsx @@ -1,12 +1,13 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { ConfirmationDefinitions, ConfirmationDefinitionsTon, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { ConfirmationDefinitions, ConfirmationDefinitionsCardano, ConfirmationDefinitionsTon, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { SigningRequest } from '@subwallet/extension-base/background/types'; import { SWTransactionResult } from '@subwallet/extension-base/services/transaction-service/types'; import { SwapTxData } from '@subwallet/extension-base/types/swap'; import { AlertBox } from '@subwallet/extension-koni-ui/components'; import { useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import CardanoSignArea from '@subwallet/extension-koni-ui/Popup/Confirmations/parts/Sign/Cardano'; import TonSignArea from '@subwallet/extension-koni-ui/Popup/Confirmations/parts/Sign/Ton'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { ConfirmationQueueItem } from '@subwallet/extension-koni-ui/stores/base/RequestState'; @@ -172,6 +173,17 @@ const Component: React.FC<Props> = (props: Props) => { /> ) } + { + (type === 'cardanoSendTransactionRequest' || type === 'cardanoWatchTransactionRequest') && ( + <CardanoSignArea + extrinsicType={transaction.extrinsicType} + id={item.id} + payload={(item as ConfirmationDefinitionsCardano['cardanoSendTransactionRequest' | 'cardanoWatchTransactionRequest'][0])} + txExpirationTime={txExpirationTime} + type={type} + /> + ) + } </> ); }; diff --git a/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx b/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx index 8db65e97c38..29214c7dc78 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx @@ -9,10 +9,12 @@ import SwapLayout from '@subwallet/extension-koni-ui/Popup/Home/History/Detail/p import { ThemeProps, TransactionHistoryDisplayItem } from '@subwallet/extension-koni-ui/types'; import { formatHistoryDate, isAbleToShowFee, toShort } from '@subwallet/extension-koni-ui/utils'; import CN from 'classnames'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; +import { hexAddPrefix, isHex } from '@polkadot/util'; + import HistoryDetailAmount from './Amount'; import HistoryDetailFee from './Fee'; import HistoryDetailHeader from './Header'; @@ -28,6 +30,12 @@ const Component: React.FC<Props> = (props: Props) => { const { language } = useSelector((state) => state.settings); + const extrinsicHash = useMemo(() => { + const hash = data.extrinsicHash || ''; + + return isHex(hexAddPrefix(hash)) ? toShort(data.extrinsicHash, 8, 9) : '...'; + }, [data.extrinsicHash]); + if (data.type === ExtrinsicType.SWAP) { return ( <SwapLayout data={data} /> @@ -47,7 +55,7 @@ const Component: React.FC<Props> = (props: Props) => { statusName={t(HistoryStatusMap[data.status].name)} valueColorSchema={HistoryStatusMap[data.status].schema} /> - <MetaInfo.Default label={t('Extrinsic hash')}>{(data.extrinsicHash || '').startsWith('0x') ? toShort(data.extrinsicHash, 8, 9) : '...'}</MetaInfo.Default> + <MetaInfo.Default label={t('Extrinsic hash')}>{extrinsicHash}</MetaInfo.Default> <MetaInfo.Default label={t('Transaction time')}>{formatHistoryDate(data.time, language, 'detail')}</MetaInfo.Default> <HistoryDetailAmount data={data} /> diff --git a/packages/extension-koni-ui/src/Popup/Home/index.tsx b/packages/extension-koni-ui/src/Popup/Home/index.tsx index d397f660a7f..643e58729b5 100644 --- a/packages/extension-koni-ui/src/Popup/Home/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/index.tsx @@ -1,7 +1,7 @@ // Copyright 2019-2022 @polkadot/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { Layout, RemindUpgradeUnifiedAccount } from '@subwallet/extension-koni-ui/components'; +import { Layout } from '@subwallet/extension-koni-ui/components'; import { GlobalSearchTokenModal } from '@subwallet/extension-koni-ui/components/Modal/GlobalSearchTokenModal'; import RemindUpgradeFirefoxVersion from '@subwallet/extension-koni-ui/components/Modal/RemindUpgradeFirefoxVersion'; import { GeneralTermModal } from '@subwallet/extension-koni-ui/components/Modal/TermsAndConditions/GeneralTermModal'; @@ -131,7 +131,6 @@ function Component ({ className = '' }: Props): React.ReactElement<Props> { <Outlet /> <GeneralTermModal onOk={onAfterConfirmTermModal} /> <RemindUpgradeFirefoxVersion /> - <RemindUpgradeUnifiedAccount /> </Layout.Home> </div> </HomeContext.Provider> diff --git a/packages/extension-koni-ui/src/Popup/MigrateAccount/BriefView/index.tsx b/packages/extension-koni-ui/src/Popup/MigrateAccount/BriefView/index.tsx new file mode 100644 index 00000000000..f11a11533b2 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/MigrateAccount/BriefView/index.tsx @@ -0,0 +1,259 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { LoadingScreen } from '@subwallet/extension-koni-ui/components'; +import ContentGenerator from '@subwallet/extension-koni-ui/components/StaticContent/ContentGenerator'; +import { useFetchMarkdownContentData } from '@subwallet/extension-koni-ui/hooks'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { Button, Icon, PageIcon } from '@subwallet/react-ui'; +import CN from 'classnames'; +import { CheckCircle, Warning, XCircle } from 'phosphor-react'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +type Props = ThemeProps & { + isForcedMigration?: boolean; + onDismiss: VoidFunction; + onMigrateNow: VoidFunction; +}; + +type ContentDataType = { + content: string, + title: string +}; + +function Component ({ className = '', isForcedMigration, onDismiss, onMigrateNow }: Props) { + const { t } = useTranslation(); + const [contentData, setContentData] = useState<ContentDataType>({ + content: '', + title: '' + }); + const [isFetchingBriefContent, setIsFetchingBriefContent] = useState<boolean>(true); + + const fetchMarkdownContentData = useFetchMarkdownContentData(); + + useEffect(() => { + let sync = true; + + if (!isForcedMigration) { + setIsFetchingBriefContent(true); + + fetchMarkdownContentData<ContentDataType>('unified_account_migration_content', ['en']) + .then((data) => { + if (sync) { + setContentData(data); + setIsFetchingBriefContent(false); + } + }) + .catch((e) => console.log('fetch unified_account_migration_content error:', e)); + } + + return () => { + sync = false; + }; + }, [fetchMarkdownContentData, isForcedMigration]); + + useEffect(() => { + if (isForcedMigration) { + setIsFetchingBriefContent(false); + } + }, [isForcedMigration]); + + if (isFetchingBriefContent) { + return (<LoadingScreen />); + } + + return ( + <div className={CN(className, { + '-forced-migration': isForcedMigration + })} + > + <div className='__header-area'> + <div className='__view-title'> + { + !isForcedMigration + ? contentData.title + : t('Migration incomplete!') + } + </div> + </div> + + <div className='__body-area'> + { + !isForcedMigration && ( + <ContentGenerator + className={'__content-generator'} + content={contentData.content || ''} + /> + ) + } + + { + isForcedMigration && ( + <> + <div className={CN('__warning-icon')}> + <PageIcon + color='var(--page-icon-color)' + iconProps={{ + phosphorIcon: Warning + }} + /> + </div> + <div className={'__forced-migration-content'}> + <div className='__content-line'> + {t('Account migration is not yet complete. If this process remains incomplete, you will not be able to perform any action on SubWallet extension.')} + </div> + <div className='__content-line'> + {t('Make sure to complete the migration to avoid any potential issues with your accounts. Hit “Continue” to resume and complete the process. ')} + </div> + </div> + </> + ) + } + </div> + + <div className='__footer-area'> + { + !isForcedMigration && ( + <> + <Button + block={true} + icon={( + <Icon + phosphorIcon={XCircle} + weight='fill' + /> + )} + onClick={onDismiss} + schema={'secondary'} + > + {t('Cancel')} + </Button> + <Button + block={true} + icon={( + <Icon + phosphorIcon={CheckCircle} + weight='fill' + /> + )} + onClick={onMigrateNow} + > + {t('Migrate now')} + </Button> + </> + ) + } + + { + isForcedMigration && ( + <Button + block={true} + onClick={onMigrateNow} + > + {t('Continue')} + </Button> + ) + } + </div> + </div> + ); +} + +export const BriefView = styled(Component)<Props>(({ theme: { extendToken, token } }: Props) => { + return ({ + display: 'flex', + flexDirection: 'column', + height: '100%', + + '.__header-area': { + minHeight: 74, + padding: token.padding, + color: token.colorTextLight1, + borderBottom: '2px solid', + borderColor: token.colorBgSecondary, + display: 'flex', + alignItems: 'center' + }, + + '.__view-title': { + flex: 1, + overflow: 'hidden', + textOverflow: 'ellipsis', + 'white-space': 'nowrap', + fontSize: token.fontSizeHeading4, + lineHeight: token.lineHeightHeading4, + textAlign: 'center' + }, + + '.__body-area': { + flex: 1, + overflow: 'auto', + padding: token.padding, + paddingBottom: 0 + }, + + '.__footer-area': { + display: 'flex', + gap: token.sizeSM, + paddingLeft: token.padding, + paddingRight: token.padding, + paddingTop: token.padding, + paddingBottom: 32 + }, + + '.__forced-migration-content': { + textAlign: 'center', + color: token.colorTextLight4, + fontSize: token.fontSize, + lineHeight: token.lineHeight + }, + + '.__warning-icon': { + display: 'flex', + justifyContent: 'center', + marginBottom: 20, + '--page-icon-color': token.colorWarning + }, + + '.__content-line + .__content-line': { + marginTop: 20 + }, + + // content generator + + '.md-element + .md-element': { + marginTop: 20 + }, + + '.md-subtitle': { + fontSize: token.fontSizeHeading5, + lineHeight: token.lineHeightHeading5, + textAlign: 'center' + }, + + '.md-banner': { + maxWidth: '100%', + display: 'block' + }, + + '.md-text-center': { + textAlign: 'center' + }, + + '.md-p-tag': { + color: token.colorTextLight4, + fontSize: token.fontSize, + lineHeight: token.lineHeight + }, + + '&.-forced-migration': { + '.__body-area': { + paddingTop: 40, + paddingRight: 32, + paddingLeft: 32 + } + } + }); +}); diff --git a/packages/extension-koni-ui/src/Popup/MigrateAccount/EnterPasswordModal.tsx b/packages/extension-koni-ui/src/Popup/MigrateAccount/EnterPasswordModal.tsx new file mode 100644 index 00000000000..c0b135db540 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/MigrateAccount/EnterPasswordModal.tsx @@ -0,0 +1,157 @@ +// Copyright 2019-2022 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { FormCallbacks, ThemeProps, VoidFunction } from '@subwallet/extension-koni-ui/types'; +import { Button, Form, Icon, Input, SwModal } from '@subwallet/react-ui'; +import { ArrowCircleRight, XCircle } from 'phosphor-react'; +import React, { useCallback, useState } from 'react'; +import styled from 'styled-components'; + +import useFocusById from '../../hooks/form/useFocusById'; + +type Props = ThemeProps & { + onClose: VoidFunction; + onSubmit: (password: string) => Promise<void>; +} + +export const enterPasswordModalId = 'enterPasswordModalId'; + +const passwordInputId = 'enter-password-input-id'; + +enum FormFieldName { + PASSWORD = 'password' +} + +interface LoginFormState { + [FormFieldName.PASSWORD]: string; +} + +function Component ({ className = '', onClose, onSubmit }: Props): React.ReactElement<Props> { + const { t } = useTranslation(); + const [form] = Form.useForm<LoginFormState>(); + const [loading, setLoading] = useState(false); + + const onError = useCallback((error: string) => { + form.setFields([{ name: FormFieldName.PASSWORD, errors: [error] }]); + (document.getElementById(passwordInputId) as HTMLInputElement)?.select(); + }, [form]); + + const _onSubmit: FormCallbacks<LoginFormState>['onFinish'] = useCallback((values: LoginFormState) => { + setLoading(true); + setTimeout(() => { + onSubmit(values[FormFieldName.PASSWORD]) + .catch((e: Error) => { + onError(e.message); + }) + .finally(() => { + setLoading(false); + }); + }, 500); + }, [onError, onSubmit]); + + useFocusById(passwordInputId); + + return ( + <SwModal + className={className} + closable={false} + footer={( + <> + <Button + block={true} + disabled={loading} + icon={( + <Icon + phosphorIcon={XCircle} + weight='fill' + /> + )} + onClick={onClose} + schema={'secondary'} + > + {t('Cancel')} + </Button> + <Button + block={true} + disabled={loading} + icon={( + <Icon + phosphorIcon={ArrowCircleRight} + weight='fill' + /> + )} + loading={loading} + onClick={form.submit} + > + {t('Continue')} + </Button> + </> + )} + id={enterPasswordModalId} + title={t('Enter password')} + zIndex={9999} + > + <div className='__brief'> + {t('Enter your SubWallet password to continue')} + </div> + + <Form + form={form} + initialValues={{ [FormFieldName.PASSWORD]: '' }} + onFinish={_onSubmit} + > + <Form.Item + name={FormFieldName.PASSWORD} + rules={[ + { + message: t('Password is required'), + required: true + } + ]} + statusHelpAsTooltip={true} + > + <Input.Password + containerClassName='__password-input' + id={passwordInputId} + label={t('Password')} + placeholder={t('Enter password')} + suffix={<span />} + /> + </Form.Item> + </Form> + </SwModal> + ); +} + +export const EnterPasswordModal = styled(Component)<Props>(({ theme: { token } }: Props) => { + return ({ + '.ant-sw-modal-body': { + paddingBottom: 0 + }, + + '.ant-sw-modal-footer': { + borderTop: 0, + display: 'flex', + gap: token.sizeXXS + }, + + '.ant-form-item': { + marginBottom: 0 + }, + + '.__brief': { + textAlign: 'center', + fontSize: token.fontSize, + color: token.colorTextLight4, + lineHeight: token.lineHeight, + marginBottom: token.margin + }, + + '.__password-input': { + '.ant-input-prefix': { + display: 'none' + } + } + }); +}); diff --git a/packages/extension-koni-ui/src/Popup/MigrateAccount/SoloAccountMigrationView/ProcessViewItem.tsx b/packages/extension-koni-ui/src/Popup/MigrateAccount/SoloAccountMigrationView/ProcessViewItem.tsx new file mode 100644 index 00000000000..e552dce756a --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/MigrateAccount/SoloAccountMigrationView/ProcessViewItem.tsx @@ -0,0 +1,280 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { SoloAccountToBeMigrated } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountChainType, AccountProxyType, SUPPORTED_ACCOUNT_CHAIN_TYPES } from '@subwallet/extension-base/types'; +import { AccountChainTypeLogos, AccountProxyTypeTag } from '@subwallet/extension-koni-ui/components'; +import { useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { validateAccountName } from '@subwallet/extension-koni-ui/messaging'; +import { SoloAccountToBeMigratedItem } from '@subwallet/extension-koni-ui/Popup/MigrateAccount/SoloAccountMigrationView/SoloAccountToBeMigratedItem'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { noop, simpleCheckForm } from '@subwallet/extension-koni-ui/utils'; +import { Button, Form, Icon, Input } from '@subwallet/react-ui'; +import CN from 'classnames'; +import { CheckCircle, XCircle } from 'phosphor-react'; +import { Callbacks, FieldData, RuleObject } from 'rc-field-form/lib/interface'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +type Props = ThemeProps & { + currentProcessOrdinal: number; + totalProcessSteps: number; + currentSoloAccountToBeMigratedGroup: SoloAccountToBeMigrated[]; + onSkip: VoidFunction; + onApprove: (soloAccounts: SoloAccountToBeMigrated[], accountName: string) => Promise<void>; +}; + +interface FormProps { + name: string; +} + +function Component ({ className = '', currentProcessOrdinal, currentSoloAccountToBeMigratedGroup, onApprove, onSkip, totalProcessSteps }: Props) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm<FormProps>(); + const defaultValues = useMemo(() => ({ + name: '' + }), []); + const [isFormValid, setIsFormValid] = useState<boolean>(false); + + const headerContent = useMemo(() => { + return `${t('Accounts migrated')}: ${currentProcessOrdinal}/${totalProcessSteps}`; + }, [currentProcessOrdinal, t, totalProcessSteps]); + + const _onApprove = useCallback(() => { + const doApprove = () => { + setLoading(true); + + const { name } = form.getFieldsValue(); + + onApprove(currentSoloAccountToBeMigratedGroup, name.trim()) + .catch(console.error) + .finally(() => { + setLoading(false); + }); + }; + + form.validateFields(['name']).then(() => { + doApprove(); + }).catch(noop); + }, [currentSoloAccountToBeMigratedGroup, form, onApprove]); + + const accountNameValidator = useCallback(async (validate: RuleObject, value: string) => { + if (value) { + try { + const { isValid } = await validateAccountName({ name: value }); + + if (!isValid) { + return Promise.reject(t('Account name already in use')); + } + } catch (e) { + return Promise.reject(t('Account name invalid')); + } + } + + return Promise.resolve(); + }, [t]); + + const onFieldsChange: Callbacks<FormProps>['onFieldsChange'] = useCallback((changes: FieldData[], allFields: FieldData[]) => { + const { empty, error } = simpleCheckForm(allFields); + + setIsFormValid(!(error || empty)); + }, []); + + return ( + <div className={className}> + <div className='__header-area'> + {headerContent} + </div> + + <div className='__body-area'> + <div className='__brief'> + {t('Enter a name for this unified account to complete the migration')} + </div> + + <div className='__section-label'> + {t('Migrate from')} + </div> + + <div className='__account-list'> + { + currentSoloAccountToBeMigratedGroup.map((account) => ( + <SoloAccountToBeMigratedItem + className={'__account-item'} + key={account.address} + {...account} + /> + )) + } + </div> + + <div className='__section-label'> + {t('To')} + </div> + + <Form + form={form} + initialValues={defaultValues} + name='__form-container' + onFieldsChange={onFieldsChange} + > + <div className='__account-name-field-wrapper'> + <div className='__account-type-tag-wrapper'> + <AccountProxyTypeTag + className={'__account-type-tag'} + type={AccountProxyType.UNIFIED} + /> + </div> + + <Form.Item + className={CN('__account-name-field')} + name={'name'} + rules={[ + { + message: t('Account name is required'), + transform: (value: string) => value.trim(), + required: true + }, + { + validator: accountNameValidator + } + ]} + statusHelpAsTooltip={true} + > + <Input + className='__account-name-input' + disabled={loading} + label={t('Account name')} + placeholder={t('Enter the account name')} + suffix={( + <AccountChainTypeLogos + chainTypes={SUPPORTED_ACCOUNT_CHAIN_TYPES as AccountChainType[]} + className={'__chain-type-logos'} + /> + )} + /> + </Form.Item> + </div> + </Form> + </div> + + <div className='__footer-area'> + <Button + block={true} + disabled={loading} + icon={( + <Icon + phosphorIcon={XCircle} + weight='fill' + /> + )} + onClick={onSkip} + schema={'secondary'} + > + {t('Skip')} + </Button> + <Button + block={true} + disabled={!isFormValid || loading} + icon={( + <Icon + phosphorIcon={CheckCircle} + weight='fill' + /> + )} + loading={loading} + onClick={_onApprove} + > + {t('Approve')} + </Button> + </div> + </div> + ); +} + +export const ProcessViewItem = styled(Component)<Props>(({ theme: { extendToken, token } }: Props) => { + return ({ + display: 'flex', + flexDirection: 'column', + height: '100%', + + '.__header-area': { + paddingLeft: token.padding, + paddingRight: token.padding, + paddingTop: 14, + paddingBottom: 14, + fontSize: token.fontSizeHeading4, + lineHeight: token.lineHeightHeading4, + textAlign: 'center', + color: token.colorTextLight1 + }, + + '.__body-area': { + flex: 1, + overflow: 'auto', + padding: token.padding, + paddingBottom: 0 + }, + + '.__footer-area': { + display: 'flex', + gap: token.sizeSM, + paddingLeft: token.padding, + paddingRight: token.padding, + paddingTop: token.padding, + paddingBottom: 32 + }, + + '.__brief': { + textAlign: 'center', + fontSize: token.fontSize, + color: token.colorTextLight4, + lineHeight: token.lineHeight, + marginBottom: token.margin + }, + + '.__section-label': { + fontSize: token.fontSize, + color: token.colorTextLight4, + lineHeight: token.lineHeight, + marginBottom: token.marginXS, + fontWeight: token.headingFontWeight + }, + + '.__account-list': { + marginBottom: token.margin + }, + + '.__account-item + .__account-item': { + marginTop: token.marginXS + }, + + '.__account-name-field-wrapper': { + position: 'relative' + }, + + '.__account-name-field': { + marginBottom: 0 + }, + + '.__account-type-tag-wrapper': { + position: 'absolute', + zIndex: 1, + right: token.sizeSM, + top: token.sizeXS, + display: 'flex' + }, + + '.__account-type-tag': { + marginRight: 0 + }, + + '.__chain-type-logos': { + paddingRight: 10 + }, + + '.__account-name-input .ant-input-suffix': { + paddingLeft: token.paddingXS + } + }); +}); diff --git a/packages/extension-koni-ui/src/Popup/MigrateAccount/SoloAccountMigrationView/SoloAccountToBeMigratedItem.tsx b/packages/extension-koni-ui/src/Popup/MigrateAccount/SoloAccountMigrationView/SoloAccountToBeMigratedItem.tsx new file mode 100644 index 00000000000..bf04bbd6fc5 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/MigrateAccount/SoloAccountMigrationView/SoloAccountToBeMigratedItem.tsx @@ -0,0 +1,109 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { SoloAccountToBeMigrated } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountProxyAvatar } from '@subwallet/extension-koni-ui/components'; +import { Theme } from '@subwallet/extension-koni-ui/themes'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { getChainTypeLogoMap, toShort } from '@subwallet/extension-koni-ui/utils'; +import React, { Context, useContext, useMemo } from 'react'; +import styled, { ThemeContext } from 'styled-components'; + +type Props = ThemeProps & SoloAccountToBeMigrated; + +function Component ({ address, + chainType, + className, + name, + proxyId }: Props) { + const logoMap = useContext<Theme>(ThemeContext as Context<Theme>).logoMap; + + const chainTypeLogoMap = useMemo(() => { + return getChainTypeLogoMap(logoMap); + }, [logoMap]); + + return ( + <div className={className}> + <div className='__item-left-part'> + + <div className='__item-account-avatar-wrapper'> + <AccountProxyAvatar + className={'__item-account-avatar'} + size={28} + value={proxyId} + /> + + <img + alt='Network type' + className={'__item-chain-type-logo'} + src={chainTypeLogoMap[chainType]} + /> + </div> + </div> + + <div className='__item-center-part'> + <div className='__item-account-name'> + {name} + </div> + + <div className='__item-account-address'> + {toShort(address, 4, 5)} + </div> + </div> + </div> + ); +} + +export const SoloAccountToBeMigratedItem = styled(Component)<Props>(({ theme: { extendToken, token } }: Props) => { + return ({ + minHeight: 52, + background: token.colorBgSecondary, + paddingLeft: token.paddingSM, + paddingRight: token.paddingSM, + paddingTop: token.paddingXS, + paddingBottom: token.paddingXS, + borderRadius: token.borderRadiusLG, + alignItems: 'center', + display: 'flex', + flexDirection: 'row', + transition: `background ${token.motionDurationMid} ease-in-out`, + gap: 5, + 'white-space': 'nowrap', + + '.__item-account-avatar-wrapper': { + position: 'relative' + }, + + '.__item-chain-type-logo': { + position: 'absolute', + right: 0, + bottom: 0, + display: 'block', + width: token.size, + height: token.size + }, + + '.__item-center-part': { + display: 'flex', + overflow: 'hidden', + alignItems: 'flex-end', + gap: 5 + }, + + '.__item-account-name': { + fontSize: token.fontSize, + color: token.colorTextLight1, + lineHeight: token.lineHeight, + fontWeight: token.headingFontWeight, + textOverflow: 'ellipsis', + overflow: 'hidden', + flexShrink: 1 + }, + + '.__item-account-address': { + fontSize: token.fontSizeSM, + color: token.colorTextLight4, + lineHeight: 1.5 + } + }); +}); diff --git a/packages/extension-koni-ui/src/Popup/MigrateAccount/SoloAccountMigrationView/index.tsx b/packages/extension-koni-ui/src/Popup/MigrateAccount/SoloAccountMigrationView/index.tsx new file mode 100644 index 00000000000..a3845c6f23d --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/MigrateAccount/SoloAccountMigrationView/index.tsx @@ -0,0 +1,111 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { RequestMigrateSoloAccount, SoloAccountToBeMigrated } from '@subwallet/extension-base/background/KoniTypes'; +import { SESSION_TIMEOUT } from '@subwallet/extension-base/services/keyring-service/context/handlers/Migration'; +import { pingSession } from '@subwallet/extension-koni-ui/messaging/migrate-unified-account'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import React, { useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; + +import { ProcessViewItem } from './ProcessViewItem'; + +type Props = ThemeProps & { + soloAccountToBeMigratedGroups: SoloAccountToBeMigrated[][]; + onApprove: (request: RequestMigrateSoloAccount) => Promise<void>; + sessionId?: string; + onCompleteMigrationProcess: VoidFunction; +}; + +function Component ({ onApprove, onCompleteMigrationProcess, sessionId, soloAccountToBeMigratedGroups }: Props) { + const [currentProcessOrdinal, setCurrentProcessOrdinal] = useState<number>(1); + const [currentToBeMigratedGroupIndex, setCurrentToBeMigratedGroupIndex] = useState<number>(0); + const [totalProcessSteps, setTotalProcessSteps] = useState<number>(soloAccountToBeMigratedGroups.length); + + const performNextProcess = useCallback((increaseProcessOrdinal = true) => { + if (currentProcessOrdinal === totalProcessSteps) { + onCompleteMigrationProcess(); + + return; + } + + setCurrentToBeMigratedGroupIndex((prev) => prev + 1); + + if (increaseProcessOrdinal) { + setCurrentProcessOrdinal((prev) => prev + 1); + } + }, [currentProcessOrdinal, onCompleteMigrationProcess, totalProcessSteps]); + + const onSkip = useCallback(() => { + setTotalProcessSteps((prev) => { + if (prev > 0) { + return prev - 1; + } + + return prev; + }); + + performNextProcess(false); + }, [performNextProcess]); + + const _onApprove = useCallback(async (soloAccounts: SoloAccountToBeMigrated[], accountName: string) => { + if (!sessionId) { + return; + } + + await onApprove({ + soloAccounts, + sessionId, + accountName + }); + + performNextProcess(); + }, [onApprove, performNextProcess, sessionId]); + + const currentSoloAccountToBeMigratedGroup = soloAccountToBeMigratedGroups[currentToBeMigratedGroupIndex]; + + useEffect(() => { + // keep the session alive while in this view + + let timer: NodeJS.Timer; + + if (sessionId) { + const doPing = () => { + pingSession({ sessionId }).catch(console.error); + }; + + timer = setInterval(() => { + doPing(); + }, SESSION_TIMEOUT / 2); + + doPing(); + } + + return () => { + clearInterval(timer); + }; + }, [sessionId]); + + return ( + <> + { + !!currentSoloAccountToBeMigratedGroup && ( + <ProcessViewItem + currentProcessOrdinal={currentProcessOrdinal} + currentSoloAccountToBeMigratedGroup={currentSoloAccountToBeMigratedGroup} + key={`ProcessViewItem-${currentToBeMigratedGroupIndex}`} + onApprove={_onApprove} + onSkip={onSkip} + totalProcessSteps={totalProcessSteps} + /> + ) + } + </> + ); +} + +export const SoloAccountMigrationView = styled(Component)<Props>(({ theme: { extendToken, token } }: Props) => { + return ({ + + }); +}); diff --git a/packages/extension-koni-ui/src/Popup/MigrateAccount/SummaryView/ResultAccountProxyItem.tsx b/packages/extension-koni-ui/src/Popup/MigrateAccount/SummaryView/ResultAccountProxyItem.tsx new file mode 100644 index 00000000000..6a0443936e6 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/MigrateAccount/SummaryView/ResultAccountProxyItem.tsx @@ -0,0 +1,83 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { AccountChainType, SUPPORTED_ACCOUNT_CHAIN_TYPES } from '@subwallet/extension-base/types'; +import { AccountChainTypeLogos, AccountProxyAvatar } from '@subwallet/extension-koni-ui/components'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import React from 'react'; +import styled from 'styled-components'; + +export type ResultAccountProxyItemType = { + accountName: string; + accountProxyId: string; +}; + +type Props = ThemeProps & ResultAccountProxyItemType; + +function Component ({ accountName, + accountProxyId, + className }: Props) { + return ( + <div className={className}> + <div className='__item-account-avatar-wrapper'> + <AccountProxyAvatar + className={'__item-account-avatar'} + size={24} + value={accountProxyId} + /> + </div> + + <div className='__item-account-name'> + {accountName} + </div> + + <AccountChainTypeLogos + chainTypes={SUPPORTED_ACCOUNT_CHAIN_TYPES as AccountChainType[]} + className={'__item-chain-type-logos'} + /> + </div> + ); +} + +export const ResultAccountProxyItem = styled(Component)<Props>(({ theme: { extendToken, token } }: Props) => { + return ({ + minHeight: 52, + background: token.colorBgSecondary, + paddingLeft: token.paddingSM, + paddingRight: token.paddingSM, + paddingTop: token.paddingXS, + paddingBottom: token.paddingXS, + borderRadius: token.borderRadiusLG, + alignItems: 'center', + display: 'flex', + flexDirection: 'row', + transition: `background ${token.motionDurationMid} ease-in-out`, + gap: token.sizeSM, + 'white-space': 'nowrap', + + '.__item-account-avatar-wrapper': { + position: 'relative' + }, + + '.__item-chain-type-logos': { + + }, + + '.__item-account-name': { + flex: 1, + fontSize: token.fontSize, + color: token.colorTextLight1, + lineHeight: token.lineHeight, + fontWeight: token.headingFontWeight, + textOverflow: 'ellipsis', + overflow: 'hidden', + flexShrink: 1 + }, + + '.__item-account-address': { + fontSize: token.fontSizeSM, + color: token.colorTextLight4, + lineHeight: 1.5 + } + }); +}); diff --git a/packages/extension-koni-ui/src/Popup/MigrateAccount/SummaryView/ResultAccountProxyListModal.tsx b/packages/extension-koni-ui/src/Popup/MigrateAccount/SummaryView/ResultAccountProxyListModal.tsx new file mode 100644 index 00000000000..4c6816a5f70 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/MigrateAccount/SummaryView/ResultAccountProxyListModal.tsx @@ -0,0 +1,68 @@ +// Copyright 2019-2022 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { ResultAccountProxyItem, ResultAccountProxyItemType } from '@subwallet/extension-koni-ui/Popup/MigrateAccount/SummaryView/ResultAccountProxyItem'; +import { ThemeProps, VoidFunction } from '@subwallet/extension-koni-ui/types'; +import { Button, SwModal } from '@subwallet/react-ui'; +import React from 'react'; +import styled from 'styled-components'; + +type Props = ThemeProps & { + onClose: VoidFunction; + accountProxies: ResultAccountProxyItemType[]; +} + +export const resultAccountProxyListModal = 'resultAccountProxyListModal'; + +function Component ({ accountProxies, className = '', onClose }: Props): React.ReactElement<Props> { + const { t } = useTranslation(); + + return ( + <SwModal + className={className} + footer={( + <> + <Button + block={true} + onClick={onClose} + > + {t('Close')} + </Button> + </> + )} + id={resultAccountProxyListModal} + onCancel={onClose} + title={t('Migrated account list')} + zIndex={9999} + > + { + accountProxies.map((ap) => ( + <ResultAccountProxyItem + className={'__account-item'} + key={ap.accountProxyId} + {...ap} + /> + )) + } + </SwModal> + ); +} + +export const ResultAccountProxyListModal = styled(Component)<Props>(({ theme: { token } }: Props) => { + return ({ + '.ant-sw-modal-body': { + paddingBottom: 0 + }, + + '.ant-sw-modal-footer': { + borderTop: 0, + display: 'flex', + gap: token.sizeXXS + }, + + '.__account-item + .__account-item': { + marginTop: token.marginXS + } + }); +}); diff --git a/packages/extension-koni-ui/src/Popup/MigrateAccount/SummaryView/index.tsx b/packages/extension-koni-ui/src/Popup/MigrateAccount/SummaryView/index.tsx new file mode 100644 index 00000000000..35385e8f7f2 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/MigrateAccount/SummaryView/index.tsx @@ -0,0 +1,313 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { detectTranslate } from '@subwallet/extension-base/utils'; +import { useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { pingUnifiedAccountMigrationDone } from '@subwallet/extension-koni-ui/messaging'; +import { ResultAccountProxyItem, ResultAccountProxyItemType } from '@subwallet/extension-koni-ui/Popup/MigrateAccount/SummaryView/ResultAccountProxyItem'; +import { ResultAccountProxyListModal, resultAccountProxyListModal } from '@subwallet/extension-koni-ui/Popup/MigrateAccount/SummaryView/ResultAccountProxyListModal'; +import { RootState } from '@subwallet/extension-koni-ui/stores'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { Button, Icon, ModalContext, PageIcon } from '@subwallet/react-ui'; +import CN from 'classnames'; +import { CheckCircle } from 'phosphor-react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { Trans } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; + +type Props = ThemeProps & { + resultProxyIds: string[]; + onClickFinish: VoidFunction; +}; + +function Component ({ className = '', onClickFinish, resultProxyIds }: Props) { + const { t } = useTranslation(); + const { activeModal, inactiveModal } = useContext(ModalContext); + const [isAccountListModalOpen, setIsAccountListModalOpen] = useState<boolean>(false); + const accountProxies = useSelector((root: RootState) => root.accountState.accountProxies); + + const accountProxyNameMapById = useMemo(() => { + const result: Record<string, string> = {}; + + accountProxies.forEach((ap) => { + result[ap.id] = ap.name; + }); + + return result; + }, [accountProxies]); + + const resultAccountProxies = useMemo<ResultAccountProxyItemType[]>(() => { + return resultProxyIds.map((id) => ({ + accountName: accountProxyNameMapById[id] || '', + accountProxyId: id + })); + }, [accountProxyNameMapById, resultProxyIds]); + + const onOpenAccountListModal = useCallback(() => { + setIsAccountListModalOpen(true); + activeModal(resultAccountProxyListModal); + }, [activeModal]); + + const onCloseAccountListModal = useCallback(() => { + inactiveModal(resultAccountProxyListModal); + setIsAccountListModalOpen(false); + }, [inactiveModal]); + + const showAccountListModalTrigger = resultAccountProxies.length > 2; + + const getAccountListModalTriggerLabel = () => { + if (resultAccountProxies.length === 3) { + return t('And 1 other'); + } + + return t('And {{number}} others', { replace: { number: resultAccountProxies.length - 2 } }); + }; + + const hasAnyAccountToMigrate = !!resultAccountProxies.length; + + useEffect(() => { + // notice to background that account migration is done + pingUnifiedAccountMigrationDone().catch(console.error); + }, []); + + return ( + <> + <div className={CN(className, { + '-no-account': !hasAnyAccountToMigrate + })} + > + <div className='__header-area'> + {t('Finish')} + </div> + + <div className='__body-area'> + <div className='__page-icon'> + <PageIcon + color='var(--page-icon-color)' + iconProps={{ + weight: 'fill', + phosphorIcon: CheckCircle + }} + /> + </div> + + <div className='__content-title'> + {t('All done!')} + </div> + + { + !hasAnyAccountToMigrate && ( + <div className='__brief'> + <Trans + components={{ + guide: ( + <a + className='__link' + href={'https://docs.subwallet.app/main/extension-user-guide/account-management/migrate-solo-accounts-to-unified-accounts'} + target='__blank' + /> + ) + }} + i18nKey={detectTranslate('All eligible accounts have been migrated. Review <guide>our guide</guide> to learn more about migration eligibility & process')} + /> + </div> + ) + } + + { + hasAnyAccountToMigrate && ( + <> + <div className='__brief'> + { + resultAccountProxies.length > 1 + ? ( + <Trans + components={{ + br: (<br />), + highlight: ( + <span + className='__highlight' + /> + ) + }} + i18nKey={detectTranslate('You have successfully migrated to <br/> <highlight>{{number}} unified accounts</highlight>')} + values={{ number: `${resultAccountProxies.length}`.padStart(2, '0') }} + /> + ) + : ( + <Trans + components={{ + br: (<br />), + highlight: ( + <span + className='__highlight' + /> + ) + }} + i18nKey={detectTranslate('You have successfully migrated to <br/> <highlight>{{number}} unified account</highlight>')} + values={{ number: `${resultAccountProxies.length}`.padStart(2, '0') }} + /> + ) + } + </div> + + <div className='__account-list-container'> + { + resultAccountProxies.slice(0, 2).map((ap) => ( + <ResultAccountProxyItem + className={'__account-item'} + key={ap.accountProxyId} + {...ap} + /> + )) + } + </div> + + { + showAccountListModalTrigger && ( + <div className='__account-list-modal-trigger-wrapper'> + <button + className={'__account-list-modal-trigger'} + onClick={onOpenAccountListModal} + > + {getAccountListModalTriggerLabel()} + </button> + </div> + ) + } + </> + ) + } + </div> + + <div className='__footer-area'> + <Button + block={true} + icon={ + hasAnyAccountToMigrate + ? ( + <Icon + phosphorIcon={CheckCircle} + weight='fill' + /> + ) + : undefined + } + onClick={onClickFinish} + > + {hasAnyAccountToMigrate ? t('Finish') : t('Back to home')} + </Button> + </div> + </div> + + { + isAccountListModalOpen && showAccountListModalTrigger && ( + <ResultAccountProxyListModal + accountProxies={resultAccountProxies} + onClose={onCloseAccountListModal} + /> + ) + } + </> + ); +} + +export const SummaryView = styled(Component)<Props>(({ theme: { extendToken, token } }: Props) => { + return ({ + display: 'flex', + flexDirection: 'column', + height: '100%', + + '.__header-area': { + paddingLeft: token.padding, + paddingRight: token.padding, + paddingTop: 14, + paddingBottom: 14, + fontSize: token.fontSizeHeading4, + lineHeight: token.lineHeightHeading4, + textAlign: 'center', + color: token.colorTextLight1 + }, + + '.__body-area': { + flex: 1, + overflow: 'auto', + padding: token.padding, + paddingBottom: 0 + }, + + '.__footer-area': { + display: 'flex', + gap: token.sizeSM, + paddingLeft: token.padding, + paddingRight: token.padding, + paddingTop: token.padding, + paddingBottom: 32 + }, + + '.__page-icon': { + display: 'flex', + justifyContent: 'center', + marginBottom: token.margin, + '--page-icon-color': token.colorSecondary + }, + + '.__content-title': { + color: token.colorTextLight2, + fontSize: token.fontSizeHeading3, + lineHeight: token.lineHeightHeading3, + textAlign: 'center', + marginBottom: token.margin + }, + + '.__brief': { + color: token.colorTextLight3, + fontSize: token.fontSizeHeading5, + lineHeight: token.lineHeightHeading5, + textAlign: 'center', + + '.__link': { + color: token.colorPrimary + }, + + '.__highlight': { + color: token.colorTextLight1 + } + }, + + '.__brief + .__account-list-container': { + marginTop: 32 + }, + + '.__account-item + .__account-item': { + marginTop: token.marginXS + }, + + '.__account-list-modal-trigger-wrapper': { + marginTop: token.marginXS, + display: 'flex', + justifyContent: 'center' + }, + + '.__account-list-modal-trigger': { + padding: 0, + cursor: 'pointer', + backgroundColor: 'transparent', + paddingLeft: token.padding, + paddingRight: token.padding, + border: 0, + fontSize: token.fontSize, + lineHeight: token.lineHeight, + color: token.colorTextLight4 + }, + + '&.-no-account': { + '.__body-area': { + paddingTop: 40, + paddingRight: 32, + paddingLeft: 32 + } + } + }); +}); diff --git a/packages/extension-koni-ui/src/Popup/MigrateAccount/index.tsx b/packages/extension-koni-ui/src/Popup/MigrateAccount/index.tsx new file mode 100644 index 00000000000..258c763e138 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/MigrateAccount/index.tsx @@ -0,0 +1,174 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { RequestMigrateSoloAccount, SoloAccountToBeMigrated } from '@subwallet/extension-base/background/KoniTypes'; +import { hasAnyAccountForMigration } from '@subwallet/extension-base/services/keyring-service/utils'; +import { useDefaultNavigate, useIsPopup } from '@subwallet/extension-koni-ui/hooks'; +import { saveMigrationAcknowledgedStatus } from '@subwallet/extension-koni-ui/messaging'; +import { migrateSoloAccount, migrateUnifiedAndFetchEligibleSoloAccounts } from '@subwallet/extension-koni-ui/messaging/migrate-unified-account'; +import { BriefView } from '@subwallet/extension-koni-ui/Popup/MigrateAccount/BriefView'; +import { RootState } from '@subwallet/extension-koni-ui/stores'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { ModalContext } from '@subwallet/react-ui'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import styled from 'styled-components'; + +import { EnterPasswordModal, enterPasswordModalId } from './EnterPasswordModal'; +import { SoloAccountMigrationView } from './SoloAccountMigrationView'; +import { SummaryView } from './SummaryView'; + +type Props = ThemeProps; + +export enum ScreenView { + BRIEF= 'brief', + SOLO_ACCOUNT_MIGRATION= 'solo-account-migration', + SUMMARY='summary' +} + +function Component ({ className = '' }: Props) { + const [searchParams] = useSearchParams(); + const isMigrationNotion = searchParams.has('is-notion'); + const isForcedMigration = searchParams.has('is-forced-migration'); + const [currentScreenView, setCurrentScreenView] = useState<ScreenView>(ScreenView.BRIEF); + const [isPasswordModalOpen, setIsPasswordModalOpen] = useState<boolean>(false); + const { activeModal, inactiveModal } = useContext(ModalContext); + const { goHome } = useDefaultNavigate(); + const navigate = useNavigate(); + const [sessionId, setSessionId] = useState<string | undefined>(); + const [resultProxyIds, setResultProxyIds] = useState<string[]>([]); + const [soloAccountToBeMigratedGroups, setSoloAccountToBeMigratedGroups] = useState<SoloAccountToBeMigrated[][]>([]); + const isAcknowledgedUnifiedAccountMigration = useSelector((state: RootState) => state.settings.isAcknowledgedUnifiedAccountMigration); + const isPopup = useIsPopup(); + + const accountProxies = useSelector((root: RootState) => root.accountState.accountProxies); + + const onClosePasswordModal = useCallback(() => { + inactiveModal(enterPasswordModalId); + setIsPasswordModalOpen(false); + }, [inactiveModal]); + + const onOpenPasswordModal = useCallback(() => { + setIsPasswordModalOpen(true); + activeModal(enterPasswordModalId); + }, [activeModal]); + + const onInteractAction = useCallback(() => { + if (isMigrationNotion && !isAcknowledgedUnifiedAccountMigration) { + // flag that user acknowledge the migration + saveMigrationAcknowledgedStatus({ isAcknowledgedUnifiedAccountMigration: true }).catch(console.error); + } + + // for now, do nothing + }, [isAcknowledgedUnifiedAccountMigration, isMigrationNotion]); + + const onClickDismiss = useCallback(() => { + onInteractAction(); + + // close this screen + isMigrationNotion ? goHome() : navigate('/settings/account-settings'); + }, [goHome, isMigrationNotion, navigate, onInteractAction]); + + const onClickMigrateNow = useCallback(() => { + onInteractAction(); + + if (!hasAnyAccountForMigration(accountProxies)) { + setCurrentScreenView(ScreenView.SUMMARY); + } else { + onOpenPasswordModal(); + } + }, [accountProxies, onInteractAction, onOpenPasswordModal]); + + const onSubmitPassword = useCallback(async (password: string) => { + // migrate all account + // open migrate solo chain accounts + + const { sessionId, soloAccounts } = await migrateUnifiedAndFetchEligibleSoloAccounts({ password }); + + const soloAccountGroups = Object.values(soloAccounts); + + if (soloAccountGroups.length) { + setSessionId(sessionId); + setSoloAccountToBeMigratedGroups(soloAccountGroups); + + setCurrentScreenView(ScreenView.SOLO_ACCOUNT_MIGRATION); + } else { + setCurrentScreenView(ScreenView.SUMMARY); + } + + onClosePasswordModal(); + }, [onClosePasswordModal]); + + const onApproveSoloAccountMigration = useCallback(async (request: RequestMigrateSoloAccount) => { + try { + const { migratedUnifiedAccountId } = await migrateSoloAccount(request); + + setResultProxyIds((prev) => { + return [...prev, migratedUnifiedAccountId]; + }); + } catch (e) { + console.log('onApproveSoloAccountMigration error:', e); + } + }, []); + + const onCompleteSoloAccountsMigrationProcess = useCallback(() => { + setCurrentScreenView(ScreenView.SUMMARY); + setSessionId(undefined); + }, []); + + const onClickFinish = useCallback(() => { + goHome(); + }, [goHome]); + + useEffect(() => { + if (!isPopup) { + goHome(); + } + }, [goHome, isPopup]); + + return ( + <> + {currentScreenView === ScreenView.BRIEF && ( + <BriefView + isForcedMigration={isForcedMigration} + onDismiss={onClickDismiss} + onMigrateNow={onClickMigrateNow} + /> + )} + + {currentScreenView === ScreenView.SOLO_ACCOUNT_MIGRATION && ( + <SoloAccountMigrationView + onApprove={onApproveSoloAccountMigration} + onCompleteMigrationProcess={onCompleteSoloAccountsMigrationProcess} + sessionId={sessionId} + soloAccountToBeMigratedGroups={soloAccountToBeMigratedGroups} + /> + )} + + {currentScreenView === ScreenView.SUMMARY && ( + <SummaryView + onClickFinish={onClickFinish} + resultProxyIds={resultProxyIds} + /> + )} + + { + isPasswordModalOpen && ( + <EnterPasswordModal + onClose={onClosePasswordModal} + onSubmit={onSubmitPassword} + /> + ) + } + </> + ); +} + +const MigrateAccount = styled(Component)<Props>(({ theme: { extendToken, token } }: Props) => { + return ({ + + }); +}); + +export default MigrateAccount; diff --git a/packages/extension-koni-ui/src/Popup/Root.tsx b/packages/extension-koni-ui/src/Popup/Root.tsx index 27df10ddb25..2e02fa6c5ac 100644 --- a/packages/extension-koni-ui/src/Popup/Root.tsx +++ b/packages/extension-koni-ui/src/Popup/Root.tsx @@ -10,7 +10,7 @@ import { CURRENT_PAGE, TRANSACTION_STORAGES } from '@subwallet/extension-koni-ui import { DEFAULT_ROUTER_PATH } from '@subwallet/extension-koni-ui/constants/router'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; import { usePredefinedModal, WalletModalContextProvider } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import { useGetCurrentPage, useSubscribeLanguage } from '@subwallet/extension-koni-ui/hooks'; +import { useGetCurrentPage, useIsPopup, useSubscribeLanguage } from '@subwallet/extension-koni-ui/hooks'; import useNotification from '@subwallet/extension-koni-ui/hooks/common/useNotification'; import useUILock from '@subwallet/extension-koni-ui/hooks/common/useUILock'; import { subscribeNotifications } from '@subwallet/extension-koni-ui/messaging'; @@ -34,6 +34,8 @@ export const RouteState = { const welcomeUrl = '/welcome'; const tokenUrl = '/home/tokens'; +const migrateAccountNotionUrl = '/migrate-account?is-notion=true'; +const forcedAccountMigrationUrl = '/migrate-account?is-forced-migration=true'; const loginUrl = '/keyring/login'; const phishingUrl = '/phishing-page-detected'; const mv3MigrationUrl = '/mv3-migration'; @@ -90,7 +92,7 @@ function DefaultRoute ({ children }: { children: React.ReactNode }): React.React const [dataLoaded, setDataLoaded] = useState(false); const initDataRef = useRef<Promise<boolean>>(dataContext.awaitStores(['accountState', 'chainStore', 'assetRegistry', 'requestState', 'settings', 'mantaPay'])); const currentPage = useGetCurrentPage(); - const [, setStorage] = useLocalStorage<string>(CURRENT_PAGE, DEFAULT_ROUTER_PATH); + const [, setCurrentPage] = useLocalStorage<string>(CURRENT_PAGE, DEFAULT_ROUTER_PATH); const firstRender = useRef(true); useSubscribeLanguage(); @@ -98,14 +100,17 @@ function DefaultRoute ({ children }: { children: React.ReactNode }): React.React const { unlockType } = useSelector((state: RootState) => state.settings); const { hasConfirmations, hasInternalConfirmations } = useSelector((state: RootState) => state.requestState); const { accounts, currentAccount, hasMasterPassword, isLocked } = useSelector((state: RootState) => state.accountState); + const isAcknowledgedUnifiedAccountMigration = useSelector((state: RootState) => state.settings.isAcknowledgedUnifiedAccountMigration); + const isUnifiedAccountMigrationInProgress = useSelector((state: RootState) => state.settings.isUnifiedAccountMigrationInProgress); const [initAccount, setInitAccount] = useState(currentAccount); const noAccount = useMemo(() => isNoAccount(accounts), [accounts]); const { isUILocked } = useUILock(); const needUnlock = isUILocked || (isLocked && unlockType === WalletUnlockType.ALWAYS_REQUIRED); const [shouldRedirect, setShouldRedirect] = useState(false); const navigate = useNavigate(); + const isPopup = useIsPopup(); - const needMigrate = useMemo( + const needMasterPasswordMigration = useMemo( () => !!accounts .filter((acc) => acc.address !== ALL_ACCOUNT_KEY && !acc.isExternal && !acc.isInjected && !acc.pendingMigrate) .filter((acc) => !acc.isMasterPassword) @@ -113,49 +118,17 @@ function DefaultRoute ({ children }: { children: React.ReactNode }): React.React , [accounts] ); - useEffect(() => { - initDataRef.current.then(() => { - setDataLoaded(true); - }).catch(console.error); - }, []); - - useEffect(() => { - let cancel = false; - let lastNotifyTime = new Date().getTime(); - - subscribeNotifications((rs) => { - rs.sort((a, b) => a.id - b.id) - .forEach(({ action, id, message, title, type }) => { - if (!cancel && id > lastNotifyTime) { - const notificationItem: NotificationProps = { message: title || message, type }; - - if (action?.url) { - notificationItem.onClick = () => { - window.open(action.url); - }; - } - - notify(notificationItem); - lastNotifyTime = id; - } - }); - }).catch(console.error); - - return () => { - cancel = true; - }; - }, [notify]); + const activePriorityPath = useMemo(() => { + if (isPopup && !isAcknowledgedUnifiedAccountMigration) { + return migrateAccountNotionUrl; + } - // Update goBack number - useEffect(() => { - if (location.pathname === RouteState.lastPathName) { - RouteState.prevDifferentPathNum -= 1; - } else { - RouteState.prevDifferentPathNum = -1; + if (isPopup && isUnifiedAccountMigrationInProgress) { + return forcedAccountMigrationUrl; } - RouteState.lastPathName = location.pathname; - }, [location]); + return undefined; + }, [isAcknowledgedUnifiedAccountMigration, isPopup, isUnifiedAccountMigrationInProgress]); const redirectPath = useMemo<string | null>(() => { const pathName = location.pathname; @@ -170,7 +143,7 @@ function DefaultRoute ({ children }: { children: React.ReactNode }): React.React if (!requireLogin) { // Do nothing - } else if (needMigrate && hasMasterPassword && !needUnlock) { + } else if (needMasterPasswordMigration && hasMasterPassword && !needUnlock) { redirectTarget = migratePasswordUrl; } else if (hasMasterPassword && needUnlock) { redirectTarget = loginUrl; @@ -191,6 +164,8 @@ function DefaultRoute ({ children }: { children: React.ReactNode }): React.React } else if (pathName === DEFAULT_ROUTER_PATH) { if (hasConfirmations) { openPModal('confirmations'); + } else if (activePriorityPath) { + redirectTarget = activePriorityPath; } else if (firstRender.current && currentPage) { redirectTarget = currentPage; } else { @@ -200,7 +175,7 @@ function DefaultRoute ({ children }: { children: React.ReactNode }): React.React redirectTarget = DEFAULT_ROUTER_PATH; } else if (pathName === welcomeUrl && !noAccount) { redirectTarget = DEFAULT_ROUTER_PATH; - } else if (pathName === migratePasswordUrl && !needMigrate) { + } else if (pathName === migratePasswordUrl && !needMasterPasswordMigration) { if (noAccount) { redirectTarget = welcomeUrl; } else { @@ -231,7 +206,51 @@ function DefaultRoute ({ children }: { children: React.ReactNode }): React.React } else { return null; } - }, [location.pathname, dataLoaded, needMigrate, hasMasterPassword, needUnlock, noAccount, hasInternalConfirmations, isOpenPModal, hasConfirmations, currentPage, openPModal]); + }, [location.pathname, dataLoaded, needMasterPasswordMigration, hasMasterPassword, needUnlock, noAccount, hasInternalConfirmations, isOpenPModal, hasConfirmations, activePriorityPath, currentPage, openPModal]); + + useEffect(() => { + initDataRef.current.then(() => { + setDataLoaded(true); + }).catch(console.error); + }, []); + + useEffect(() => { + let cancel = false; + let lastNotifyTime = new Date().getTime(); + + subscribeNotifications((rs) => { + rs.sort((a, b) => a.id - b.id) + .forEach(({ action, id, message, title, type }) => { + if (!cancel && id > lastNotifyTime) { + const notificationItem: NotificationProps = { message: title || message, type }; + + if (action?.url) { + notificationItem.onClick = () => { + window.open(action.url); + }; + } + + notify(notificationItem); + lastNotifyTime = id; + } + }); + }).catch(console.error); + + return () => { + cancel = true; + }; + }, [notify]); + + // Update goBack number + useEffect(() => { + if (location.pathname === RouteState.lastPathName) { + RouteState.prevDifferentPathNum -= 1; + } else { + RouteState.prevDifferentPathNum = -1; + } + + RouteState.lastPathName = location.pathname; + }, [location]); // Remove transaction persist state useEffect(() => { @@ -253,14 +272,14 @@ function DefaultRoute ({ children }: { children: React.ReactNode }): React.React useEffect(() => { if (rootLoading || redirectPath) { if (redirectPath && currentPage !== redirectPath && allowBlackScreenWS.includes(redirectPath)) { - setStorage(redirectPath); + setCurrentPage(redirectPath); } setShouldRedirect(true); } else { setShouldRedirect(false); } - }, [rootLoading, redirectPath, currentPage, setStorage]); + }, [rootLoading, redirectPath, currentPage, setCurrentPage]); useEffect(() => { if (shouldRedirect && redirectPath) { diff --git a/packages/extension-koni-ui/src/Popup/Settings/AccountSettings.tsx b/packages/extension-koni-ui/src/Popup/Settings/AccountSettings.tsx new file mode 100644 index 00000000000..30874d39d33 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Settings/AccountSettings.tsx @@ -0,0 +1,192 @@ +// Copyright 2019-2022 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; +import useNotification from '@subwallet/extension-koni-ui/hooks/common/useNotification'; +import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; +import { Theme, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { BackgroundIcon, Icon, SettingItem, SwIconProps } from '@subwallet/react-ui'; +import { CaretRight, CornersOut, Strategy } from 'phosphor-react'; +import React, { useCallback, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styled, { useTheme } from 'styled-components'; + +type Props = ThemeProps + +type SettingItemType = { + key: string, + leftIcon: SwIconProps['phosphorIcon'] | React.ReactNode, + leftIconBgColor: string, + rightIcon: SwIconProps['phosphorIcon'], + title: string, + onClick?: () => void, + isHidden?: boolean, +}; + +type SettingGroupItemType = { + key: string, + label?: string, + items: SettingItemType[], +}; + +const isReactNode = (element: unknown): element is React.ReactNode => { + return React.isValidElement(element); +}; + +function generateLeftIcon (backgroundColor: string, icon: SwIconProps['phosphorIcon'] | React.ReactNode): React.ReactNode { + const isNode = isReactNode(icon); + + return ( + <BackgroundIcon + backgroundColor={backgroundColor} + customIcon={isNode ? icon : undefined} + phosphorIcon={isNode ? undefined : icon} + size='sm' + type={isNode ? 'customIcon' : 'phosphor'} + weight='fill' + /> + ); +} + +function generateRightIcon (icon: SwIconProps['phosphorIcon']): React.ReactNode { + return ( + <Icon + className='__right-icon' + customSize={'20px'} + phosphorIcon={icon} + type='phosphor' + /> + ); +} + +function Component ({ className = '' }: Props): React.ReactElement<Props> { + const navigate = useNavigate(); + const { token } = useTheme() as Theme; + const notify = useNotification(); + const { t } = useTranslation(); + + const goBack = useCallback(() => { + navigate('/settings/list'); + }, [navigate]); + + const SettingGroupItemType = useMemo((): SettingGroupItemType[] => ([ + { + key: 'general', + items: [ + { + key: 'migrate-account', + leftIcon: Strategy, + leftIconBgColor: token.colorPrimary, + rightIcon: CaretRight, + title: t('Migrate to unified account'), + onClick: () => { + navigate('/migrate-account'); + } + }, + { + key: 'split-account', + leftIcon: CornersOut, + leftIconBgColor: token['volcano-6'], + rightIcon: CaretRight, + title: t('Split unified account'), + onClick: () => { + notify({ + message: 'Coming soon!' + }); + } + } + ] + } + ]), [navigate, notify, t, token]); + + return ( + <PageWrapper className={`account-settings ${className}`}> + <Layout.WithSubHeaderOnly + onBack={goBack} + title={t('Account settings')} + > + <div className={'__scroll-container'}> + { + SettingGroupItemType.map((group) => { + return ( + <div + className={'__group-container'} + key={group.key} + > + {!!group.label && (<div className='__group-label'>{group.label}</div>)} + + <div className={'__group-content'}> + {group.items.map((item) => item.isHidden + ? null + : ( + <SettingItem + className={'__setting-item setting-item'} + key={item.key} + leftItemIcon={generateLeftIcon(item.leftIconBgColor, item.leftIcon)} + name={item.title} + onPressItem={item.onClick} + rightItem={ + <> + {generateRightIcon(item.rightIcon)} + </> + } + /> + ))} + </div> + </div> + ); + }) + } + </div> + </Layout.WithSubHeaderOnly> + </PageWrapper> + ); +} + +export const AccountSettings = styled(Component)<Props>(({ theme: { token } }: Props) => { + return ({ + height: '100%', + backgroundColor: token.colorBgDefault, + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + + '.__scroll-container': { + overflow: 'auto', + paddingTop: token.padding, + paddingRight: token.padding, + paddingLeft: token.padding, + paddingBottom: token.paddingLG + }, + + '.__group-label': { + color: token.colorTextLight3, + fontSize: token.fontSizeSM, + lineHeight: token.lineHeightSM, + marginBottom: token.marginXS, + textTransform: 'uppercase' + }, + + '.__group-container': { + paddingBottom: token.padding + }, + + '.__setting-item + .__setting-item': { + marginTop: token.marginXS + }, + + '.ant-web3-block-right-item': { + minWidth: 40, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + color: token['gray-4'] + }, + + '.__setting-item:hover .ant-web3-block-right-item': { + color: token['gray-6'] + } + }); +}); + +export default AccountSettings; diff --git a/packages/extension-koni-ui/src/Popup/Settings/index.tsx b/packages/extension-koni-ui/src/Popup/Settings/index.tsx index e3eb9fd7fb4..344b77d80a2 100644 --- a/packages/extension-koni-ui/src/Popup/Settings/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Settings/index.tsx @@ -16,7 +16,7 @@ import { Theme, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { computeStatus, openInNewTab } from '@subwallet/extension-koni-ui/utils'; import { BackgroundIcon, Button, ButtonProps, Icon, Image, ModalContext, SettingItem, SwHeader, SwIconProps, SwModal } from '@subwallet/react-ui'; import CN from 'classnames'; -import { ArrowsOut, ArrowSquareOut, Book, BookBookmark, CaretRight, ChatTeardropText, Coin, EnvelopeSimple, FrameCorners, Globe, GlobeHemisphereEast, Lock, Rocket, ShareNetwork, ShieldCheck, X } from 'phosphor-react'; +import { ArrowsOut, ArrowSquareOut, Book, BookBookmark, CaretRight, ChatTeardropText, Coin, EnvelopeSimple, FrameCorners, Globe, GlobeHemisphereEast, Lock, Rocket, ShareNetwork, ShieldCheck, UserCircleGear, X } from 'phosphor-react'; import React, { useCallback, useContext, useMemo, useState } from 'react'; import { Outlet, useNavigate } from 'react-router-dom'; import styled, { useTheme } from 'styled-components'; @@ -145,6 +145,17 @@ function Component ({ className = '' }: Props): React.ReactElement<Props> { navigate('/settings/security', { state: true }); } }, + { + key: 'account-settings', + leftIcon: UserCircleGear, + leftIconBgColor: token['purple-8'], + rightIcon: CaretRight, + title: t('Account settings'), + onClick: () => { + navigate('/settings/account-settings'); + }, + isHidden: !isPopup + }, { key: 'crowdloans', leftIcon: Rocket, 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 4d6e3c85258..e321fcb4303 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -11,7 +11,7 @@ import { getAvailBridgeGatewayContract, getSnowBridgeGatewayContract } from '@su import { isAvailChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/availBridge'; import { _isPolygonChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/polygonBridge'; import { _isPosChainBridge, _isPosChainL2Bridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/posBridge'; -import { _getAssetDecimals, _getAssetName, _getAssetOriginChain, _getAssetSymbol, _getContractAddressOfToken, _getMultiChainAsset, _getOriginChainOfAsset, _getTokenMinAmount, _isChainEvmCompatible, _isNativeToken, _isTokenTransferredByEvm } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getAssetName, _getAssetOriginChain, _getAssetSymbol, _getContractAddressOfToken, _getMultiChainAsset, _getOriginChainOfAsset, _getTokenMinAmount, _isChainCardanoCompatible, _isChainEvmCompatible, _isNativeToken, _isTokenTransferredByEvm } from '@subwallet/extension-base/services/chain-service/utils'; import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; import { AccountChainType, AccountProxy, AccountProxyType, AccountSignMode, AnalyzedGroup, BasicTxWarningCode } from '@subwallet/extension-base/types'; @@ -177,7 +177,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone return true; } - return !!chainInfo && !!assetInfo && _isChainEvmCompatible(chainInfo) && destChainValue === chainValue && _isNativeToken(assetInfo); + return !!chainInfo && !!assetInfo && destChainValue === chainValue && _isNativeToken(assetInfo) && (_isChainEvmCompatible(chainInfo) || _isChainCardanoCompatible(chainInfo)); }, [chainInfoMap, chainValue, destChainValue, assetInfo]); const disabledToAddressInput = useMemo(() => { diff --git a/packages/extension-koni-ui/src/Popup/router.tsx b/packages/extension-koni-ui/src/Popup/router.tsx index d5555ef1596..e27656312cb 100644 --- a/packages/extension-koni-ui/src/Popup/router.tsx +++ b/packages/extension-koni-ui/src/Popup/router.tsx @@ -76,6 +76,7 @@ const Settings = new LazyLoader('Settings', () => import('@subwallet/extension-k const GeneralSetting = new LazyLoader('GeneralSetting', () => import('@subwallet/extension-koni-ui/Popup/Settings/GeneralSetting')); const MissionPools = new LazyLoader('MissionPools', () => import('@subwallet/extension-koni-ui/Popup/Settings/MissionPool/index')); const ManageAddressBook = new LazyLoader('ManageAddressBook', () => import('@subwallet/extension-koni-ui/Popup/Settings/AddressBook')); +const AccountSettings = new LazyLoader('ManageAddressBook', () => import('@subwallet/extension-koni-ui/Popup/Settings/AccountSettings')); const ManageChains = new LazyLoader('ManageChains', () => import('@subwallet/extension-koni-ui/Popup/Settings/Chains/ManageChains')); const ChainImport = new LazyLoader('ChainImport', () => import('@subwallet/extension-koni-ui/Popup/Settings/Chains/ChainImport')); @@ -122,6 +123,7 @@ const CancelUnstake = new LazyLoader('CancelUnstake', () => import('@subwallet/e const ClaimReward = new LazyLoader('ClaimReward', () => import('@subwallet/extension-koni-ui/Popup/Transaction/variants/ClaimReward')); const Withdraw = new LazyLoader('Withdraw', () => import('@subwallet/extension-koni-ui/Popup/Transaction/variants/Withdraw')); const ClaimBridge = new LazyLoader('ClaimBridge', () => import('@subwallet/extension-koni-ui/Popup/Transaction/variants/ClaimBridge')); +const MigrateAccount = new LazyLoader('MigrateAccount', () => import('@subwallet/extension-koni-ui/Popup/MigrateAccount')); // Earning @@ -234,6 +236,7 @@ export const router = createHashRouter([ children: [ Settings.generateRouterObject('list'), GeneralSetting.generateRouterObject('general'), + AccountSettings.generateRouterObject('account-settings'), Crowdloans.generateRouterObject('crowdloans'), ManageAddressBook.generateRouterObject('address-book'), SecurityList.generateRouterObject('security'), @@ -281,6 +284,9 @@ export const router = createHashRouter([ ExportAllDone.generateRouterObject('export-all-done') ] }, + { + ...MigrateAccount.generateRouterObject('migrate-account') + }, { path: 'wallet-connect', element: <Outlet />, diff --git a/packages/extension-koni-ui/src/assets/logo/index.ts b/packages/extension-koni-ui/src/assets/logo/index.ts index 26104b2f277..5677b4bfd3f 100644 --- a/packages/extension-koni-ui/src/assets/logo/index.ts +++ b/packages/extension-koni-ui/src/assets/logo/index.ts @@ -15,6 +15,7 @@ export const DefaultLogosMap: Record<string, string> = { polkadot_vault: './images/projects/polkadot-vault.png', walletconnect: './images/projects/walletconnect.png', banxa: './images/projects/banxa.png', + cardano: './images/projects/cardano.png', coinbase: './images/projects/coinbase.png', stellaswap: './images/projects/stellaswap.png', xtwitter: './images/projects/xtwitter.png', diff --git a/packages/extension-koni-ui/src/assets/subwallet/index.ts b/packages/extension-koni-ui/src/assets/subwallet/index.ts index d9ff7d0dbc6..23e2189ef7f 100644 --- a/packages/extension-koni-ui/src/assets/subwallet/index.ts +++ b/packages/extension-koni-ui/src/assets/subwallet/index.ts @@ -12,6 +12,10 @@ const SwLogosMap: Record<string, string> = { avatar_placeholder: require('./avatar_placeholder.png'), default: DefaultLogosMap.default, transak: DefaultLogosMap.transak, + cardano: DefaultLogosMap.cardano, + cardano_preproduction: DefaultLogosMap.cardano, + ['cardano-NATIVE-ADA'.toLowerCase()]: DefaultLogosMap.cardano, + ['cardano_preproduction-NATIVE-tADA'.toLowerCase()]: DefaultLogosMap.cardano, onramper: DefaultLogosMap.onramper, moonpay: DefaultLogosMap.moonpay, banxa: DefaultLogosMap.banxa, diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx new file mode 100644 index 00000000000..c7631481a39 --- /dev/null +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx @@ -0,0 +1,65 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ACCOUNT_CHAIN_TYPE_ORDINAL_MAP, AccountChainType } from '@subwallet/extension-base/types'; +import { Theme } from '@subwallet/extension-koni-ui/themes'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { getChainTypeLogoMap } from '@subwallet/extension-koni-ui/utils'; +import React, { Context, useContext, useMemo } from 'react'; +import styled, { ThemeContext } from 'styled-components'; + +type Props = ThemeProps & { + chainTypes: AccountChainType[]; +} + +function Component ({ chainTypes, className }: Props): React.ReactElement<Props> { + const logoMap = useContext<Theme>(ThemeContext as Context<Theme>).logoMap; + + const chainTypeLogoMap = useMemo(() => { + return getChainTypeLogoMap(logoMap); + }, [logoMap]); + + const sortedChainTypes = useMemo(() => { + const result = [...chainTypes]; + + result.sort((a, b) => ACCOUNT_CHAIN_TYPE_ORDINAL_MAP[a] - ACCOUNT_CHAIN_TYPE_ORDINAL_MAP[b]); + + return result; + }, [chainTypes]); + + return ( + <div className={className}> + { + sortedChainTypes.map((nt) => ( + <img + alt='Chain type' + className={'__chain-type-logo'} + key={nt} + src={chainTypeLogoMap[nt]} + /> + )) + } + </div> + ); +} + +const AccountChainTypeLogos = styled(Component)<Props>(({ theme: { token } }: Props) => { + return { + display: 'flex', + alignItems: 'center', + + '.__chain-type-logo': { + display: 'block', + boxShadow: '-4px 0px 4px 0px rgba(0, 0, 0, 0.40)', + width: token.size, + height: token.size, + borderRadius: '100%' + }, + + '.__chain-type-logo + .__chain-type-logo': { + marginLeft: -token.marginXXS + } + }; +}); + +export default AccountChainTypeLogos; diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxyItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxyItem.tsx index 277acfae3df..de54ef7bf5d 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxyItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxyItem.tsx @@ -81,7 +81,10 @@ const AccountProxyItem = styled(Component)<Props>(({ theme }) => { '.__item-middle-part': { flex: 1, - textAlign: 'left' + textAlign: 'left', + 'white-space': 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' }, '.__item-right-part': { diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx index 0b3c9c931cf..49b9e350146 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AccountChainType, AccountProxy, AccountProxyType } from '@subwallet/extension-base/types'; +import { AccountProxy, AccountProxyType } from '@subwallet/extension-base/types'; import { useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { Theme } from '@subwallet/extension-koni-ui/themes'; import { PhosphorIcon, ThemeProps } from '@subwallet/extension-koni-ui/types'; @@ -9,9 +9,10 @@ import { Button, Icon } from '@subwallet/react-ui'; import CN from 'classnames'; import { CheckCircle, Copy, Eye, GitCommit, GitMerge, Needle, PencilSimpleLine, QrCode, Question, Strategy, Swatches } from 'phosphor-react'; import { IconWeight } from 'phosphor-react/src/lib'; -import React, { Context, useContext, useMemo } from 'react'; +import React, { Context, useContext } from 'react'; import styled, { ThemeContext } from 'styled-components'; +import AccountChainTypeLogos from './AccountChainTypeLogos'; import AccountProxyAvatar from './AccountProxyAvatar'; type Props = ThemeProps & { @@ -41,19 +42,9 @@ function Component (props: Props): React.ReactElement<Props> { showDerivedPath } = props; const token = useContext<Theme>(ThemeContext as Context<Theme>).token; - const logoMap = useContext<Theme>(ThemeContext as Context<Theme>).logoMap; const { t } = useTranslation(); - const chainTypeLogoMap = useMemo(() => { - return { - [AccountChainType.SUBSTRATE]: logoMap.network.polkadot as string, - [AccountChainType.ETHEREUM]: logoMap.network.ethereum as string, - [AccountChainType.BITCOIN]: logoMap.network.bitcoin as string, - [AccountChainType.TON]: logoMap.network.ton as string - }; - }, [logoMap.network.bitcoin, logoMap.network.ethereum, logoMap.network.polkadot, logoMap.network.ton]); - const _onClickDeriveButton: React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement> = React.useCallback((event) => { event.stopPropagation(); onClickDeriveButton?.(); @@ -157,10 +148,10 @@ function Component (props: Props): React.ReactElement<Props> { </div> <div className='__item-center-part'> <div className='__item-name'>{accountProxy.name}</div> - <div className='__item-chain-types'> - { - showDerivedPath && !!accountProxy.parentId - ? <div className={'__item-derived-path'}> + { + showDerivedPath && !!accountProxy.parentId + ? ( + <div className={'__item-derived-path'}> <Icon className={'__derived-account-flag'} customSize='12px' @@ -171,18 +162,14 @@ function Component (props: Props): React.ReactElement<Props> { {accountProxy.suri || ''} </div> </div> - : accountProxy.chainTypes.map((nt) => { - return ( - <img - alt='Network type' - className={'__item-chain-type-item'} - key={nt} - src={chainTypeLogoMap[nt]} - /> - ); - }) - } - </div> + ) + : ( + <AccountChainTypeLogos + chainTypes={accountProxy.chainTypes} + className={'__item-chain-type-logos'} + /> + ) + } </div> <div className='__item-right-part'> <div className='__item-actions'> @@ -314,20 +301,8 @@ const AccountProxySelectorItem = styled(Component)<Props>(({ theme }) => { overflow: 'hidden', 'white-space': 'nowrap' }, - '.__item-chain-types': { - display: 'flex', - paddingTop: 2 - }, - '.__item-chain-type-item': { - display: 'block', - boxShadow: '-4px 0px 4px 0px rgba(0, 0, 0, 0.40)', - width: token.size, - height: token.size, - borderRadius: '100%', - marginLeft: -token.marginXXS - }, - '.__item-chain-type-item:first-of-type': { - marginLeft: 0 + '.__item-chain-type-logos': { + minHeight: 20 }, '.__item-address': { fontSize: token.fontSizeSM, diff --git a/packages/extension-koni-ui/src/components/AccountProxy/index.ts b/packages/extension-koni-ui/src/components/AccountProxy/index.ts index 375f0f920e0..16d2066e185 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/index.ts +++ b/packages/extension-koni-ui/src/components/AccountProxy/index.ts @@ -1,6 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +export { default as AccountChainTypeLogos } from './AccountChainTypeLogos'; export { default as AccountProxySelectorItem } from './AccountProxySelectorItem'; export { default as AccountProxyBriefInfo } from './AccountProxyBriefInfo'; export { default as AccountProxySelectorAllItem } from './AccountProxySelectorAllItem'; diff --git a/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/AccountSelectorModal.tsx b/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/AccountSelectorModal.tsx index c92d0696bdd..b1e7f252a98 100644 --- a/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/AccountSelectorModal.tsx +++ b/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/AccountSelectorModal.tsx @@ -10,16 +10,16 @@ import ExportAllSelector from '@subwallet/extension-koni-ui/components/Layout/pa import SelectAccountFooter from '@subwallet/extension-koni-ui/components/Layout/parts/SelectAccount/Footer'; import Search from '@subwallet/extension-koni-ui/components/Search'; import { ACCOUNT_CHAIN_ADDRESSES_MODAL, SELECT_ACCOUNT_MODAL } from '@subwallet/extension-koni-ui/constants/modal'; -import { useDefaultNavigate, useSetSessionLatest } from '@subwallet/extension-koni-ui/hooks'; +import { useDefaultNavigate, useIsPopup, useSetSessionLatest } from '@subwallet/extension-koni-ui/hooks'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; import { saveCurrentAccountAddress } from '@subwallet/extension-koni-ui/messaging'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { Theme } from '@subwallet/extension-koni-ui/themes'; import { AccountDetailParam, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { isAccountAll } from '@subwallet/extension-koni-ui/utils'; -import { Icon, ModalContext, SwList, SwModal, Tooltip } from '@subwallet/react-ui'; +import { Button, Icon, ModalContext, SwList, SwModal, Tooltip } from '@subwallet/react-ui'; import CN from 'classnames'; -import { Circle, Export } from 'phosphor-react'; +import { Circle, Export, Gear } from 'phosphor-react'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -88,6 +88,7 @@ const Component: React.FC<Props> = ({ className }: Props) => { const navigate = useNavigate(); const { setStateSelectAccount } = useSetSessionLatest(); const isModalVisible = useMemo(() => checkActive(modalId), [checkActive]); + const isPopup = useIsPopup(); const accountProxies = useSelector((state: RootState) => state.accountState.accountProxies); const currentAccountProxy = useSelector((state: RootState) => state.accountState.currentAccountProxy); @@ -338,6 +339,22 @@ const Component: React.FC<Props> = ({ className }: Props) => { } }, [isModalVisible]); + const accountSettingButtonProps = useMemo<ButtonProps>(() => { + return { + icon: ( + <Icon + phosphorIcon={Gear} + /> + ), + type: 'ghost', + onClick: () => { + navigate('/settings/account-settings'); + }, + tooltip: t('Account settings'), + tooltipPlacement: 'topRight' + }; + }, [navigate, t]); + const rightIconProps = useMemo((): ButtonProps | undefined => { if (!enableExtraction) { return; @@ -398,7 +415,19 @@ const Component: React.FC<Props> = ({ className }: Props) => { id={modalId} onCancel={_onCancel} rightIconProps={rightIconProps} - title={t('Select account')} + title={( + <> + {t('Select account')} + + {isPopup && ( + <Button + {...accountSettingButtonProps} + className={'__account-setting-button -schema-header'} + size={'xs'} + /> + )} + </> + )} > <Search autoFocus={true} @@ -435,6 +464,23 @@ const Component: React.FC<Props> = ({ className }: Props) => { export const AccountSelectorModal = styled(Component)<Props>(({ theme: { token } }: Props) => { return { + '.ant-sw-header-center-part': { + position: 'relative', + height: 40 + }, + + '.ant-sw-sub-header-title': { + fontSize: token.fontSizeXL, + lineHeight: token.lineHeightHeading4, + fontWeight: token.fontWeightStrong + }, + + '.ant-sw-header-center-part .__account-setting-button': { + position: 'absolute', + right: 0, + top: 0 + }, + '.ant-sw-modal-content': { height: '100vh' }, diff --git a/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/ExportAllSelectItem.tsx b/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/ExportAllSelectItem.tsx index c500b72e57d..4786221ae9d 100644 --- a/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/ExportAllSelectItem.tsx +++ b/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/ExportAllSelectItem.tsx @@ -1,8 +1,8 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AccountChainType, AccountProxy, AccountProxyType } from '@subwallet/extension-base/types'; -import { AccountProxyAvatar } from '@subwallet/extension-koni-ui/components'; +import { AccountProxy, AccountProxyType } from '@subwallet/extension-base/types'; +import { AccountChainTypeLogos, AccountProxyAvatar } from '@subwallet/extension-koni-ui/components'; import { Theme } from '@subwallet/extension-koni-ui/themes'; import { PhosphorIcon } from '@subwallet/extension-koni-ui/types'; import { Icon } from '@subwallet/react-ui'; @@ -40,15 +40,6 @@ function Component (props: _AccountCardItem): React.ReactElement<_AccountCardIte const { accountType, chainTypes, id: accountProxyId, name: accountName } = useMemo(() => accountProxy, [accountProxy]); const token = useContext<Theme>(ThemeContext as Context<Theme>).token; - const logoMap = useContext<Theme>(ThemeContext as Context<Theme>).logoMap; - const chainTypeLogoMap = useMemo(() => { - return { - [AccountChainType.SUBSTRATE]: logoMap.network.polkadot as string, - [AccountChainType.ETHEREUM]: logoMap.network.ethereum as string, - [AccountChainType.BITCOIN]: logoMap.network.bitcoin as string, - [AccountChainType.TON]: logoMap.network.ton as string - }; - }, [logoMap.network.bitcoin, logoMap.network.ethereum, logoMap.network.polkadot, logoMap.network.ton]); const _onSelect = useCallback(() => { onClick && onClick(accountProxyId || ''); }, @@ -142,20 +133,10 @@ function Component (props: _AccountCardItem): React.ReactElement<_AccountCardIte <div className='__item-center-part'> <div className={'middle-item__name-wrapper'}> <div className='__item-name'>{accountName}</div> - <div className='__item-chain-types'> - { - chainTypes.map((nt) => { - return ( - <img - alt='Network type' - className={'__item-chain-type-item'} - key={nt} - src={chainTypeLogoMap[nt]} - /> - ); - }) - } - </div> + <AccountChainTypeLogos + chainTypes={chainTypes} + className={'__item-chain-type-logos'} + /> </div> </div> @@ -238,22 +219,8 @@ const ExportAllSelectItem = styled(Component)<_AccountCardItem>(({ theme }) => { overflow: 'hidden', 'white-space': 'nowrap' }, - '.__item-chain-types': { - display: 'flex', - paddingTop: 2, - - '.__item-chain-type-item': { - display: 'block', - boxShadow: '-4px 0px 4px 0px rgba(0, 0, 0, 0.40)', - width: token.size, - height: token.size, - borderRadius: '100%', - marginLeft: -token.marginXXS - }, - - '.__item-chain-type-item:first-of-type': { - marginLeft: 0 - } + '.__item-chain-type-logos': { + minHeight: 20 }, '.__item-right-part': { marginLeft: 'auto', diff --git a/packages/extension-koni-ui/src/components/Modal/DeriveAccountActionModal.tsx b/packages/extension-koni-ui/src/components/Modal/DeriveAccountActionModal.tsx index a29696a56e2..08325853879 100644 --- a/packages/extension-koni-ui/src/components/Modal/DeriveAccountActionModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/DeriveAccountActionModal.tsx @@ -37,7 +37,7 @@ interface DeriveFormState { const modalId = DERIVE_ACCOUNT_ACTION_MODAL; -const alertTypes: DerivePathInfo['type'][] = ['unified', 'ton', 'ethereum']; +const alertTypes: DerivePathInfo['type'][] = ['unified', 'ton', 'ethereum', 'cardano']; const Component: React.FC<Props> = (props: Props) => { const { className, onCompleteCb, proxyId } = props; @@ -76,9 +76,10 @@ const Component: React.FC<Props> = (props: Props) => { 'bitcoin-86': logoMap.network.bitcoin as string, 'bittest-44': logoMap.network.bitcoin as string, 'bittest-84': logoMap.network.bitcoin as string, - 'bittest-86': logoMap.network.bitcoin as string + 'bittest-86': logoMap.network.bitcoin as string, + cardano: logoMap.network.cardano as string }; - }, [logoMap.network.bitcoin, logoMap.network.ethereum, logoMap.network.polkadot, logoMap.network.ton]); + }, [logoMap.network.bitcoin, logoMap.network.cardano, logoMap.network.ethereum, logoMap.network.polkadot, logoMap.network.ton]); const [form] = Form.useForm<DeriveFormState>(); diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AccountMigrationInProgressWarningModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AccountMigrationInProgressWarningModal.tsx new file mode 100644 index 00000000000..c8d98b72f91 --- /dev/null +++ b/packages/extension-koni-ui/src/components/Modal/Global/AccountMigrationInProgressWarningModal.tsx @@ -0,0 +1,104 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ACCOUNT_MIGRATION_IN_PROGRESS_WARNING_MODAL } from '@subwallet/extension-koni-ui/constants'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { Button, Icon, PageIcon, SwModal } from '@subwallet/react-ui'; +import CN from 'classnames'; +import { ArrowClockwise, Warning } from 'phosphor-react'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +type Props = ThemeProps; + +const modalId = ACCOUNT_MIGRATION_IN_PROGRESS_WARNING_MODAL; + +const Component: React.FC<Props> = (props: Props) => { + const { className } = props; + const { t } = useTranslation(); + + const onClickActionButton = useCallback(() => { + window.location.reload(); + }, []); + + return ( + <> + <SwModal + className={CN(className)} + closable={false} + destroyOnClose={true} + footer={ + <> + <Button + block={true} + className={'__action-button'} + icon={( + <Icon + phosphorIcon={ArrowClockwise} + weight={'fill'} + /> + )} + onClick={onClickActionButton} + > + {t('Reload view')} + </Button> + </> + } + id={modalId} + maskClosable={false} + title={t('Migration in progress')} + zIndex={1000000} + > + <div className='__modal-content'> + <div className={CN('__warning-icon')}> + <PageIcon + color='var(--page-icon-color)' + iconProps={{ + phosphorIcon: Warning + }} + /> + </div> + + {t('You can\'t perform any action in Expand view while account migration is in progress. Reopen SubWallet extension to complete migration, then reload to continue using Expand view')} + </div> + </SwModal> + </> + ); +}; + +const AccountMigrationInProgressWarningModal = styled(Component)<Props>(({ theme: { token } }: Props) => { + return { + '.ant-sw-modal-body': {}, + + '.ant-sw-modal-footer': { + display: 'flex', + borderTop: 0, + gap: token.sizeXXS + }, + + '.ant-sw-header-center-part': { + width: '100%', + maxWidth: 292 + }, + + '.__warning-icon': { + display: 'flex', + justifyContent: 'center', + marginBottom: 20, + '--page-icon-color': token.colorWarning + }, + + '.__modal-content': { + fontSize: token.fontSize, + lineHeight: token.lineHeightHeading6, + textAlign: 'center', + color: token.colorTextDescription, + paddingTop: token.padding, + paddingLeft: token.padding, + paddingRight: token.padding + } + }; +}); + +export default AccountMigrationInProgressWarningModal; diff --git a/packages/extension-koni-ui/src/components/Modal/Global/index.ts b/packages/extension-koni-ui/src/components/Modal/Global/index.ts index d429b314b78..5fcc9dce36f 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/index.ts +++ b/packages/extension-koni-ui/src/components/Modal/Global/index.ts @@ -2,3 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export { default as AddressQrModal } from './AddressQrModal'; +export { default as AccountMigrationInProgressWarningModal } from './AccountMigrationInProgressWarningModal'; diff --git a/packages/extension-koni-ui/src/components/Modal/RemindDuplicateAccountNameModal.tsx b/packages/extension-koni-ui/src/components/Modal/RemindDuplicateAccountNameModal.tsx index 1acf25ab4ca..787a52208f2 100644 --- a/packages/extension-koni-ui/src/components/Modal/RemindDuplicateAccountNameModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/RemindDuplicateAccountNameModal.tsx @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { REMIND_DUPLICATE_ACCOUNT_NAME_MODAL, UPGRADE_DUPLICATE_ACCOUNT_NAME } from '@subwallet/extension-koni-ui/constants'; +import { NOTIFICATION_MODAL_WHITELIST_PATHS, REMIND_DUPLICATE_ACCOUNT_NAME_MODAL, UPGRADE_DUPLICATE_ACCOUNT_NAME } from '@subwallet/extension-koni-ui/constants'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; import { getValueLocalStorageWS, setValueLocalStorageWS } from '@subwallet/extension-koni-ui/messaging'; import { Theme } from '@subwallet/extension-koni-ui/themes'; @@ -11,6 +11,7 @@ import { Button, ModalContext, PageIcon, SwModal } from '@subwallet/react-ui'; import CN from 'classnames'; import { ShieldWarning } from 'phosphor-react'; import React, { useCallback, useContext, useEffect, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; import styled, { useTheme } from 'styled-components'; type Props = ThemeProps; @@ -19,6 +20,7 @@ const RemindDuplicateAccountNameModalId = REMIND_DUPLICATE_ACCOUNT_NAME_MODAL; const CHANGE_ACCOUNT_NAME_URL = 'https://docs.subwallet.app/main/extension-user-guide/account-management/switch-between-accounts-and-change-account-name#change-your-account-name'; function Component ({ className }: Props): React.ReactElement<Props> { + const location = useLocation(); const { t } = useTranslation(); const { activeModal, inactiveModal } = useContext(ModalContext); const { token } = useTheme() as Theme; @@ -28,13 +30,19 @@ function Component ({ className }: Props): React.ReactElement<Props> { setValueLocalStorageWS({ key: UPGRADE_DUPLICATE_ACCOUNT_NAME, value: 'false' }).catch(noop); }, [inactiveModal]); + const isInWhitelistPaths = useMemo(() => { + return NOTIFICATION_MODAL_WHITELIST_PATHS.includes(location.pathname); + }, [location.pathname]); + useEffect(() => { - getValueLocalStorageWS(UPGRADE_DUPLICATE_ACCOUNT_NAME).then((value) => { - if (value === 'true') { - activeModal(RemindDuplicateAccountNameModalId); - } - }).catch(noop); - }, [activeModal]); + if (isInWhitelistPaths) { + getValueLocalStorageWS(UPGRADE_DUPLICATE_ACCOUNT_NAME).then((value) => { + if (value === 'true') { + activeModal(RemindDuplicateAccountNameModalId); + } + }).catch(noop); + } + }, [activeModal, isInWhitelistPaths]); const footerModal = useMemo(() => { return ( @@ -54,6 +62,7 @@ function Component ({ className }: Props): React.ReactElement<Props> { <SwModal className={CN(className)} closable={true} + destroyOnClose={true} footer={footerModal} id={RemindDuplicateAccountNameModalId} maskClosable={false} diff --git a/packages/extension-koni-ui/src/components/Modal/index.tsx b/packages/extension-koni-ui/src/components/Modal/index.tsx index 225db02870b..a3064c4590a 100644 --- a/packages/extension-koni-ui/src/components/Modal/index.tsx +++ b/packages/extension-koni-ui/src/components/Modal/index.tsx @@ -6,7 +6,7 @@ export { default as DeriveAccountActionModal } from './DeriveAccountActionModal' export { default as DisconnectExtensionModal } from './DisconnectExtensionModal'; export { default as ReceiveModal } from './ReceiveModalNew'; export { default as RemindBackupSeedPhraseModal } from './RemindBackupSeedPhraseModal'; -export { default as RemindUpgradeUnifiedAccount } from './RemindDuplicateAccountNameModal'; +export { default as RemindDuplicateAccountNameModal } from './RemindDuplicateAccountNameModal'; export { default as RemindUpgradeVersionModal } from './RemindUpgradeFirefoxVersion'; export { default as RequestCameraAccessModal } from './RequestCameraAccessModal'; export { default as RequestCreatePasswordModal } from './RequestCreatePasswordModal'; diff --git a/packages/extension-koni-ui/src/components/StaticContent/ContentGenerator.tsx b/packages/extension-koni-ui/src/components/StaticContent/ContentGenerator.tsx index 13932b70959..671b59e3b8a 100644 --- a/packages/extension-koni-ui/src/components/StaticContent/ContentGenerator.tsx +++ b/packages/extension-koni-ui/src/components/StaticContent/ContentGenerator.tsx @@ -30,10 +30,20 @@ const Component = ({ className, content }: Props) => { img (props) { const { children, className, node, src, ...rest } = props; + if (className?.includes('md-element')) { + return ( + <img + {...rest} + className={className} + src={src} + /> + ); + } + return ( <Image {...rest} - className={'custom-img'} + className={className || 'custom-img'} onClick={noop} src={src} width={'100%'} diff --git a/packages/extension-koni-ui/src/constants/modal.ts b/packages/extension-koni-ui/src/constants/modal.ts index 0035d0bbebb..18b59f11a65 100644 --- a/packages/extension-koni-ui/src/constants/modal.ts +++ b/packages/extension-koni-ui/src/constants/modal.ts @@ -30,7 +30,7 @@ export const ADD_CONNECTION_MODAL = 'add-connection-modal'; export const DISCONNECT_EXTENSION_MODAL = 'disconnect-extension-modal'; export const REMIND_BACKUP_SEED_PHRASE_MODAL = 'remind-backup-seed-phrase-modal'; export const REMIND_UPGRADE_FIREFOX_VERSION = 'remind-update-firefox-version'; -export const REMIND_DUPLICATE_ACCOUNT_NAME_MODAL = 'remind-update-unified-account'; +export const REMIND_DUPLICATE_ACCOUNT_NAME_MODAL = 'remind-duplicate-account-name-modal'; export const EXPORT_ACCOUNTS_PASSWORD_MODAL = 'export-accounts-password-modal'; export const ADD_NETWORK_WALLET_CONNECT_MODAL = 'add-network-wallet-connect-modal'; export const ADDRESS_QR_MODAL = 'address-qr-modal'; @@ -39,6 +39,7 @@ export const ACCOUNT_NAME_MODAL = 'account-name-modal'; export const GLOBAL_ALERT_MODAL = 'global-alert-modal'; export const TON_WALLET_CONTRACT_SELECTOR_MODAL = 'ton-wallet-contract-selector-modal'; export const TON_ACCOUNT_SELECTOR_MODAL = 'ton-account-selector-modal'; +export const ACCOUNT_MIGRATION_IN_PROGRESS_WARNING_MODAL = 'account-migration-in-progress-warning-modal'; /* Campaign */ export const HOME_CAMPAIGN_BANNER_MODAL = 'home-campaign-banner-modal'; diff --git a/packages/extension-koni-ui/src/constants/router.ts b/packages/extension-koni-ui/src/constants/router.ts index f11a40d8031..97af8a6280a 100644 --- a/packages/extension-koni-ui/src/constants/router.ts +++ b/packages/extension-koni-ui/src/constants/router.ts @@ -2,3 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 export const DEFAULT_ROUTER_PATH = '/'; + +export const NOTIFICATION_MODAL_WHITELIST_PATHS = [ + '/migrate-account', + '/home/tokens' +]; diff --git a/packages/extension-koni-ui/src/contexts/DataContext.tsx b/packages/extension-koni-ui/src/contexts/DataContext.tsx index 17b25a68a25..fffba561b2d 100644 --- a/packages/extension-koni-ui/src/contexts/DataContext.tsx +++ b/packages/extension-koni-ui/src/contexts/DataContext.tsx @@ -3,7 +3,7 @@ import { ping } from '@subwallet/extension-koni-ui/messaging'; import { persistor, store, StoreName } from '@subwallet/extension-koni-ui/stores'; -import { getMissionPoolData, subscribeAccountsData, subscribeAddressBook, subscribeAssetLogoMaps, subscribeAssetRegistry, subscribeAssetSettings, subscribeAuthorizeRequests, subscribeAuthUrls, subscribeBalance, subscribeBuyServices, subscribeBuyTokens, subscribeCampaignBannerData, subscribeCampaignConfirmationData, subscribeCampaignPopupData, subscribeCampaignPopupVisibility, subscribeChainInfoMap, subscribeChainLogoMaps, subscribeChainStakingMetadata, subscribeChainStateMap, subscribeChainStatusMap, subscribeConfirmationRequests, subscribeConfirmationRequestsTon, subscribeConnectWCRequests, subscribeCrowdloan, subscribeKeyringState, subscribeLedgerGenericAllowNetworks, subscribeMantaPayConfig, subscribeMantaPaySyncingState, subscribeMetadataRequests, subscribeMultiChainAssetMap, subscribeNftCollections, subscribeNftItems, subscribePrice, subscribeProcessingCampaign, subscribeRewardHistory, subscribeSigningRequests, subscribeStaking, subscribeStakingNominatorMetadata, subscribeStakingReward, subscribeSwapPairs, subscribeTransactionRequests, subscribeTxHistory, subscribeUiSettings, subscribeUnreadNotificationCount, subscribeWalletConnectSessions, subscribeWCNotSupportRequests, subscribeXcmRefMap, subscribeYieldMinAmountPercent, subscribeYieldPoolInfo, subscribeYieldPositionInfo, subscribeYieldReward } from '@subwallet/extension-koni-ui/stores/utils'; +import { getMissionPoolData, subscribeAccountsData, subscribeAddressBook, subscribeAssetLogoMaps, subscribeAssetRegistry, subscribeAssetSettings, subscribeAuthorizeRequests, subscribeAuthUrls, subscribeBalance, subscribeBuyServices, subscribeBuyTokens, subscribeCampaignBannerData, subscribeCampaignConfirmationData, subscribeCampaignPopupData, subscribeCampaignPopupVisibility, subscribeChainInfoMap, subscribeChainLogoMaps, subscribeChainStakingMetadata, subscribeChainStateMap, subscribeChainStatusMap, subscribeConfirmationRequests, subscribeConfirmationRequestsCardano, subscribeConfirmationRequestsTon, subscribeConnectWCRequests, subscribeCrowdloan, subscribeKeyringState, subscribeLedgerGenericAllowNetworks, subscribeMantaPayConfig, subscribeMantaPaySyncingState, subscribeMetadataRequests, subscribeMultiChainAssetMap, subscribeNftCollections, subscribeNftItems, subscribePrice, subscribeProcessingCampaign, subscribeRewardHistory, subscribeSigningRequests, subscribeStaking, subscribeStakingNominatorMetadata, subscribeStakingReward, subscribeSwapPairs, subscribeTransactionRequests, subscribeTxHistory, subscribeUiSettings, subscribeUnreadNotificationCount, subscribeWalletConnectSessions, subscribeWCNotSupportRequests, subscribeXcmRefMap, subscribeYieldMinAmountPercent, subscribeYieldPoolInfo, subscribeYieldPositionInfo, subscribeYieldReward } from '@subwallet/extension-koni-ui/stores/utils'; import Bowser from 'bowser'; import React from 'react'; import { Provider } from 'react-redux'; @@ -227,6 +227,7 @@ export const DataContextProvider = ({ children }: DataContextProviderProps) => { _DataContext.addHandler({ ...subscribeSigningRequests, name: 'subscribeSigningRequests', relatedStores: ['requestState'], isStartImmediately: true }); _DataContext.addHandler({ ...subscribeConfirmationRequests, name: 'subscribeConfirmationRequests', relatedStores: ['requestState'], isStartImmediately: true }); _DataContext.addHandler({ ...subscribeConfirmationRequestsTon, name: 'subscribeConfirmationRequestsTon', relatedStores: ['requestState'], isStartImmediately: true }); + _DataContext.addHandler({ ...subscribeConfirmationRequestsCardano, name: 'subscribeConfirmationRequestsCardano', relatedStores: ['requestState'], isStartImmediately: true }); _DataContext.addHandler({ ...subscribeTransactionRequests, name: 'subscribeTransactionRequests', relatedStores: ['requestState'], isStartImmediately: true }); _DataContext.addHandler({ ...subscribeConnectWCRequests, name: 'subscribeConnectWCRequests', relatedStores: ['requestState'], isStartImmediately: true }); _DataContext.addHandler({ ...subscribeWCNotSupportRequests, name: 'subscribeWCNotSupportRequests', relatedStores: ['requestState'], isStartImmediately: true }); diff --git a/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx b/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx index a11d1efc398..258bdf335bb 100644 --- a/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx +++ b/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx @@ -1,11 +1,11 @@ // Copyright 2019-2022 @polkadot/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AddressQrModal, AlertModal, AttachAccountModal, ClaimDappStakingRewardsModal, CreateAccountModal, DeriveAccountActionModal, DeriveAccountListModal, ImportAccountModal, ImportSeedModal, NewSeedModal, RemindBackupSeedPhraseModal, RequestCameraAccessModal, RequestCreatePasswordModal } from '@subwallet/extension-koni-ui/components'; +import { AccountMigrationInProgressWarningModal, AddressQrModal, AlertModal, AttachAccountModal, ClaimDappStakingRewardsModal, CreateAccountModal, DeriveAccountActionModal, DeriveAccountListModal, ImportAccountModal, ImportSeedModal, NewSeedModal, RemindBackupSeedPhraseModal, RemindDuplicateAccountNameModal, RequestCameraAccessModal, RequestCreatePasswordModal } from '@subwallet/extension-koni-ui/components'; import { CustomizeModal } from '@subwallet/extension-koni-ui/components/Modal/Customize/CustomizeModal'; import { AccountDeriveActionProps } from '@subwallet/extension-koni-ui/components/Modal/DeriveAccountActionModal'; -import { ADDRESS_QR_MODAL, DERIVE_ACCOUNT_ACTION_MODAL, EARNING_INSTRUCTION_MODAL, GLOBAL_ALERT_MODAL } from '@subwallet/extension-koni-ui/constants'; -import { useAlert, useGetConfig, useSetSessionLatest } from '@subwallet/extension-koni-ui/hooks'; +import { ACCOUNT_MIGRATION_IN_PROGRESS_WARNING_MODAL, ADDRESS_QR_MODAL, DERIVE_ACCOUNT_ACTION_MODAL, EARNING_INSTRUCTION_MODAL, GLOBAL_ALERT_MODAL } from '@subwallet/extension-koni-ui/constants'; +import { useAlert, useGetConfig, useIsPopup, useSetSessionLatest } from '@subwallet/extension-koni-ui/hooks'; import Confirmations from '@subwallet/extension-koni-ui/Popup/Confirmations'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AlertDialogProps } from '@subwallet/extension-koni-ui/types'; @@ -105,9 +105,12 @@ export const WalletModalContextProvider = ({ children }: Props) => { const { getConfig } = useGetConfig(); const { onHandleSessionLatest, setTimeBackUp } = useSetSessionLatest(); const { alertProps, closeAlert, openAlert } = useAlert(alertModalId); + const isUnifiedAccountMigrationInProgress = useSelector((state: RootState) => state.settings.isUnifiedAccountMigrationInProgress); + const isPopup = useIsPopup(); useExcludeModal('confirmations'); useExcludeModal(EARNING_INSTRUCTION_MODAL); + useExcludeModal(ACCOUNT_MIGRATION_IN_PROGRESS_WARNING_MODAL); const onCloseModal = useCallback(() => { setSearchParams((prev) => { @@ -170,6 +173,12 @@ export const WalletModalContextProvider = ({ children }: Props) => { } }, [hasMasterPassword, inactiveAll, isLocked]); + useEffect(() => { + if (!isPopup && isUnifiedAccountMigrationInProgress) { + activeModal(ACCOUNT_MIGRATION_IN_PROGRESS_WARNING_MODAL); + } + }, [activeModal, isPopup, isUnifiedAccountMigrationInProgress]); + useEffect(() => { const confirmID = searchParams.get('popup'); @@ -220,6 +229,8 @@ export const WalletModalContextProvider = ({ children }: Props) => { <RequestCameraAccessModal /> <CustomizeModal /> <UnlockModal /> + <AccountMigrationInProgressWarningModal /> + <RemindDuplicateAccountNameModal /> { !!addressQrModalProps && ( diff --git a/packages/extension-koni-ui/src/hooks/staticContent/index.ts b/packages/extension-koni-ui/src/hooks/staticContent/index.ts index 39e42f50d8e..e98b46dfe08 100644 --- a/packages/extension-koni-ui/src/hooks/staticContent/index.ts +++ b/packages/extension-koni-ui/src/hooks/staticContent/index.ts @@ -2,3 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export { default as useGetConfig } from './useGetConfig'; +export { default as useFetchMarkdownContentData } from './useFetchMarkdownContentData'; diff --git a/packages/extension-koni-ui/src/hooks/staticContent/useFetchMarkdownContentData.tsx b/packages/extension-koni-ui/src/hooks/staticContent/useFetchMarkdownContentData.tsx new file mode 100644 index 00000000000..c14ddb6ee32 --- /dev/null +++ b/packages/extension-koni-ui/src/hooks/staticContent/useFetchMarkdownContentData.tsx @@ -0,0 +1,27 @@ +// Copyright 2019-2022 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { LanguageType } from '@subwallet/extension-base/background/KoniTypes'; +import { fetchStaticData } from '@subwallet/extension-base/utils'; +import { isProductionMode } from '@subwallet/extension-koni-ui/constants'; +import { RootState } from '@subwallet/extension-koni-ui/stores'; +import { useCallback } from 'react'; +import { useSelector } from 'react-redux'; + +const useFetchMarkdownContentData = () => { + const currentLanguage = useSelector((state: RootState) => state.settings.language); + + const getJsonFile = useCallback((supportedLanguages: LanguageType[], fallbackLanguage: LanguageType) => { + const resultLanguage = supportedLanguages.includes(currentLanguage) ? currentLanguage : fallbackLanguage; + + return isProductionMode ? `list-${resultLanguage}.json` : `preview-${resultLanguage}.json`; + }, [currentLanguage]); + + return useCallback(<T = unknown>(folder: string, supportedLanguages: LanguageType[], fallbackLanguage: LanguageType = 'en') => { + const jsonFile = getJsonFile(supportedLanguages, fallbackLanguage); + + return fetchStaticData<T>(`markdown-contents/${folder}`, jsonFile, true); + }, [getJsonFile]); +}; + +export default useFetchMarkdownContentData; diff --git a/packages/extension-koni-ui/src/messaging/confirmation/base.ts b/packages/extension-koni-ui/src/messaging/confirmation/base.ts index 5a456901c95..9c7f992e5e5 100644 --- a/packages/extension-koni-ui/src/messaging/confirmation/base.ts +++ b/packages/extension-koni-ui/src/messaging/confirmation/base.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { ConfirmationDefinitions, ConfirmationDefinitionsTon, ConfirmationType, ConfirmationTypeTon, RequestSigningApprovePasswordV2 } from '@subwallet/extension-base/background/KoniTypes'; +import { ConfirmationDefinitions, ConfirmationDefinitionsCardano, ConfirmationDefinitionsTon, ConfirmationType, ConfirmationTypeCardano, ConfirmationTypeTon, RequestSigningApprovePasswordV2 } from '@subwallet/extension-base/background/KoniTypes'; import { ResponseSigningIsLocked } from '@subwallet/extension-base/background/types'; import { HexString } from '@polkadot/util/types'; @@ -35,3 +35,7 @@ export async function completeConfirmation<CT extends ConfirmationType> (type: C export async function completeConfirmationTon<CT extends ConfirmationTypeTon> (type: CT, payload: ConfirmationDefinitionsTon[CT][1]): Promise<boolean> { return sendMessage('pri(confirmationsTon.complete)', { [type]: payload }); } + +export async function completeConfirmationCardano<CT extends ConfirmationTypeCardano> (type: CT, payload: ConfirmationDefinitionsCardano[CT][1]): Promise<boolean> { + return sendMessage('pri(confirmationsCardano.complete)', { [type]: payload }); +} diff --git a/packages/extension-koni-ui/src/messaging/migrate-unified-account/index.ts b/packages/extension-koni-ui/src/messaging/migrate-unified-account/index.ts new file mode 100644 index 00000000000..620ab7d7650 --- /dev/null +++ b/packages/extension-koni-ui/src/messaging/migrate-unified-account/index.ts @@ -0,0 +1,17 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { RequestMigrateSoloAccount, RequestMigrateUnifiedAndFetchEligibleSoloAccounts, RequestPingSession, ResponseMigrateSoloAccount, ResponseMigrateUnifiedAndFetchEligibleSoloAccounts } from '@subwallet/extension-base/background/KoniTypes'; +import { sendMessage } from '@subwallet/extension-koni-ui/messaging'; + +export function migrateUnifiedAndFetchEligibleSoloAccounts (request: RequestMigrateUnifiedAndFetchEligibleSoloAccounts): Promise<ResponseMigrateUnifiedAndFetchEligibleSoloAccounts> { + return sendMessage('pri(migrate.migrateUnifiedAndFetchEligibleSoloAccounts)', request); +} + +export function migrateSoloAccount (request: RequestMigrateSoloAccount): Promise<ResponseMigrateSoloAccount> { + return sendMessage('pri(migrate.migrateSoloAccount)', request); +} + +export function pingSession (request: RequestPingSession) { + return sendMessage('pri(migrate.pingSession)', request); +} diff --git a/packages/extension-koni-ui/src/messaging/settings/base.ts b/packages/extension-koni-ui/src/messaging/settings/base.ts index 1fa93af7774..48a0ea7e4e1 100644 --- a/packages/extension-koni-ui/src/messaging/settings/base.ts +++ b/packages/extension-koni-ui/src/messaging/settings/base.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { BrowserConfirmationType, CurrencyType, LanguageType, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveOSConfig, RequestSettingsType, RequestSubscribeBalancesVisibility, ThemeNames, UiSettings, WalletUnlockType } from '@subwallet/extension-base/background/KoniTypes'; +import { BrowserConfirmationType, CurrencyType, LanguageType, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveMigrationAcknowledgedStatus, RequestSaveOSConfig, RequestSaveUnifiedAccountMigrationInProgress, RequestSettingsType, RequestSubscribeBalancesVisibility, ThemeNames, UiSettings, WalletUnlockType } from '@subwallet/extension-base/background/KoniTypes'; import { NotificationSetup } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; import { sendMessage } from '@subwallet/extension-koni-ui/messaging'; @@ -41,6 +41,18 @@ export async function saveNotificationSetup (request: NotificationSetup): Promis return sendMessage('pri(settings.saveNotificationSetup)', request); } +export async function saveUnifiedAccountMigrationInProgress (request: RequestSaveUnifiedAccountMigrationInProgress): Promise<boolean> { + return sendMessage('pri(settings.saveUnifiedAccountMigrationInProgress)', request); +} + +export async function pingUnifiedAccountMigrationDone (): Promise<boolean> { + return sendMessage('pri(settings.pingUnifiedAccountMigrationDone)'); +} + +export async function saveMigrationAcknowledgedStatus (request: RequestSaveMigrationAcknowledgedStatus): Promise<boolean> { + return sendMessage('pri(settings.saveMigrationAcknowledgedStatus)', request); +} + export async function saveLanguage (lang: LanguageType): Promise<boolean> { return sendMessage('pri(settings.saveLanguage)', { language: lang }); } diff --git a/packages/extension-koni-ui/src/stores/base/RequestState.ts b/packages/extension-koni-ui/src/stores/base/RequestState.ts index b1b2b1039d8..d70a8826bc4 100644 --- a/packages/extension-koni-ui/src/stores/base/RequestState.ts +++ b/packages/extension-koni-ui/src/stores/base/RequestState.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { ConfirmationsQueue, ConfirmationsQueueTon } from '@subwallet/extension-base/background/KoniTypes'; +import { ConfirmationsQueue, ConfirmationsQueueCardano, ConfirmationsQueueTon } from '@subwallet/extension-base/background/KoniTypes'; import { AuthorizeRequest, ConfirmationRequestBase, MetadataRequest, SigningRequest } from '@subwallet/extension-base/background/types'; import { SWTransactionResult } from '@subwallet/extension-base/services/transaction-service/types'; import { WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; @@ -30,6 +30,10 @@ const initialState: RequestState = { tonSendTransactionRequest: {}, tonWatchTransactionRequest: {}, + cardanoSignatureRequest: {}, + cardanoSendTransactionRequest: {}, + cardanoWatchTransactionRequest: {}, + // Summary Info reduxStatus: ReduxStatus.INIT, hasConfirmations: false, @@ -50,6 +54,9 @@ export const CONFIRMATIONS_FIELDS: Array<keyof RequestState> = [ 'tonSignatureRequest', 'tonSendTransactionRequest', 'tonWatchTransactionRequest', + 'cardanoSignatureRequest', + 'cardanoSendTransactionRequest', + 'tonWatchTransactionRequest', 'connectWCRequest', 'notSupportWCRequest' ]; @@ -67,6 +74,7 @@ const readyMap = { updateSigningRequests: false, updateConfirmationRequests: false, updateConfirmationRequestsTon: false, + updateConfirmationRequestCardano: false, updateConnectWalletConnect: false, updateNotSupportWalletConnect: false }; @@ -130,6 +138,11 @@ const requestStateSlice = createSlice({ readyMap.updateConfirmationRequestsTon = true; computeStateSummary(state as RequestState); }, + updateConfirmationRequestsCardano (state, action: PayloadAction<Partial<ConfirmationsQueueCardano>>) { + Object.assign(state, action.payload); + readyMap.updateConfirmationRequestCardano = true; + computeStateSummary(state as RequestState); + }, updateWCNotSupportRequests (state, { payload }: PayloadAction<Record<string, WalletConnectNotSupportRequest>>) { state.notSupportWCRequest = payload; readyMap.updateNotSupportWalletConnect = true; diff --git a/packages/extension-koni-ui/src/stores/types.ts b/packages/extension-koni-ui/src/stores/types.ts index f339820298d..1f9cc6b1ad4 100644 --- a/packages/extension-koni-ui/src/stores/types.ts +++ b/packages/extension-koni-ui/src/stores/types.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _AssetRef, _ChainAsset, _ChainInfo, _MultiChainAsset } from '@subwallet/chain-list/types'; -import { AddressBookState, AllLogoMap, AssetSetting, CampaignBanner, ChainStakingMetadata, ConfirmationDefinitions, ConfirmationsQueue, ConfirmationsQueueTon, ConfirmationType, CrowdloanItem, KeyringState, LanguageType, MantaPayConfig, NftCollection, NftItem, NominatorMetadata, PriceJson, StakingItem, StakingRewardItem, TransactionHistoryItem, UiSettings, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; +import { AddressBookState, AllLogoMap, AssetSetting, CampaignBanner, ChainStakingMetadata, ConfirmationDefinitions, ConfirmationsQueue, ConfirmationsQueueCardano, ConfirmationsQueueTon, ConfirmationType, CrowdloanItem, KeyringState, LanguageType, MantaPayConfig, NftCollection, NftItem, NominatorMetadata, PriceJson, StakingItem, StakingRewardItem, TransactionHistoryItem, UiSettings, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; import { AccountsContext, AuthorizeRequest, MetadataRequest, SigningRequest } from '@subwallet/extension-base/background/types'; import { _ChainApiStatus, _ChainState } from '@subwallet/extension-base/services/chain-service/types'; import { AppBannerData, AppConfirmationData, AppPopupData } from '@subwallet/extension-base/services/mkt-campaign-service/types'; @@ -97,7 +97,7 @@ export interface AccountState extends AccountsContext, KeyringState, AddressBook isAllAccount: boolean } -export interface RequestState extends ConfirmationsQueue, ConfirmationsQueueTon, BaseReduxStore { +export interface RequestState extends ConfirmationsQueue, ConfirmationsQueueTon, ConfirmationsQueueCardano, BaseReduxStore { authorizeRequest: Record<string, AuthorizeRequest>; metadataRequest: Record<string, MetadataRequest>; signingRequest: Record<string, SigningRequest>; diff --git a/packages/extension-koni-ui/src/stores/utils/index.ts b/packages/extension-koni-ui/src/stores/utils/index.ts index a0d892c268c..5109a2b64be 100644 --- a/packages/extension-koni-ui/src/stores/utils/index.ts +++ b/packages/extension-koni-ui/src/stores/utils/index.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _AssetRef, _ChainAsset, _ChainInfo, _MultiChainAsset } from '@subwallet/chain-list/types'; -import { AddressBookInfo, AssetSetting, CampaignBanner, ChainStakingMetadata, ConfirmationsQueue, ConfirmationsQueueTon, CrowdloanJson, KeyringState, MantaPayConfig, MantaPaySyncState, NftCollection, NftJson, NominatorMetadata, PriceJson, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, TransactionHistoryItem, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; +import { AddressBookInfo, AssetSetting, CampaignBanner, ChainStakingMetadata, ConfirmationsQueue, ConfirmationsQueueCardano, ConfirmationsQueueTon, CrowdloanJson, KeyringState, MantaPayConfig, MantaPaySyncState, NftCollection, NftJson, NominatorMetadata, PriceJson, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, TransactionHistoryItem, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; import { AccountsContext, AuthorizeRequest, ConfirmationRequestBase, MetadataRequest, SigningRequest } from '@subwallet/extension-base/background/types'; import { _ChainApiStatus, _ChainState } from '@subwallet/extension-base/services/chain-service/types'; import { AppBannerData, AppConfirmationData, AppPopupData } from '@subwallet/extension-base/services/mkt-campaign-service/types'; @@ -148,6 +148,12 @@ export const updateConfirmationRequestsTon = (data: ConfirmationsQueueTon) => { export const subscribeConfirmationRequestsTon = lazySubscribeMessage('pri(confirmationsTon.subscribe)', null, updateConfirmationRequestsTon, updateConfirmationRequestsTon); +export const updateConfirmationRequestsCardano = (data: ConfirmationsQueueCardano) => { + store.dispatch({ type: 'requestState/updateConfirmationRequestsCardano', payload: data }); +}; + +export const subscribeConfirmationRequestsCardano = lazySubscribeMessage('pri(confirmationsCardano.subscribe)', null, updateConfirmationRequestsCardano, updateConfirmationRequestsCardano); + export const updateTransactionRequests = (data: Record<string, SWTransactionResult>) => { // Convert data to object with key as id diff --git a/packages/extension-koni-ui/src/types/confirmation.ts b/packages/extension-koni-ui/src/types/confirmation.ts index ccdf4f6aafb..4a3edcf0ecc 100644 --- a/packages/extension-koni-ui/src/types/confirmation.ts +++ b/packages/extension-koni-ui/src/types/confirmation.ts @@ -1,9 +1,11 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { ConfirmationDefinitions, ConfirmationDefinitionsTon } from '@subwallet/extension-base/background/KoniTypes'; +import { ConfirmationDefinitions, ConfirmationDefinitionsCardano, ConfirmationDefinitionsTon } from '@subwallet/extension-base/background/KoniTypes'; export type EvmSignatureSupportType = keyof Pick<ConfirmationDefinitions, 'evmSignatureRequest' | 'evmSendTransactionRequest' | 'evmWatchTransactionRequest'>; export type EvmErrorSupportType = keyof Pick<ConfirmationDefinitions, 'errorConnectNetwork'>; export type TonSignatureSupportType = keyof Pick<ConfirmationDefinitionsTon, 'tonSignatureRequest' | 'tonWatchTransactionRequest' | 'tonSendTransactionRequest'>; + +export type CardanoSignatureSupportType = keyof Pick<ConfirmationDefinitionsCardano, 'cardanoSignatureRequest' | 'cardanoWatchTransactionRequest' | 'cardanoSendTransactionRequest'>; diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index ff7a0b65b6e..6b444034250 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -15,6 +15,7 @@ import { getNetworkKeyByGenesisHash } from '@subwallet/extension-koni-ui/utils/c import { AccountInfoByNetwork } from '@subwallet/extension-koni-ui/utils/types'; import { isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; import { KeypairType } from '@subwallet/keyring/types'; +import { Web3LogoMap } from '@subwallet/react-ui/es/config-provider/context'; import { decodeAddress, encodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; @@ -179,6 +180,8 @@ export function getReformatedAddressRelatedToChain (accountJson: AccountJson, ch return accountJson.address; } else if (accountJson.chainType === AccountChainType.TON && chainInfo.tonInfo) { return reformatAddress(accountJson.address, chainInfo.isTestnet ? 0 : 1); + } else if (accountJson.chainType === AccountChainType.CARDANO && chainInfo.cardanoInfo) { + return reformatAddress(accountJson.address, chainInfo.isTestnet ? 0 : 1); } return undefined; @@ -217,3 +220,13 @@ export const isAddressAllowedWithAuthType = (address: string, authAccountTypes?: return false; }; + +export function getChainTypeLogoMap (logoMap: Web3LogoMap): Record<string, string> { + return { + [AccountChainType.SUBSTRATE]: logoMap.network.polkadot as string, + [AccountChainType.ETHEREUM]: logoMap.network.ethereum as string, + [AccountChainType.BITCOIN]: logoMap.network.bitcoin as string, + [AccountChainType.TON]: logoMap.network.ton as string, + [AccountChainType.CARDANO]: logoMap.network.cardano as string + }; +} diff --git a/packages/extension-koni-ui/src/utils/chain/chain.ts b/packages/extension-koni-ui/src/utils/chain/chain.ts index 9e43f24f5d1..93371a8b53b 100644 --- a/packages/extension-koni-ui/src/utils/chain/chain.ts +++ b/packages/extension-koni-ui/src/utils/chain/chain.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _ChainInfo, _ChainStatus } from '@subwallet/chain-list/types'; -import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureSubstrateChain } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureSubstrateChain } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountChainType, AccountProxy } from '@subwallet/extension-base/types'; import { isAccountAll } from '@subwallet/extension-base/utils'; @@ -51,6 +51,10 @@ export const isChainInfoAccordantAccountChainType = (chainInfo: _ChainInfo, chai return _isChainBitcoinCompatible(chainInfo); } + if (chainType === AccountChainType.CARDANO) { + return _isChainCardanoCompatible(chainInfo); + } + return false; }; diff --git a/packages/extension-koni/package.json b/packages/extension-koni/package.json index 6697617a061..9a38da2e6a4 100644 --- a/packages/extension-koni/package.json +++ b/packages/extension-koni/package.json @@ -17,6 +17,7 @@ "version": "1.3.15-0", "dependencies": { "@babel/runtime": "^7.20.6", + "@emurgo/cardano-serialization-lib-browser": "^13.2.0", "@subwallet/extension-base": "^1.3.15-0", "@subwallet/extension-inject": "^1.3.15-0", "@subwallet/extension-koni-ui": "^1.3.15-0" diff --git a/packages/extension-koni/public/images/projects/cardano.png b/packages/extension-koni/public/images/projects/cardano.png new file mode 100644 index 00000000000..13fd5c628cd Binary files /dev/null and b/packages/extension-koni/public/images/projects/cardano.png differ diff --git a/packages/extension-koni/webpack.shared.cjs b/packages/extension-koni/webpack.shared.cjs index 7a530f5c638..f6264ea6564 100644 --- a/packages/extension-koni/webpack.shared.cjs +++ b/packages/extension-koni/webpack.shared.cjs @@ -35,7 +35,8 @@ const packages = [ 'extension-dapp', 'extension-inject', 'extension-koni', - 'extension-koni-ui' + 'extension-koni-ui', + 'subwallet-api-sdk' ]; const _additionalEnv = { @@ -57,7 +58,8 @@ const _additionalEnv = { BITTENSOR_API_KEY_8: JSON.stringify(process.env.BITTENSOR_API_KEY_8), BITTENSOR_API_KEY_9: JSON.stringify(process.env.BITTENSOR_API_KEY_9), BITTENSOR_API_KEY_10: JSON.stringify(process.env.BITTENSOR_API_KEY_10), - SIMPLE_SWAP_API_KEY: JSON.stringify(process.env.SIMPLE_SWAP_API_KEY) + SIMPLE_SWAP_API_KEY: JSON.stringify(process.env.SIMPLE_SWAP_API_KEY), + SUBWALLET_API: JSON.stringify(process.env.SUBWALLET_API) }; const additionalEnvDict = { @@ -159,7 +161,11 @@ module.exports = (entry, alias = {}, isFirefox = false) => { filename: 'notification.html', template: 'public/notification.html', chunks: ['extension'] - }) + }), + new webpack.NormalModuleReplacementPlugin( + /@emurgo\/cardano-serialization-lib-nodejs/, + '@emurgo/cardano-serialization-lib-browser' + ) ], resolve: { alias: packages.reduce((alias, p) => ({ diff --git a/packages/subwallet-api-sdk/LICENSE b/packages/subwallet-api-sdk/LICENSE new file mode 100644 index 00000000000..0d381b2e97d --- /dev/null +++ b/packages/subwallet-api-sdk/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/subwallet-api-sdk/README.md b/packages/subwallet-api-sdk/README.md new file mode 100644 index 00000000000..04a55e38dc6 --- /dev/null +++ b/packages/subwallet-api-sdk/README.md @@ -0,0 +1,2 @@ +# TODO +Update content diff --git a/packages/subwallet-api-sdk/package.json b/packages/subwallet-api-sdk/package.json new file mode 100644 index 00000000000..618b4c7b5c1 --- /dev/null +++ b/packages/subwallet-api-sdk/package.json @@ -0,0 +1,26 @@ +{ + "author": "S2kael<laiducminh1002@gmail.com>", + "bugs": "https://github.com/Koniverse/Subwallet-V2/issues", + "contributors": [], + "description": "Sdk for subwallet api", + "homepage": "https://github.com/Koniverse/Subwallet-V2/tree/master/packages/subwallet-api-sdk#readme", + "license": "Apache-2.0", + "maintainers": [], + "name": "@subwallet/subwallet-api-sdk", + "repository": { + "directory": "packages/subwallet-api-sdk", + "type": "git", + "url": "https://github.com/Koniverse/Subwallet-V2.git" + }, + "sideEffects": [ + "./detectPackage.js", + "./detectPackage.cjs" + ], + "type": "module", + "version": "1.3.12-1", + "main": "index.js", + "dependencies": { + "@polkadot/util": "^13.2.3", + "@subwallet/chain-list": "0.2.97" + } +} diff --git a/packages/subwallet-api-sdk/src/bundle.ts b/packages/subwallet-api-sdk/src/bundle.ts new file mode 100644 index 00000000000..ab9b2abb372 --- /dev/null +++ b/packages/subwallet-api-sdk/src/bundle.ts @@ -0,0 +1,10 @@ +// Copyright 2019-2022 @polkadot/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { SubWalletApiSdk } from './sdk'; + +export { packageInfo } from './packageInfo'; + +export const subwalletApiSdk = SubWalletApiSdk.instance(); + +export { SubWalletApiSdk } from './sdk'; diff --git a/packages/subwallet-api-sdk/src/cardano/index.ts b/packages/subwallet-api-sdk/src/cardano/index.ts new file mode 100644 index 00000000000..df5cde1bb6b --- /dev/null +++ b/packages/subwallet-api-sdk/src/cardano/index.ts @@ -0,0 +1,52 @@ +// Copyright 2017-2022 @subwallet/subwallet-api-sdk authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { BuildCardanoTxParams, getFirstNumberAfterSubstring, POPULAR_CARDANO_ERROR_PHRASE, toUnit } from '@subwallet/subwallet-api-sdk/cardano/utils'; +import { SWApiResponse } from '@subwallet/subwallet-api-sdk/types'; + +export async function fetchUnsignedPayload (baseUrl: string, params: BuildCardanoTxParams) { + const searchParams = new URLSearchParams({ + sender: params.from, + receiver: params.to, + unit: params.cardanoId, + quantity: params.value + }); + + if (params.cardanoTtlOffset) { + searchParams.append('ttl', params.cardanoTtlOffset.toString()); + } + + try { + const rawResponse = await fetch(baseUrl + searchParams.toString(), { + method: 'GET', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json' + } + }); + + const response = await rawResponse.json() as SWApiResponse<string>; + + if (response.error && response.status === 'error') { + throw new Error(response.error.message); + } + + return response.data; + } catch (error) { + const errorMessage = (error as Error).message; + + if (errorMessage.includes(POPULAR_CARDANO_ERROR_PHRASE.NOT_MATCH_MIN_AMOUNT)) { + const minAdaRequiredRaw = getFirstNumberAfterSubstring(errorMessage, POPULAR_CARDANO_ERROR_PHRASE.NOT_MATCH_MIN_AMOUNT); + const minAdaRequired = minAdaRequiredRaw ? toUnit(minAdaRequiredRaw, params.tokenDecimals) : 1; + + throw new Error(`Amount too low. Increase your amount above ${minAdaRequired} ${params.nativeTokenSymbol} and try again`); + } + + if (errorMessage.includes(POPULAR_CARDANO_ERROR_PHRASE.INSUFFICIENT_INPUT)) { + throw new Error(`Insufficient ${params.nativeTokenSymbol} balance to perform transaction. Top up ${params.nativeTokenSymbol} and try again`); + } + + console.error(`Transaction is not built successfully: ${errorMessage}`); + throw new Error('Unable to perform this transaction at the moment. Try again later'); + } +} diff --git a/packages/subwallet-api-sdk/src/cardano/utils.ts b/packages/subwallet-api-sdk/src/cardano/utils.ts new file mode 100644 index 00000000000..9b73cef85c4 --- /dev/null +++ b/packages/subwallet-api-sdk/src/cardano/utils.ts @@ -0,0 +1,36 @@ +// Copyright 2017-2022 @subwallet/subwallet-api-sdk authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export interface BuildCardanoTxParams { + tokenDecimals: number; + nativeTokenSymbol: string; + cardanoId: string; + from: string; + to: string; + value: string; + cardanoTtlOffset: number | null; +} + +export enum POPULAR_CARDANO_ERROR_PHRASE { + NOT_MATCH_MIN_AMOUNT = 'less than the minimum UTXO value', + INSUFFICIENT_INPUT = 'Insufficient input in transaction' +} + +export function getFirstNumberAfterSubstring (inputStr: string, subStr: string) { + const regex = new RegExp(`(${subStr})\\D*(\\d+)`); + const match = inputStr.match(regex); + + if (match) { + return parseInt(match[2], 10); + } else { + return null; + } +} + +export function toUnit (balance: number, decimals: number) { + if (balance === 0) { + return 0; + } + + return balance / (10 ** decimals); +} diff --git a/packages/subwallet-api-sdk/src/detectOther.ts b/packages/subwallet-api-sdk/src/detectOther.ts new file mode 100644 index 00000000000..8762793e3e6 --- /dev/null +++ b/packages/subwallet-api-sdk/src/detectOther.ts @@ -0,0 +1,6 @@ +// Copyright 2017-2022 @subwallet/subwallet-api-sdk authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Empty template, auto-generated by @polkadot/dev + +export default []; diff --git a/packages/subwallet-api-sdk/src/detectPackage.ts b/packages/subwallet-api-sdk/src/detectPackage.ts new file mode 100644 index 00000000000..b6a03e37700 --- /dev/null +++ b/packages/subwallet-api-sdk/src/detectPackage.ts @@ -0,0 +1,11 @@ +// Copyright 2017-2022 @subwallet/subwallet-api-sdk authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Do not edit, auto-generated by @polkadot/dev + +import { detectPackage } from '@polkadot/util'; + +import others from './detectOther'; +import { packageInfo } from './packageInfo'; + +detectPackage(packageInfo, null, others); diff --git a/packages/subwallet-api-sdk/src/index.ts b/packages/subwallet-api-sdk/src/index.ts new file mode 100644 index 00000000000..11267d9d469 --- /dev/null +++ b/packages/subwallet-api-sdk/src/index.ts @@ -0,0 +1,10 @@ +// Copyright 2017-2022 @subwallet/subwallet-api-sdk authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import './detectPackage'; + +import { subwalletApiSdk } from './bundle'; + +export * from './bundle'; + +export default subwalletApiSdk; diff --git a/packages/subwallet-api-sdk/src/packageInfo.ts b/packages/subwallet-api-sdk/src/packageInfo.ts new file mode 100644 index 00000000000..2aefee8f9dc --- /dev/null +++ b/packages/subwallet-api-sdk/src/packageInfo.ts @@ -0,0 +1,6 @@ +// Copyright 2017-2022 @subwallet/subwallet-api-sdk authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Do not edit, auto-generated by @polkadot/dev + +export const packageInfo = { name: '@subwallet/subwallet-api-sdk', path: 'auto', type: 'auto', version: '1.3.12-1' }; diff --git a/packages/subwallet-api-sdk/src/sdk.ts b/packages/subwallet-api-sdk/src/sdk.ts new file mode 100644 index 00000000000..9a226fde45c --- /dev/null +++ b/packages/subwallet-api-sdk/src/sdk.ts @@ -0,0 +1,31 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// TODO: NEED TO UPDATE THIS INTERFACE +import { fetchUnsignedPayload } from '@subwallet/subwallet-api-sdk/cardano'; +import { BuildCardanoTxParams } from '@subwallet/subwallet-api-sdk/cardano/utils'; + +export class SubWalletApiSdk { + private baseUrl = ''; + private static _instance: SubWalletApiSdk | undefined = undefined; + + public init (url: string) { + this.baseUrl = url; + } + + async fetchUnsignedPayload (params: BuildCardanoTxParams): Promise<string> { + const url = `${this.baseUrl}/cardano/build-cardano-tx?`; + + return fetchUnsignedPayload(url, params); + } + + static instance () { + if (this._instance) { + return this._instance; + } + + this._instance = new SubWalletApiSdk(); + + return this._instance; + } +} diff --git a/packages/subwallet-api-sdk/src/types.ts b/packages/subwallet-api-sdk/src/types.ts new file mode 100644 index 00000000000..81516c82c6f --- /dev/null +++ b/packages/subwallet-api-sdk/src/types.ts @@ -0,0 +1,13 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export type ApiStatusValue = 'error' | 'success' | 'fail'; + +export interface SWApiResponse<T> { + status: ApiStatusValue, + data: T, + error?: { + message: string; + code: number; + } +} diff --git a/packages/subwallet-api-sdk/tsconfig.build.json b/packages/subwallet-api-sdk/tsconfig.build.json new file mode 100644 index 00000000000..da24d9f2259 --- /dev/null +++ b/packages/subwallet-api-sdk/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "..", + "outDir": "./build", + "rootDir": "./src" + }, + "references": [] +} diff --git a/packages/subwallet-api-sdk/tsconfig.json b/packages/subwallet-api-sdk/tsconfig.json new file mode 100644 index 00000000000..da24d9f2259 --- /dev/null +++ b/packages/subwallet-api-sdk/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "..", + "outDir": "./build", + "rootDir": "./src" + }, + "references": [] +} diff --git a/packages/web-runner/webpack.config.cjs b/packages/web-runner/webpack.config.cjs index d30b27dd13f..3713ec042dd 100644 --- a/packages/web-runner/webpack.config.cjs +++ b/packages/web-runner/webpack.config.cjs @@ -43,7 +43,8 @@ const packages = [ 'extension-dapp', 'extension-inject', 'extension-koni', - 'extension-koni-ui' + 'extension-koni-ui', + 'subwallet-api-sdk' ]; const polkadotDevOptions = require('@polkadot/dev/config/babel-config-webpack.cjs'); @@ -63,7 +64,8 @@ const _additionalEnv = { BITTENSOR_API_KEY_8: JSON.stringify(process.env.BITTENSOR_API_KEY_8), BITTENSOR_API_KEY_9: JSON.stringify(process.env.BITTENSOR_API_KEY_9), BITTENSOR_API_KEY_10: JSON.stringify(process.env.BITTENSOR_API_KEY_10), - SIMPLE_SWAP_API_KEY: JSON.stringify(process.env.SIMPLE_SWAP_API_KEY) + SIMPLE_SWAP_API_KEY: JSON.stringify(process.env.SIMPLE_SWAP_API_KEY), + SUBWALLET_API: JSON.stringify(process.env.SUBWALLET_API) }; // Overwrite babel babel config from polkadot dev diff --git a/packages/webapp/webpack.config.cjs b/packages/webapp/webpack.config.cjs index f402d5af366..4aa3aeed397 100644 --- a/packages/webapp/webpack.config.cjs +++ b/packages/webapp/webpack.config.cjs @@ -43,7 +43,8 @@ const packages = [ 'extension-dapp', 'extension-inject', 'extension-koni', - 'extension-web-ui' + 'extension-web-ui', + 'subwallet-api-sdk' ]; const polkadotDevOptions = require('@polkadot/dev/config/babel-config-webpack.cjs'); @@ -68,7 +69,8 @@ const _additionalEnv = { BITTENSOR_API_KEY_8: JSON.stringify(process.env.BITTENSOR_API_KEY_8), BITTENSOR_API_KEY_9: JSON.stringify(process.env.BITTENSOR_API_KEY_9), BITTENSOR_API_KEY_10: JSON.stringify(process.env.BITTENSOR_API_KEY_10), - SIMPLE_SWAP_API_KEY: JSON.stringify(process.env.SIMPLE_SWAP_API_KEY) + SIMPLE_SWAP_API_KEY: JSON.stringify(process.env.SIMPLE_SWAP_API_KEY), + SUBWALLET_API: JSON.stringify(process.env.SUBWALLET_API) }; const createConfig = (entry, alias = {}, useSplitChunk = false) => { diff --git a/tsconfig.base.json b/tsconfig.base.json index db4b95306f1..94d22d65b27 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -22,7 +22,9 @@ "@subwallet/web-runner": ["web-runner/src"], "@subwallet/web-runner/*": ["web-runner/src/*"], "@subwallet/webapp": ["webapp/src"], - "@subwallet/webapp/*": ["webapp/src/*"] + "@subwallet/webapp/*": ["webapp/src/*"], + "@subwallet/subwallet-api-sdk": ["subwallet-api-sdk/src"], + "@subwallet/subwallet-api-sdk/*": ["subwallet-api-sdk/src/*"] }, "skipLibCheck": true } diff --git a/tsconfig.build.json b/tsconfig.build.json index 5c08d3bbeb1..46f975c6812 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -12,6 +12,7 @@ { "path": "./packages/extension-inject/tsconfig.build.json" }, { "path": "./packages/extension-mocks/tsconfig.build.json" }, { "path": "./packages/extension-koni-ui/tsconfig.build.json" }, + { "path": "./packages/subwallet-api-sdk/tsconfig.build.json" }, // { "path": "./packages/extension-web-ui/tsconfig.build.json" }, // { "path": "./packages/web-runner/tsconfig.build.json" }, // { "path": "./packages/webapp/tsconfig.build.json" } diff --git a/tsconfig.json b/tsconfig.json index 020908b34ab..c88b445fb1e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ { "path": "./packages/extension-koni" }, { "path": "./packages/extension-koni-ui" }, { "path": "./packages/extension-web-ui" }, - { "path": "./packages/web-runner" } + { "path": "./packages/web-runner" }, + { "path": "./packages/subwallet-api-sdk" } ] } diff --git a/yarn.lock b/yarn.lock index 53252659373..1a3bdea4c9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2125,6 +2125,20 @@ __metadata: languageName: node linkType: hard +"@emurgo/cardano-serialization-lib-browser@npm:^13.2.0": + version: 13.2.0 + resolution: "@emurgo/cardano-serialization-lib-browser@npm:13.2.0" + checksum: 976f8081c7c1ff852463a47da0c8850300af380d0c032d004293c34b557195336a2707c39f24cd98b52de3b379cb770ee6f34107c7f85b3ddac125b2719b17e2 + languageName: node + linkType: hard + +"@emurgo/cardano-serialization-lib-nodejs@npm:^13.2.0": + version: 13.2.0 + resolution: "@emurgo/cardano-serialization-lib-nodejs@npm:13.2.0" + checksum: 547d41e27af3ec8d66f70d679741d18e38f976474c89566d7465151cd892b6a05170c95877206088c547164ad4ef4a7d00563fd29d5429d75ab49740a6605501 + languageName: node + linkType: hard + "@equilab/api@npm:~1.14.25": version: 1.14.25 resolution: "@equilab/api@npm:1.14.25" @@ -4510,90 +4524,90 @@ __metadata: linkType: hard "@polkadot/api-augment@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/api-augment@npm:15.0.1" - dependencies: - "@polkadot/api-base": 15.0.1 - "@polkadot/rpc-augment": 15.0.1 - "@polkadot/types": 15.0.1 - "@polkadot/types-augment": 15.0.1 - "@polkadot/types-codec": 15.0.1 + version: 15.0.2 + resolution: "@polkadot/api-augment@npm:15.0.2" + dependencies: + "@polkadot/api-base": 15.0.2 + "@polkadot/rpc-augment": 15.0.2 + "@polkadot/types": 15.0.2 + "@polkadot/types-augment": 15.0.2 + "@polkadot/types-codec": 15.0.2 "@polkadot/util": ^13.2.3 tslib: ^2.8.0 - checksum: 985919f2154f44516cedf6981a15cb993acb84c6a92f2305832d89b4e4174d3183823e280836db71c93e1072697f09e4d469e83ebdf04a567d48700a18acea86 + checksum: 4262788399081eb37af7757836090cf3ed276871c0765c1cafe57fa6054597686d5959aef125982fb0fdb43e70c3959b29bfd0b5681480dab39dff0d16e5bb86 languageName: node linkType: hard "@polkadot/api-base@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/api-base@npm:15.0.1" + version: 15.0.2 + resolution: "@polkadot/api-base@npm:15.0.2" dependencies: - "@polkadot/rpc-core": 15.0.1 - "@polkadot/types": 15.0.1 + "@polkadot/rpc-core": 15.0.2 + "@polkadot/types": 15.0.2 "@polkadot/util": ^13.2.3 rxjs: ^7.8.1 tslib: ^2.8.0 - checksum: 0d37180b934c1049d1bb2caa7d9cd53bd1882612820664b65b73f43f0de4e85142a8dfe117558e9385924841ec4fc934b5152a5280e8c74834cf541008033286 + checksum: f46c078e569e147fbcf65a2c055329f2ee3a066c64187f9f6b2fc0dd1b784fffedcd691bd28538f076cddaadd6950eaec4ff2531e382ec0ba020d52395264b3f languageName: node linkType: hard "@polkadot/api-contract@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/api-contract@npm:15.0.1" - dependencies: - "@polkadot/api": 15.0.1 - "@polkadot/api-augment": 15.0.1 - "@polkadot/types": 15.0.1 - "@polkadot/types-codec": 15.0.1 - "@polkadot/types-create": 15.0.1 + version: 15.0.2 + resolution: "@polkadot/api-contract@npm:15.0.2" + dependencies: + "@polkadot/api": 15.0.2 + "@polkadot/api-augment": 15.0.2 + "@polkadot/types": 15.0.2 + "@polkadot/types-codec": 15.0.2 + "@polkadot/types-create": 15.0.2 "@polkadot/util": ^13.2.3 "@polkadot/util-crypto": ^13.2.3 rxjs: ^7.8.1 tslib: ^2.8.0 - checksum: ca3ac606b54da89efb25523f532fe33304ad85f687e4ac3c685a74e213c19678e0d2b1444de460784d28d2821b39cfd7738a53bec116a8ad616f4375274ab4d3 + checksum: 7c8a3cac56fc03fbc99604f221168659a516390d63f3ede40a8027e0159094f3a3a277c9d5e4dcdbcf21f31b815b71d51c097a028c84cce0248c7f3fb49b9394 languageName: node linkType: hard "@polkadot/api-derive@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/api-derive@npm:15.0.1" - dependencies: - "@polkadot/api": 15.0.1 - "@polkadot/api-augment": 15.0.1 - "@polkadot/api-base": 15.0.1 - "@polkadot/rpc-core": 15.0.1 - "@polkadot/types": 15.0.1 - "@polkadot/types-codec": 15.0.1 + version: 15.0.2 + resolution: "@polkadot/api-derive@npm:15.0.2" + dependencies: + "@polkadot/api": 15.0.2 + "@polkadot/api-augment": 15.0.2 + "@polkadot/api-base": 15.0.2 + "@polkadot/rpc-core": 15.0.2 + "@polkadot/types": 15.0.2 + "@polkadot/types-codec": 15.0.2 "@polkadot/util": ^13.2.3 "@polkadot/util-crypto": ^13.2.3 rxjs: ^7.8.1 tslib: ^2.8.0 - checksum: adc1a74e5a6964d21f11c5fdfd5958d31126c2a5071acde17a1fe604885fd45230d62f1195131a40f9c5347e5101825a515b2baa7fecef12214b0fea792e273d + checksum: 23f8fb2f0efae0027a0c9891a46edefea209ed84f25e5145ad023130a20567e5e069d2c98bfb63a073c092683aae083aad614b8a947f5795b13b4c64f2ca8103 languageName: node linkType: hard "@polkadot/api@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/api@npm:15.0.1" + version: 15.0.2 + resolution: "@polkadot/api@npm:15.0.2" dependencies: - "@polkadot/api-augment": 15.0.1 - "@polkadot/api-base": 15.0.1 - "@polkadot/api-derive": 15.0.1 + "@polkadot/api-augment": 15.0.2 + "@polkadot/api-base": 15.0.2 + "@polkadot/api-derive": 15.0.2 "@polkadot/keyring": ^13.2.3 - "@polkadot/rpc-augment": 15.0.1 - "@polkadot/rpc-core": 15.0.1 - "@polkadot/rpc-provider": 15.0.1 - "@polkadot/types": 15.0.1 - "@polkadot/types-augment": 15.0.1 - "@polkadot/types-codec": 15.0.1 - "@polkadot/types-create": 15.0.1 - "@polkadot/types-known": 15.0.1 + "@polkadot/rpc-augment": 15.0.2 + "@polkadot/rpc-core": 15.0.2 + "@polkadot/rpc-provider": 15.0.2 + "@polkadot/types": 15.0.2 + "@polkadot/types-augment": 15.0.2 + "@polkadot/types-codec": 15.0.2 + "@polkadot/types-create": 15.0.2 + "@polkadot/types-known": 15.0.2 "@polkadot/util": ^13.2.3 "@polkadot/util-crypto": ^13.2.3 eventemitter3: ^5.0.1 rxjs: ^7.8.1 tslib: ^2.8.0 - checksum: 0a717efee25f2080ecd8bd1eb79ab3459d0352b4c235aac077563ac74988fdbfdddff80e962e7787df9b4ebb7c3875d4db14540735a6f3cce5ca7f916c879c48 + checksum: 844e488b2a34f928a72dffd5cb21228eed1fa86f801843a44762f446f781dc18f1920e13da7372d3080d7c619abb9c517aae3832ea98deab62d2139bee5cb169 languageName: node linkType: hard @@ -5032,39 +5046,39 @@ __metadata: linkType: hard "@polkadot/rpc-augment@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/rpc-augment@npm:15.0.1" + version: 15.0.2 + resolution: "@polkadot/rpc-augment@npm:15.0.2" dependencies: - "@polkadot/rpc-core": 15.0.1 - "@polkadot/types": 15.0.1 - "@polkadot/types-codec": 15.0.1 + "@polkadot/rpc-core": 15.0.2 + "@polkadot/types": 15.0.2 + "@polkadot/types-codec": 15.0.2 "@polkadot/util": ^13.2.3 tslib: ^2.8.0 - checksum: 544a0345513edd3f3bcc9132ab49bb8919215a6e5893786cf4e5875ee2ba7beff853c364b92738192d3a08b0e5a8dcc9927d125f71b53faaae75aed8f05a2507 + checksum: a8c322a2e7ccb0fc3806e804c52f9e532d169f0c6ab17acc2b0434065cfc445eec9ee0dbe905a7b4685a7fae04c37df9d8436de5e3a3823970cf402a892f3c9f languageName: node linkType: hard "@polkadot/rpc-core@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/rpc-core@npm:15.0.1" + version: 15.0.2 + resolution: "@polkadot/rpc-core@npm:15.0.2" dependencies: - "@polkadot/rpc-augment": 15.0.1 - "@polkadot/rpc-provider": 15.0.1 - "@polkadot/types": 15.0.1 + "@polkadot/rpc-augment": 15.0.2 + "@polkadot/rpc-provider": 15.0.2 + "@polkadot/types": 15.0.2 "@polkadot/util": ^13.2.3 rxjs: ^7.8.1 tslib: ^2.8.0 - checksum: eaed371caefa16c22cb36ee183de8f8bd063c50b7565259d76478d5c005b2cd9f919c6740430d7d110548c8cc8a31d02e18ef007dcd269d8d83202bb4b5de9d6 + checksum: dc282499840d64ab7805ba151ca9d8c932119b8c1d793328dbab00139cc66b29c54e2a0fc135fb69f2319be8e3cc0699b4562c8f2a33c397f0583bf7bdee3bb2 languageName: node linkType: hard "@polkadot/rpc-provider@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/rpc-provider@npm:15.0.1" + version: 15.0.2 + resolution: "@polkadot/rpc-provider@npm:15.0.2" dependencies: "@polkadot/keyring": ^13.2.3 - "@polkadot/types": 15.0.1 - "@polkadot/types-support": 15.0.1 + "@polkadot/types": 15.0.2 + "@polkadot/types-support": 15.0.2 "@polkadot/util": ^13.2.3 "@polkadot/util-crypto": ^13.2.3 "@polkadot/x-fetch": ^13.2.3 @@ -5078,19 +5092,19 @@ __metadata: dependenciesMeta: "@substrate/connect": optional: true - checksum: 91fe12ac4f76ab29c6c85d8db3b9097593aa8510e65ecfd2bde62dd5f478b203240b6c6935257cab8fc717bba536b161e1f4050d8db05177d671af70be031715 + checksum: 5c72d9370cf442d87c476703023e46aafff7414c74d9c01cb4f45c0e65a9a73d2a058c467163428fbc0280a3b7470eb312329fee79e17797e7e05839d65c8373 languageName: node linkType: hard -"@polkadot/types-augment@npm:15.0.1, @polkadot/types-augment@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/types-augment@npm:15.0.1" +"@polkadot/types-augment@npm:15.0.2, @polkadot/types-augment@npm:^15.0.1": + version: 15.0.2 + resolution: "@polkadot/types-augment@npm:15.0.2" dependencies: - "@polkadot/types": 15.0.1 - "@polkadot/types-codec": 15.0.1 + "@polkadot/types": 15.0.2 + "@polkadot/types-codec": 15.0.2 "@polkadot/util": ^13.2.3 tslib: ^2.8.0 - checksum: e3b4da599bae7469466b8e6d917c0c470c9e48e23148e31264cdb36d1101c485094a6c5f19bcbb5f587836b4bc8efe79673966903895e6edb3e84cb488dcecd3 + checksum: 0ecbe404963d0a2f2933a31d278a65ef30eccb0d3fe8df0f686edbd5af31a99f477347cdb192f45ec8e13acb2328a8c9703f15fb713d0af8985741c7bad46ba7 languageName: node linkType: hard @@ -5107,64 +5121,64 @@ __metadata: linkType: hard "@polkadot/types-codec@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/types-codec@npm:15.0.1" + version: 15.0.2 + resolution: "@polkadot/types-codec@npm:15.0.2" dependencies: "@polkadot/util": ^13.2.3 "@polkadot/x-bigint": ^13.2.3 tslib: ^2.8.0 - checksum: 647e2822e4587241be2c2fe355d8ca2c324be0366e5f274e5a0530d5a74e63c92cc73d4babb778c2913f6f2b191851394c9c5e7005e3eaaa41a52981b3b325e7 + checksum: 835be95b20bb51913050807becaffa1ab772c6659eb9f57d506aaf63a5cde93f202876a5b5c078fd6978dcfb0a5b9461298b8826e47d0f0ee96f8180ca18c19a languageName: node linkType: hard -"@polkadot/types-create@npm:15.0.1": - version: 15.0.1 - resolution: "@polkadot/types-create@npm:15.0.1" +"@polkadot/types-create@npm:15.0.2": + version: 15.0.2 + resolution: "@polkadot/types-create@npm:15.0.2" dependencies: - "@polkadot/types-codec": 15.0.1 + "@polkadot/types-codec": 15.0.2 "@polkadot/util": ^13.2.3 tslib: ^2.8.0 - checksum: 20abe0c198400d664a9cb182932054177ba655935955fa50f4a3035c3637d2f24987d5d3fe33f79b72c8bc765413cfaec77190e840d65652b047922ca35d6148 + checksum: 3972b5d919ffade3b85fc7c7fb79fc90d30151ed7af078a54f218137c8db8799e576cf9b2fc74db97f9a4c28d6e60483be446a2a9b42c0b7e61331efb3615869 languageName: node linkType: hard "@polkadot/types-known@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/types-known@npm:15.0.1" + version: 15.0.2 + resolution: "@polkadot/types-known@npm:15.0.2" dependencies: "@polkadot/networks": ^13.2.3 - "@polkadot/types": 15.0.1 - "@polkadot/types-codec": 15.0.1 - "@polkadot/types-create": 15.0.1 + "@polkadot/types": 15.0.2 + "@polkadot/types-codec": 15.0.2 + "@polkadot/types-create": 15.0.2 "@polkadot/util": ^13.2.3 tslib: ^2.8.0 - checksum: 137f76c37dac52ef6e45167074198683ef7d44a72fee0bafc51b902977ac72b5aac880d71915948eaee0ac6bbd629897d45dfbcd9333684cf70858ba35b03977 + checksum: bbf684baed7f00308e808f4c32ee894d59407cdf947532bb6e1e5e4763b1451d5a684fa76c86c6392721f80b0509987a39664d5ca9b52a9177a35cd4924758fe languageName: node linkType: hard "@polkadot/types-support@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/types-support@npm:15.0.1" + version: 15.0.2 + resolution: "@polkadot/types-support@npm:15.0.2" dependencies: "@polkadot/util": ^13.2.3 tslib: ^2.8.0 - checksum: 810d827395982f3b80bcdf01e8b85cbb0808812001c088aa41b8cbf031b2dbb620243c21b6b0e1204273dad022355607257095d0c60763acbe5562a7d6049f37 + checksum: bad355ee97ce1b8194832b78da7a2bf5c199c4d8eb810a52fcdab1e71b0188c7737f716df9705891f363c029abb8cc9f1c83e8e7b11409a959be03c007274ec9 languageName: node linkType: hard "@polkadot/types@npm:^15.0.1": - version: 15.0.1 - resolution: "@polkadot/types@npm:15.0.1" + version: 15.0.2 + resolution: "@polkadot/types@npm:15.0.2" dependencies: "@polkadot/keyring": ^13.2.3 - "@polkadot/types-augment": 15.0.1 - "@polkadot/types-codec": 15.0.1 - "@polkadot/types-create": 15.0.1 + "@polkadot/types-augment": 15.0.2 + "@polkadot/types-codec": 15.0.2 + "@polkadot/types-create": 15.0.2 "@polkadot/util": ^13.2.3 "@polkadot/util-crypto": ^13.2.3 rxjs: ^7.8.1 tslib: ^2.8.0 - checksum: 4bf7aa67919df55a2726a163e6640cd12b7e7008f7644cfd4cf53ca30920070bc2782abcb66d8fd6873c64666f03876e3606176260500a3079035d2268c9b493 + checksum: 6daf5cc97683945055cca1ba33fd4c590c31219de961eb34eff22cf3feea7b809b59766fcc7b9edf6ae8bfc04b6c8bc286c9e496adc650dc6361fb29d03f85f0 languageName: node linkType: hard @@ -6368,9 +6382,9 @@ __metadata: linkType: hard "@substrate/connect-known-chains@npm:^1.1.5": - version: 1.8.0 - resolution: "@substrate/connect-known-chains@npm:1.8.0" - checksum: ccf536ca2fb6bdfc9f3a85ff0036eebc403ef743f8fbf6290a25439184375e4e90f9ce20ed414fc9c5231b22cfa3382b135bd865ab61cc3d1378959bef0c9ee4 + version: 1.8.1 + resolution: "@substrate/connect-known-chains@npm:1.8.1" + checksum: 35ed5ed36c38cc66e3ef48d942c42cf1686e369fa6c21faef2bb318514fa1596c30900d0963498ba7a3bb992b87a80f333b8bbfef14915d0c98edac807125010 languageName: node linkType: hard @@ -6439,15 +6453,14 @@ __metadata: languageName: node linkType: hard -"@subwallet/chain-list@npm:0.2.98": - version: 0.2.98 - resolution: "@subwallet/chain-list@npm:0.2.98" +"@subwallet/chain-list@file:../SubWallet-Chainlist/packages/chain-list/build/::locator=root-workspace-0b6124%40workspace%3A.": + version: 0.2.91 + resolution: "@subwallet/chain-list@file:../SubWallet-Chainlist/packages/chain-list/build/#../SubWallet-Chainlist/packages/chain-list/build/::hash=4282d2&locator=root-workspace-0b6124%40workspace%3A." dependencies: "@polkadot/dev": 0.67.167 "@polkadot/util": ^12.5.1 eventemitter3: ^5.0.1 - ts-md5: ^1.3.1 - checksum: bf03699ae2f5eb97f49ebbdb92416095230e38b104a0c945884c4f05447adf728c722b57dd145805231433468faa471bd5d6d8b71e7c6fc307a32724897edb71 + checksum: 09f75a4d785258efce275f7bba01f6b3aa48a7550458354527daa7d3e64d59250b2f07189adfa77dda8e62ab9dbcaf4a204e0027a942414e855a6659cbb3b3ca languageName: node linkType: hard @@ -6459,6 +6472,7 @@ __metadata: "@apollo/client": ^3.7.14 "@azns/resolver-core": ^1.4.0 "@chainflip/sdk": ^1.6.0 + "@emurgo/cardano-serialization-lib-nodejs": ^13.2.0 "@equilab/api": ~1.14.25 "@ethereumjs/common": ^4.1.0 "@ethereumjs/tx": ^5.1.0 @@ -6497,6 +6511,7 @@ __metadata: "@subwallet/extension-inject": ^1.3.15-0 "@subwallet/extension-mocks": ^1.3.15-0 "@subwallet/keyring": ^0.1.8-beta.0 + "@subwallet/subwallet-api-sdk": ^1.3.12-1 "@subwallet/ui-keyring": ^0.1.8-beta.0 "@ton/core": ^0.56.3 "@ton/crypto": ^3.2.0 @@ -6706,6 +6721,7 @@ __metadata: resolution: "@subwallet/extension-koni@workspace:packages/extension-koni" dependencies: "@babel/runtime": ^7.20.6 + "@emurgo/cardano-serialization-lib-browser": ^13.2.0 "@polkadot/dev": ^0.65.23 "@subwallet/extension-base": ^1.3.15-0 "@subwallet/extension-inject": ^1.3.15-0 @@ -6840,10 +6856,11 @@ __metadata: languageName: unknown linkType: soft -"@subwallet/keyring@npm:^0.1.8-beta.0": - version: 0.1.8-beta.0 - resolution: "@subwallet/keyring@npm:0.1.8-beta.0" +"@subwallet/keyring@file:../SubWallet-Base/packages/keyring/build/::locator=root-workspace-0b6124%40workspace%3A.": + version: 0.1.7 + resolution: "@subwallet/keyring@file:../SubWallet-Base/packages/keyring/build/#../SubWallet-Base/packages/keyring/build/::hash=92482e&locator=root-workspace-0b6124%40workspace%3A." dependencies: + "@emurgo/cardano-serialization-lib-nodejs": ^13.2.0 "@ethereumjs/tx": ^5.0.0 "@metamask/eth-sig-util": ^7.0.3 "@metamask/eth-simple-keyring": ^6.0.1 @@ -6862,7 +6879,7 @@ __metadata: rxjs: ^7.5.6 tiny-secp256k1: ^2.2.3 tslib: ^2.6.2 - checksum: f29e78fd71ddf09c8e531b682f93b49305580ab2c266d781558322d6af5d0bfae8d80ae21e0c35d7a25711a270991e290cbe49d891b6b47388bb566aa5befb06 + checksum: 1f49ff91a60e4ee0f8e72a2f8f3d0a3083098ba9aa2d5f0d8b185f74367add12864c1fdfe2666aa87f420642cfa5c36254c561f319b1d91009f8e8129fb7ce13 languageName: node linkType: hard @@ -6944,19 +6961,28 @@ __metadata: languageName: node linkType: hard -"@subwallet/ui-keyring@npm:0.1.8-beta.0": - version: 0.1.8-beta.0 - resolution: "@subwallet/ui-keyring@npm:0.1.8-beta.0" +"@subwallet/subwallet-api-sdk@^1.3.12-1, @subwallet/subwallet-api-sdk@workspace:packages/subwallet-api-sdk": + version: 0.0.0-use.local + resolution: "@subwallet/subwallet-api-sdk@workspace:packages/subwallet-api-sdk" + dependencies: + "@polkadot/util": ^13.2.3 + "@subwallet/chain-list": 0.2.97 + languageName: unknown + linkType: soft + +"@subwallet/ui-keyring@file:../SubWallet-Base/packages/ui-keyring/build/::locator=root-workspace-0b6124%40workspace%3A.": + version: 0.1.7 + resolution: "@subwallet/ui-keyring@file:../SubWallet-Base/packages/ui-keyring/build/#../SubWallet-Base/packages/ui-keyring/build/::hash=a1c075&locator=root-workspace-0b6124%40workspace%3A." dependencies: "@babel/runtime": ^7.20.1 "@polkadot/ui-settings": 2.9.14 "@polkadot/util": ^12.2.1 "@polkadot/util-crypto": ^12.2.1 - "@subwallet/keyring": ^0.1.8-beta.0 + "@subwallet/keyring": ^0.1.7 mkdirp: ^1.0.4 rxjs: ^7.5.7 store: ^2.0.12 - checksum: 6ebb69675403eb36300b0aea895364852bff24f05a69ab69f5b7a6734c3f4a2c27ca5a9b5f3d5c1b2a2e970bf17a22a0b8985d90f1d237871f3132425cd6ef70 + checksum: 590f3230b812b5405b5094da4565e798edfe670efb3839e41670d83ba51e372788dbc523e5365764625ef418b7be6e88ba6a949023e3b7023c7931afea52d946 languageName: node linkType: hard @@ -7056,10 +7082,10 @@ __metadata: languageName: node linkType: hard -"@thi.ng/api@npm:^8.11.13": - version: 8.11.13 - resolution: "@thi.ng/api@npm:8.11.13" - checksum: dc29cd79d8158cdda6766fee2d7fbb34661acc1dfac709f30a58fb062562cbf929cbe3a6ca6f4856a121594acdd7b5292981595bee71cc4dd0fd99e35aa0c1ef +"@thi.ng/api@npm:^8.11.14": + version: 8.11.14 + resolution: "@thi.ng/api@npm:8.11.14" + checksum: 052da7c146c826c2112b236d478a285d1c9da0fb061dac372826232dbbab0541f23a97f1a4410f79e6428a10c56bfce00307481b52a3793e304cb165dd8d3952 languageName: node linkType: hard @@ -7169,11 +7195,11 @@ __metadata: linkType: hard "@thi.ng/memoize@npm:^4.0.2": - version: 4.0.3 - resolution: "@thi.ng/memoize@npm:4.0.3" + version: 4.0.4 + resolution: "@thi.ng/memoize@npm:4.0.4" dependencies: - "@thi.ng/api": ^8.11.13 - checksum: ddc13c6a6ed3af467079fca5ad338dc469153228b6b41c302927d2ac38eb68340a1c925b688bb5b75c13fa0df370936b8a57b2cdbe5ed3900e6faf6e4c944e88 + "@thi.ng/api": ^8.11.14 + checksum: d3aeb17624f972cdc69a0ede55aef2427e61387b4df8c493c3ae54fa84711a7ac3676bba28fcef1c6b716272ca8b18018653bd6491fa01fb50488387bd57e8d4 languageName: node linkType: hard