diff --git a/local-libs/keyring/cjs/pair/index.js b/local-libs/keyring/cjs/pair/index.js index 3ea2a566a03..965b08a726f 100644 --- a/local-libs/keyring/cjs/pair/index.js +++ b/local-libs/keyring/cjs/pair/index.js @@ -5,8 +5,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.createPair = createPair; +var _bip322Js = require("bip322-js"); var bitcoin = _interopRequireWildcard(require("bitcoinjs-lib")); -var _bitcoinjsMessage = require("bitcoinjs-message"); var _ecpair = require("ecpair"); var _ethSimpleKeyring = _interopRequireDefault(require("eth-simple-keyring")); var ecc = _interopRequireWildcard(require("tiny-secp256k1")); @@ -108,13 +108,13 @@ function createPair(_ref, _ref2) { const encodeAddress = () => { const raw = _utils.TYPE_ADDRESS[type](publicKey); const bitNetwork = ['bitcoin-44', 'bitcoin-84', 'bitcoin-86'].includes(type) ? bitcoin.networks.bitcoin : ['bittest-44', 'bittest-84', 'bittest-86'].includes(type) ? bitcoin.networks.testnet : bitcoin.networks.regtest; + let dataKey; /** * With bitcoin accounts, some attached account have no public key (only address). * In this case, public key is the hash of result after decoded address. * Add `noPublicKey` in metadata for this case. */ - let dataKey; if (meta.noPublicKey) { dataKey = 'hash'; } else { @@ -316,21 +316,24 @@ function createPair(_ref, _ref2) { type }, derived, meta, null); }, - signMessage: (message, compressed, options) => { + signMessage: message => { if (isLocked(secretKey)) { throw new Error('Cannot encrypt with a locked key pair'); } - const _message = typeof message === 'string' ? message : Buffer.from(message); + const _message = typeof message === 'string' ? message : (0, _util.u8aToString)(message); + const address = encodeAddress(); + const _pair = ECPair.fromPrivateKey(Buffer.from(secretKey)); + const wif = _pair.toWIF(); // Sign the message - const signature = (0, _bitcoinjsMessage.sign)(_message, Buffer.from(secretKey), compressed, options); - return signature.toString('base64'); + const signature = _bip322Js.Signer.sign(wif, address, _message); + return typeof signature === 'string' ? signature : signature.toString('base64'); }, - signTransaction: (transaction, indexes) => { + signTransaction: (psbt, indexes, sighashTypes, tapLeafHashToSign) => { if (isLocked(secretKey)) { throw new Error('Cannot encrypt with a locked key pair'); } - if (!transaction) { + if (!psbt) { throw new Error('Not found sign method'); } const pair = ECPair.fromPrivateKey(Buffer.from(secretKey)); @@ -338,12 +341,12 @@ function createPair(_ref, _ref2) { for (const index of indexes) { if (isTaproot) { const tweakedSigner = pair.tweak(bitcoin.crypto.taggedHash('TapTweak', toXOnly(pair.publicKey))); - transaction.signTaprootInput(index, tweakedSigner); + psbt.signTaprootInput(index, tweakedSigner, tapLeafHashToSign, sighashTypes); } else { - transaction.signInput(index, pair); + psbt.signInput(index, pair, sighashTypes); } } - return transaction; + return psbt; }, get output() { return output || Buffer.from([]); diff --git a/local-libs/keyring/cjs/types.js b/local-libs/keyring/cjs/types.js index 765402d2755..f7de91985af 100644 --- a/local-libs/keyring/cjs/types.js +++ b/local-libs/keyring/cjs/types.js @@ -3,13 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.BitcoinAddressType = void 0; +exports.SubstrateKeypairTypes = exports.EthereumKeypairTypes = exports.BitcoinKeypairTypes = exports.BitcoinAddressType = void 0; // Copyright 2017-2022 @polkadot/keyring authors & contributors // SPDX-License-Identifier: Apache-2.0 + /** * * * */ + +const SubstrateKeypairTypes = ['sr25519', 'ed25519', 'ecdsa']; +exports.SubstrateKeypairTypes = SubstrateKeypairTypes; +const EthereumKeypairTypes = ['ethereum']; +exports.EthereumKeypairTypes = EthereumKeypairTypes; +const BitcoinKeypairTypes = ['bitcoin-44', 'bitcoin-84', 'bitcoin-86', 'bittest-44', 'bittest-84', 'bittest-86']; +exports.BitcoinKeypairTypes = BitcoinKeypairTypes; let BitcoinAddressType; exports.BitcoinAddressType = BitcoinAddressType; (function (BitcoinAddressType) { diff --git a/local-libs/keyring/cjs/utils/derive-path.js b/local-libs/keyring/cjs/utils/derive-path.js index 7412fefe285..14848b69952 100644 --- a/local-libs/keyring/cjs/utils/derive-path.js +++ b/local-libs/keyring/cjs/utils/derive-path.js @@ -15,7 +15,7 @@ const getEvmDerivePath = index => { return emvPath.replace('{index}', index.toString()); }; exports.getEvmDerivePath = getEvmDerivePath; -const getBitDerivePathFunction = (proposal, slip44) => { +const getBitDerivePathFunction = (slip44, proposal) => { const path = bitPath.replace('{proposal}', proposal.toString()).replace('{slip44}', slip44.toString()); return index => { return path.replace('{index}', index.toString()); @@ -27,17 +27,17 @@ const getDerivePath = type => { case 'ethereum': return getEvmDerivePath; case 'bitcoin-44': - return getBitDerivePathFunction(44, 0); + return getBitDerivePathFunction(0, 44); case 'bitcoin-84': - return getBitDerivePathFunction(84, 0); + return getBitDerivePathFunction(0, 84); case 'bitcoin-86': - return getBitDerivePathFunction(86, 0); + return getBitDerivePathFunction(0, 86); case 'bittest-44': - return getBitDerivePathFunction(44, 1); + return getBitDerivePathFunction(1, 44); case 'bittest-84': - return getBitDerivePathFunction(84, 1); + return getBitDerivePathFunction(1, 84); case 'bittest-86': - return getBitDerivePathFunction(86, 1); + return getBitDerivePathFunction(1, 86); default: return () => ''; } diff --git a/local-libs/keyring/package.json b/local-libs/keyring/package.json index 1767e9b1c39..1690caaf2b2 100644 --- a/local-libs/keyring/package.json +++ b/local-libs/keyring/package.json @@ -205,6 +205,7 @@ "@polkadot/util-crypto": "^12.2.1", "bcryptjs": "^2.4.3", "bignumber.js": "^9.1.2", + "bip322-js": "^2.0.0", "bitcoinjs-lib": "^6.1.5", "bitcoinjs-message": "^2.2.0", "ecpair": "^2.1.0", diff --git a/local-libs/keyring/pair/index.js b/local-libs/keyring/pair/index.js index e41a426a330..ccf880aae23 100644 --- a/local-libs/keyring/pair/index.js +++ b/local-libs/keyring/pair/index.js @@ -1,12 +1,12 @@ // Copyright 2017-2022 @polkadot/keyring authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { Signer } from 'bip322-js'; import * as bitcoin from 'bitcoinjs-lib'; -import { sign as bitcoinSignMessage } from 'bitcoinjs-message'; import { ECPairFactory } from 'ecpair'; import SimpleKeyring from 'eth-simple-keyring'; import * as ecc from 'tiny-secp256k1'; -import { hexAddPrefix, hexStripPrefix, objectSpread, u8aConcat, u8aEmpty, u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util'; +import { hexAddPrefix, hexStripPrefix, objectSpread, u8aConcat, u8aEmpty, u8aEq, u8aToHex, u8aToString, u8aToU8a } from '@polkadot/util'; import { blake2AsU8a, ethereumEncode, hdEthereum, keyExtractPath, mnemonicToLegacySeed, secp256k1Compress, signatureVerify, sr25519VrfSign, sr25519VrfVerify } from '@polkadot/util-crypto'; import { entropyToMnemonic } from '@polkadot/util-crypto/mnemonic/bip39'; import { getDerivePath, keyFromPath, TYPE_ADDRESS, TYPE_FROM_SEED, TYPE_PREFIX, TYPE_SIGNATURE } from "../utils/index.js"; @@ -94,13 +94,13 @@ export function createPair({ const encodeAddress = () => { const raw = TYPE_ADDRESS[type](publicKey); const bitNetwork = ['bitcoin-44', 'bitcoin-84', 'bitcoin-86'].includes(type) ? bitcoin.networks.bitcoin : ['bittest-44', 'bittest-84', 'bittest-86'].includes(type) ? bitcoin.networks.testnet : bitcoin.networks.regtest; + let dataKey; /** * With bitcoin accounts, some attached account have no public key (only address). * In this case, public key is the hash of result after decoded address. * Add `noPublicKey` in metadata for this case. */ - let dataKey; if (meta.noPublicKey) { dataKey = 'hash'; } else { @@ -301,21 +301,24 @@ export function createPair({ type }, derived, meta, null); }, - signMessage: (message, compressed, options) => { + signMessage: message => { if (isLocked(secretKey)) { throw new Error('Cannot encrypt with a locked key pair'); } - const _message = typeof message === 'string' ? message : Buffer.from(message); + const _message = typeof message === 'string' ? message : u8aToString(message); + const address = encodeAddress(); + const _pair = ECPair.fromPrivateKey(Buffer.from(secretKey)); + const wif = _pair.toWIF(); // Sign the message - const signature = bitcoinSignMessage(_message, Buffer.from(secretKey), compressed, options); - return signature.toString('base64'); + const signature = Signer.sign(wif, address, _message); + return typeof signature === 'string' ? signature : signature.toString('base64'); }, - signTransaction: (transaction, indexes) => { + signTransaction: (psbt, indexes, sighashTypes, tapLeafHashToSign) => { if (isLocked(secretKey)) { throw new Error('Cannot encrypt with a locked key pair'); } - if (!transaction) { + if (!psbt) { throw new Error('Not found sign method'); } const pair = ECPair.fromPrivateKey(Buffer.from(secretKey)); @@ -323,12 +326,12 @@ export function createPair({ for (const index of indexes) { if (isTaproot) { const tweakedSigner = pair.tweak(bitcoin.crypto.taggedHash('TapTweak', toXOnly(pair.publicKey))); - transaction.signTaprootInput(index, tweakedSigner); + psbt.signTaprootInput(index, tweakedSigner, tapLeafHashToSign, sighashTypes); } else { - transaction.signInput(index, pair); + psbt.signInput(index, pair, sighashTypes); } } - return transaction; + return psbt; }, get output() { return output || Buffer.from([]); diff --git a/local-libs/keyring/types.d.ts b/local-libs/keyring/types.d.ts index 7797ae55742..53d1b857cf1 100644 --- a/local-libs/keyring/types.d.ts +++ b/local-libs/keyring/types.d.ts @@ -4,7 +4,6 @@ import type { HexString } from '@polkadot/util/types'; import type { EncryptedJson, Keypair, Prefix } from '@polkadot/util-crypto/types'; import { TypedTransaction } from '@ethereumjs/tx'; import { Psbt as BitcoinTransaction } from 'bitcoinjs-lib'; -import { SignatureOptions } from 'bitcoinjs-message'; /** * * @@ -58,8 +57,8 @@ export interface SubstrateSigner { } export interface BitcoinSigner { derive: (index: number, meta?: KeyringPair$Meta) => KeyringPair; - signMessage: (message: HexString | string | Uint8Array, compressed?: boolean, options?: SignatureOptions) => string; - signTransaction: (transaction: BitcoinTransaction, indexes: number[]) => BitcoinTransaction; + signMessage: (message: HexString | string | Uint8Array) => string; + signTransaction: (transaction: BitcoinTransaction, indexes: number[], sighashTypes?: number[], tapLeafHashToSign?: Buffer | undefined) => BitcoinTransaction; output: Buffer; internalPubkey: Buffer; } diff --git a/local-libs/keyring/utils/derive-path.d.ts b/local-libs/keyring/utils/derive-path.d.ts index 06c925adc30..341cf531a0f 100644 --- a/local-libs/keyring/utils/derive-path.d.ts +++ b/local-libs/keyring/utils/derive-path.d.ts @@ -2,5 +2,5 @@ import { KeypairType } from '../types'; export declare const emvPath = "m/44'/60'/0'/0/{index}"; export declare const bitPath = "m/{proposal}'/{slip44}'/{index}'/0/0"; export declare const getEvmDerivePath: (index: number) => string; -export declare const getBitDerivePathFunction: (proposal: number, slip44: number) => (index: number) => string; +export declare const getBitDerivePathFunction: (slip44: number, proposal: number) => (index: number) => string; export declare const getDerivePath: (type: KeypairType) => (index: number) => string; diff --git a/local-libs/keyring/utils/derive-path.js b/local-libs/keyring/utils/derive-path.js index d24cb9e7c18..722c90fa1aa 100644 --- a/local-libs/keyring/utils/derive-path.js +++ b/local-libs/keyring/utils/derive-path.js @@ -6,7 +6,7 @@ export const bitPath = "m/{proposal}'/{slip44}'/{index}'/0/0"; export const getEvmDerivePath = index => { return emvPath.replace('{index}', index.toString()); }; -export const getBitDerivePathFunction = (proposal, slip44) => { +export const getBitDerivePathFunction = (slip44, proposal) => { const path = bitPath.replace('{proposal}', proposal.toString()).replace('{slip44}', slip44.toString()); return index => { return path.replace('{index}', index.toString()); @@ -17,17 +17,17 @@ export const getDerivePath = type => { case 'ethereum': return getEvmDerivePath; case 'bitcoin-44': - return getBitDerivePathFunction(44, 0); + return getBitDerivePathFunction(0, 44); case 'bitcoin-84': - return getBitDerivePathFunction(84, 0); + return getBitDerivePathFunction(0, 84); case 'bitcoin-86': - return getBitDerivePathFunction(86, 0); + return getBitDerivePathFunction(0, 86); case 'bittest-44': - return getBitDerivePathFunction(44, 1); + return getBitDerivePathFunction(1, 44); case 'bittest-84': - return getBitDerivePathFunction(84, 1); + return getBitDerivePathFunction(1, 84); case 'bittest-86': - return getBitDerivePathFunction(86, 1); + return getBitDerivePathFunction(1, 86); default: return () => ''; } diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json index ca81ec64f6c..be69dbada6e 100644 --- a/packages/extension-base/package.json +++ b/packages/extension-base/package.json @@ -89,9 +89,11 @@ "web3-core-subscriptions": "1.10.0", "web3-eth": "1.10.0", "web3-eth-contract": "^1.10.0", - "web3-utils": "^1.10.0" + "web3-utils": "^1.10.0", + "yup": "^1.4.0" }, "devDependencies": { + "@btckit/types": "^0.0.19", "@types/uuid": "^9.0.1" } } diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index c4e27793280..89c710bba73 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -1,21 +1,25 @@ // Copyright 2019-2022 @polkadot/extension-koni authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { Psbt, PsbtTxInput, PsbtTxOutput } from 'bitcoinjs-lib'; + import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _FundStatus, _MultiChainAsset } from '@subwallet/chain-list/types'; import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { AuthUrls, Resolver } from '@subwallet/extension-base/background/handlers/State'; import { AccountAuthType, AccountJson, AccountProxy, AddressJson, AuthorizeRequest, ConfirmationRequestBase, RequestAccountList, RequestAccountProxy, RequestAccountSubscribe, RequestAccountUnsubscribe, RequestAuthorizeCancel, RequestAuthorizeReject, RequestAuthorizeSubscribe, RequestAuthorizeTab, RequestCurrentAccountAddress, ResponseAuthorizeList, ResponseJsonGetAccountInfo, SeedLengths } from '@subwallet/extension-base/background/types'; +import { BitcoinApiStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _BitcoinApi, _ChainState, _EvmApi, _NetworkUpsertParams, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse, _ValidateCustomBrc20Request, _ValidateCustomBrc20Response, _ValidateCustomRuneRequest, _ValidateCustomRuneResponse, EnableChainParams, EnableMultiChainParams } from '@subwallet/extension-base/services/chain-service/types'; import { CrowdloanContributionsResponse } from '@subwallet/extension-base/services/subscan-service/types'; -import { SWTransactionResponse, SWTransactionResult } from '@subwallet/extension-base/services/transaction-service/types'; +import { BitcoinTransactionData, SWTransactionResponse, SWTransactionResult } from '@subwallet/extension-base/services/transaction-service/types'; import { WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; -import { BalanceJson, BuyServiceInfo, BuyTokenInfo, EarningRewardHistoryItem, EarningRewardJson, EarningStatus, HandleYieldStepParams, LeavePoolAdditionalData, NominationPoolInfo, OptimalYieldPath, OptimalYieldPathParams, RequestEarlyValidateYield, RequestGetYieldPoolTargets, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitTransfer, RequestSubscribeTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseEarlyValidateYield, ResponseGetYieldPoolTargets, ResponseSubscribeTransfer, SubmitYieldStepData, TokenApproveData, UnlockDotTransactionNft, UnstakingStatus, ValidateYieldProcessParams, YieldPoolInfo, YieldPositionInfo, YieldValidationStatus } from '@subwallet/extension-base/types'; +import { BalanceJson, BitcoinFeeDetail, BuyServiceInfo, BuyTokenInfo, EarningRewardHistoryItem, EarningRewardJson, EarningStatus, HandleYieldStepParams, LeavePoolAdditionalData, NominationPoolInfo, OptimalYieldPath, OptimalYieldPathParams, RequestEarlyValidateYield, RequestGetYieldPoolTargets, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitTransfer, RequestSubmitTransferWithId, RequestSubscribeTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseEarlyValidateYield, ResponseGetYieldPoolTargets, ResponseSubscribeTransfer, SubmitYieldStepData, TokenApproveData, UnlockDotTransactionNft, UnstakingStatus, UtxoResponseItem, ValidateYieldProcessParams, YieldPoolInfo, YieldPositionInfo, YieldValidationStatus } from '@subwallet/extension-base/types'; import { InjectedAccount, InjectedAccountWithMeta, MetadataDefBase } from '@subwallet/extension-inject/types'; import { KeypairType, KeyringPair$Json, KeyringPair$Meta } from '@subwallet/keyring/types'; import { KeyringOptions } from '@subwallet/ui-keyring/options/types'; import { KeyringAddress, KeyringPairs$Json } from '@subwallet/ui-keyring/types'; import { SessionTypes } from '@walletconnect/types/dist/types/sign-client/session'; +import BN from 'bn.js'; import { DexieExportJsonStructure } from 'dexie-export-import'; import Web3 from 'web3'; import { RequestArguments, TransactionConfig } from 'web3-core'; @@ -100,6 +104,10 @@ export interface RequestAuthorizationPerAccount extends RequestAuthorization { address: string; } +export interface RequestAuthorizationPerAccountProxy extends RequestAuthorization { + proxyId: string; +} + // Manage single site with multi account export interface RequestAuthorizationPerSite { @@ -1287,6 +1295,16 @@ export enum EvmProviderErrorType { INTERNAL_ERROR = 'INTERNAL_ERROR', } +export enum BitcoinProviderErrorType { + USER_REJECTED_REQUEST = 'USER_REJECTED_REQUEST', + UNAUTHORIZED = 'UNAUTHORIZED', + UNSUPPORTED_METHOD = 'UNSUPPORTED_METHOD', + DISCONNECTED = 'DISCONNECTED', + CHAIN_DISCONNECTED = 'CHAIN_DISCONNECTED', + INVALID_PARAMS = 'INVALID_PARAMS', + INTERNAL_ERROR = 'INTERNAL_ERROR', +} + export interface EvmSendTransactionParams { from: string; to?: string; @@ -1315,6 +1333,40 @@ export interface BitcoinSignRequest { canSign: boolean; } +export interface BitcoinRecipientTransactionParams{ + address: string; + amount: string; +} + +export interface BitcoinSendTransactionParams { + account: string; + network: 'mainnet' | 'testnet'; + recipients: BitcoinRecipientTransactionParams[] +} + +export interface BitcoinSignPsbtPayload extends Omit{ + txInput: PsbtTxInput[]; + txOutput: PsbtTxOutput[]; + psbt: Psbt +} + +enum SignatureHash { + DEFAULT = 0, + ALL = 1, + NONE = 2, + SINGLE = 3, + ANYONECANPAY = 128 +} + +export interface BitcoinSignPsbtRawRequest { + psbt: string; + allowedSighash?: SignatureHash[]; + signAtIndex?: number | number[]; + broadcast?: boolean; + network: 'mainnet' | 'testnet'; + account: string; +} + export interface EvmSignatureRequest extends EvmSignRequest { id: string; type: string; @@ -1326,16 +1378,29 @@ export interface BitcoinSignatureRequest extends BitcoinSignRequest { payloadJson: any; } +export interface BitcoinAppState { + networkKey?: string, + isConnected?: boolean, + strategy?: BitcoinApiStrategy, + listenEvents?: string[] +} + export interface EvmSendTransactionRequest extends TransactionConfig, EvmSignRequest { estimateGas: string; parseData: EvmTransactionData; isToContract: boolean; } -export type BitcoinSendTransactionRequest = BitcoinSignRequest +export interface BitcoinSendTransactionRequest extends BitcoinSignRequest, BitcoinTransactionConfig { + outputs?: BitcoinOutputUtox[], + inputs?: UtxoResponseItem[], +} export type EvmWatchTransactionRequest = EvmSendTransactionRequest; export type BitcoinWatchTransactionRequest = BitcoinSendTransactionRequest; +export type BitcoinSignPsbtRequest = BitcoinSignRequest & { + payload: BitcoinSignPsbtPayload; +}; export interface ConfirmationsQueueItemOptions { requiredPassword?: boolean; @@ -1343,6 +1408,32 @@ export interface ConfirmationsQueueItemOptions { networkKey?: string; } +export interface BitcoinOutputUtox { + address: string; + value: number; +} + +export interface BitcoinTransactionConfig{ + id?: string, + from?: string | number; + to?: string; + value?: number | string | BN; + networkKey?: string; + tokenSlug?: string; + fee?: BitcoinFeeDetail; +} + +export interface SignMessageBitcoinResult { + signature: string; + message: string; + address: string; +} + +export interface SignPsbtBitcoinResult { + psbt: string; + txid?: string +} + export interface ConfirmationsQueueItem extends ConfirmationsQueueItemOptions, ConfirmationRequestBase { payload: T; payloadJson: string; @@ -1407,9 +1498,11 @@ export interface ConfirmationDefinitions { } export interface ConfirmationDefinitionsBitcoin { - bitcoinSignatureRequest: [ConfirmationsQueueItem, ConfirmationResult], + bitcoinSignatureRequest: [ConfirmationsQueueItem, ConfirmationResult], bitcoinSendTransactionRequest: [ConfirmationsQueueItem, ConfirmationResult], - bitcoinWatchTransactionRequest: [ConfirmationsQueueItem, ConfirmationResult] + bitcoinSendTransactionRequestAfterConfirmation: [ConfirmationsQueueItem, ConfirmationResult], + bitcoinWatchTransactionRequest: [ConfirmationsQueueItem, ConfirmationResult], + bitcoinSignPsbtRequest: [ConfirmationsQueueItem, ConfirmationResult], } export type ConfirmationType = keyof ConfirmationDefinitions; @@ -2327,6 +2420,7 @@ export interface KoniRequestSignatures { 'pri(authorize.changeSiteAll)': [RequestAuthorizationAll, boolean, AuthUrls]; 'pri(authorize.changeSite)': [RequestAuthorization, boolean, AuthUrls]; 'pri(authorize.changeSitePerAccount)': [RequestAuthorizationPerAccount, boolean, AuthUrls]; + 'pri(authorize.changeSitePerAccountProxy)': [RequestAuthorizationPerAccountProxy, boolean, AuthUrls]; 'pri(authorize.changeSitePerSite)': [RequestAuthorizationPerSite, boolean]; 'pri(authorize.changeSiteBlock)': [RequestAuthorizationBlock, boolean]; 'pri(authorize.forgetSite)': [RequestForgetSite, boolean, AuthUrls]; @@ -2476,6 +2570,8 @@ export interface KoniRequestSignatures { // Transfer 'pri(accounts.checkTransfer)': [RequestCheckTransfer, ValidateTransactionResponse]; 'pri(accounts.transfer)': [RequestSubmitTransfer, SWTransactionResponse]; + 'pri(accounts.transfer.after.confirmation)': [RequestSubmitTransferWithId, SWTransactionResponse]; + 'pri(accounts.getBitcoinTransactionData)': [RequestSubmitTransfer, BitcoinTransactionData]; 'pri(accounts.checkCrossChainTransfer)': [RequestCheckCrossChainTransfer, ValidateTransactionResponse]; 'pri(accounts.crossChainTransfer)': [RequestCrossChainTransfer, SWTransactionResponse]; @@ -2504,6 +2600,8 @@ export interface KoniRequestSignatures { 'pri(account.external.reject)': [RequestRejectExternalRequest, ResponseRejectExternalRequest]; 'pri(account.external.resolve)': [RequestResolveExternalRequest, ResponseResolveExternalRequest]; + 'bitcoin(request)': [RequestArguments, unknown]; + // Evm 'evm(events.subscribe)': [RequestEvmEvents, boolean, EvmEvent]; 'evm(request)': [RequestArguments, unknown]; diff --git a/packages/extension-base/src/background/errors/BitcoinProviderError.ts b/packages/extension-base/src/background/errors/BitcoinProviderError.ts new file mode 100644 index 00000000000..6938b287b99 --- /dev/null +++ b/packages/extension-base/src/background/errors/BitcoinProviderError.ts @@ -0,0 +1,50 @@ +// Copyright 2019-2022 @subwallet/extension-koni authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { SWError } from '@subwallet/extension-base/background/errors/SWError'; +import { BitcoinProviderErrorType } from '@subwallet/extension-base/background/KoniTypes'; +import { detectTranslate } from '@subwallet/extension-base/utils'; +import { t } from 'i18next'; + +const defaultErrorMap: Record = { + USER_REJECTED_REQUEST: { + message: detectTranslate('User Rejected Request'), + code: 4001 + }, + UNAUTHORIZED: { + message: detectTranslate('Failed to sign'), + code: 4100 + }, + UNSUPPORTED_METHOD: { + message: detectTranslate('Unsupported Method'), + code: 4200 + }, + DISCONNECTED: { + message: detectTranslate('Network is disconnected'), + code: 4900 + }, + CHAIN_DISCONNECTED: { + message: detectTranslate('Network is disconnected'), + code: 4901 + }, + INVALID_PARAMS: { + message: detectTranslate('Undefined error. Please contact OpenBit support'), + code: -32602 + }, + INTERNAL_ERROR: { + message: detectTranslate('Undefined error. Please contact OpenBit support'), + code: -32603 + } +}; + +export class BitcoinProviderError extends SWError { + override errorType: BitcoinProviderErrorType; + + constructor (errorType: BitcoinProviderErrorType, errMessage?: string, data?: unknown) { + const { code, message } = defaultErrorMap[errorType]; + const finalMessage = errMessage || t(message || '') || errorType; + + super(errorType, finalMessage, code, data); + this.errorType = errorType; + } +} diff --git a/packages/extension-base/src/background/types.ts b/packages/extension-base/src/background/types.ts index 7c0130f9f57..f074f7d3b54 100644 --- a/packages/extension-base/src/background/types.ts +++ b/packages/extension-base/src/background/types.ts @@ -195,7 +195,7 @@ export interface TransportRequestMessage { request: RequestTypes[TMessageType]; } -export type AccountAuthType = 'substrate' | 'evm' | 'both'; +export type AccountAuthType = 'substrate' | 'evm' | 'both' | 'bitcoin'; export interface RequestAuthorizeTab { origin: string; diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index d7afff0207b..49cd1f61054 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -7,8 +7,8 @@ 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, AccountExternalErrorCode, AccountProxiesWithCurrentProxy, AccountsWithCurrentAddress, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BasicTxErrorType, BasicTxWarningCode, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CreateDeriveAccountInfo, CronReloadRequest, CrowdloanJson, CurrentAccountInfo, DeriveAccountInfo, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, OptionInputAddress, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateSuriV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAccountMeta, RequestAccountProxyCreateSuri, RequestAccountProxyEdit, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBatchRestoreV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestCheckPublicAndSecretKey, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestConnectWalletConnect, RequestCrossChainTransfer, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDeriveCreateMultiple, RequestDeriveCreateV2, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetDeriveAccounts, RequestGetTransaction, RequestJsonRestoreV2, RequestKeyringExportAccountProxyMnemonic, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveRecentAccount, RequestSeedCreateV2, RequestSeedValidateV2, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTransferCheckReferenceCount, RequestTransferCheckSupporting, RequestTransferExistentialDeposit, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateSuriV2, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseAccountMeta, ResponseChangeMasterPassword, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseFindRawMetadata, ResponseGetDeriveAccounts, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponsePrivateKeyValidateV2, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSeedCreateV2, ResponseSeedValidateV2, ResponseSubscribeHistory, ResponseUnlockKeyring, StakingJson, StakingRewardJson, StakingTxErrorType, StakingType, SupportTransferResponse, ThemeNames, TransactionHistoryItem, TransactionResponse, TransferTxErrorType, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; -import { AccountAuthType, AccountJson, AccountProxy, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountProxy, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeCancel, RequestAuthorizeReject, RequestBatchRestore, RequestCurrentAccountAddress, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApproveSignature, RequestSigningCancel, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseType, SigningRequest, WindowOpenParams } from '@subwallet/extension-base/background/types'; +import { AccountExternalError, AccountExternalErrorCode, AccountProxiesWithCurrentProxy, AccountsWithCurrentAddress, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BasicTxErrorType, BasicTxWarningCode, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CreateDeriveAccountInfo, CronReloadRequest, CrowdloanJson, CurrentAccountInfo, DeriveAccountInfo, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, OptionInputAddress, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateSuriV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAccountMeta, RequestAccountProxyCreateSuri, RequestAccountProxyEdit, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerAccountProxy, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBatchRestoreV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestCheckPublicAndSecretKey, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestConnectWalletConnect, RequestCrossChainTransfer, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDeriveCreateMultiple, RequestDeriveCreateV2, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetDeriveAccounts, RequestGetTransaction, RequestJsonRestoreV2, RequestKeyringExportAccountProxyMnemonic, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveRecentAccount, RequestSeedCreateV2, RequestSeedValidateV2, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTransferCheckReferenceCount, RequestTransferCheckSupporting, RequestTransferExistentialDeposit, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateSuriV2, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseAccountMeta, ResponseChangeMasterPassword, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseFindRawMetadata, ResponseGetDeriveAccounts, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponsePrivateKeyValidateV2, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSeedCreateV2, ResponseSeedValidateV2, ResponseSubscribeHistory, ResponseUnlockKeyring, StakingJson, StakingRewardJson, StakingTxErrorType, StakingType, SupportTransferResponse, ThemeNames, TransactionHistoryItem, TransactionResponse, TransferTxErrorType, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountJson, AccountProxy, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountProxy, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeCancel, RequestAuthorizeReject, RequestBatchRestore, RequestCurrentAccountAddress, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApproveSignature, RequestSigningCancel, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseType, SigningRequest, WindowOpenParams } from '@subwallet/extension-base/background/types'; import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning'; import { ALL_ACCOUNT_KEY, ALL_GENESIS_HASH, BTC_DUST_AMOUNT, SUPPORT_KEYPAIR_TYPES, XCM_FEE_RATIO, XCM_MIN_AMOUNT_RATIO } from '@subwallet/extension-base/constants'; import { ALLOWED_PATH } from '@subwallet/extension-base/defaults'; @@ -34,18 +34,18 @@ import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-se import { EXTENSION_REQUEST_URL } from '@subwallet/extension-base/services/request-service/constants'; import { AuthUrls } from '@subwallet/extension-base/services/request-service/types'; import { DEFAULT_AUTO_LOCK_TIME } from '@subwallet/extension-base/services/setting-service/constants'; -import { SWTransaction, SWTransactionResponse, SWTransactionResult, TransactionEmitter, ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types'; +import { BitcoinTransactionData, SWTransaction, SWTransactionResponse, SWTransactionResult, TransactionEmitter, ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types'; import { WALLET_CONNECT_EIP155_NAMESPACE } from '@subwallet/extension-base/services/wallet-connect-service/constants'; import { isProposalExpired, isSupportWalletConnectChain, isSupportWalletConnectNamespace } from '@subwallet/extension-base/services/wallet-connect-service/helpers'; import { ResultApproveWalletConnectSession, WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; import { AccountsStore } from '@subwallet/extension-base/stores'; -import { BalanceJson, BitcoinFeeInfo, BitcoinFeeRate, BuyServiceInfo, BuyTokenInfo, DetermineUtxosForSpendArgs, EarningRewardJson, EvmEIP1995FeeOption, EvmFeeInfo, FeeChainType, FeeDetail, FeeInfo, GetFeeFunction, NominationPoolInfo, OptimalYieldPathParams, RequestEarlyValidateYield, RequestGetYieldPoolTargets, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitTransfer, RequestSubscribeTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseGetYieldPoolTargets, ResponseSubscribeTransfer, SubstrateFeeInfo, ValidateYieldProcessParams, YieldPoolType } from '@subwallet/extension-base/types'; -import { combineBitcoinFee, combineEthFee, convertSubjectInfoToAddresses, createTransactionFromRLP, determineUtxosForSpend, determineUtxosForSpendAll, filterUneconomicalUtxos, generateAccountProxyId, getSizeInfo, isSameAddress, keyringGetAccounts, reformatAddress, signatureToHex, Transaction as QrTransaction, uniqueStringArray } from '@subwallet/extension-base/utils'; +import { BalanceJson, BitcoinFeeInfo, BitcoinFeeRate, BuyServiceInfo, BuyTokenInfo, DetermineUtxosForSpendArgs, EarningRewardJson, EvmEIP1995FeeOption, EvmFeeInfo, FeeChainType, FeeDetail, FeeInfo, GetFeeFunction, NominationPoolInfo, OptimalYieldPathParams, RequestEarlyValidateYield, RequestGetYieldPoolTargets, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitTransfer, RequestSubmitTransferWithId, RequestSubscribeTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseGetYieldPoolTargets, ResponseSubscribeTransfer, SubstrateFeeInfo, ValidateYieldProcessParams, YieldPoolType } from '@subwallet/extension-base/types'; +import { combineBitcoinFee, combineEthFee, convertSubjectInfoToAddresses, createTransactionFromRLP, determineUtxosForSpend, determineUtxosForSpendAll, filterUneconomicalUtxos, generateAccountProxyId, getSizeInfo, isAddressValidWithAuthType, isSameAddress, keyringGetAccounts, reformatAddress, signatureToHex, Transaction as QrTransaction, uniqueStringArray } from '@subwallet/extension-base/utils'; import { parseContractInput, parseEvmRlp } from '@subwallet/extension-base/utils/eth/parseTransaction'; import { balanceFormatter, BN_ZERO, formatNumber } from '@subwallet/extension-base/utils/number'; import { MetadataDef } from '@subwallet/extension-inject/types'; import { createPair, decodeAddress, getDerivePath, getKeypairTypeByAddress } from '@subwallet/keyring'; -import { BitcoinAddressType, BitcoinKeypairTypes, EthereumKeypairTypes, KeypairType, KeyringPair, KeyringPair$Json, KeyringPair$Meta, SubstrateKeypairTypes } from '@subwallet/keyring/types'; +import { BitcoinAddressType, BitcoinKeypairTypes, KeypairType, KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@subwallet/keyring/types'; import { getBitcoinAddressInfo, validateBitcoinAddress } from '@subwallet/keyring/utils'; import { keyring } from '@subwallet/ui-keyring'; import { SingleAddress, SubjectInfo } from '@subwallet/ui-keyring/observable/types'; @@ -107,6 +107,7 @@ function transformAccountProxies (accounts: SubjectInfo): AccountProxy[] { if (!proxyMap[proxyId].accounts.length) { proxyMap[proxyId].name = accountToAdd.name; proxyMap[proxyId].isMaster = accountToAdd.isMasterAccount; + proxyMap[proxyId].isReadOnly = accountToAdd.isReadOnly; } proxyMap[proxyId].accounts.push(accountToAdd); @@ -862,51 +863,22 @@ export default class KoniExtension { return true; } - private getAccounts (): string[] { - const storedAccounts = this.#koniState.keyringService.accounts; - const transformedAccounts = transformAccounts(storedAccounts); - - return transformedAccounts.map((a) => a.address); - } - - private isAddressValidWithAuthType (address: string, accountAuthType?: AccountAuthType): boolean { - const type = getKeypairTypeByAddress(address); - - if (accountAuthType === 'substrate') { - return SubstrateKeypairTypes.includes(type); - } else if (accountAuthType === 'evm') { - return EthereumKeypairTypes.includes(type); - } - - return false; - } - - private filterAccountsByAccountAuthType (accounts: string[], accountAuthType?: AccountAuthType): string[] { - if (accountAuthType === 'substrate') { - return accounts.filter((address) => !isEthereumAddress(address)); - } else if (accountAuthType === 'evm') { - return accounts.filter((address) => isEthereumAddress(address)); - } else { - return accounts; - } - } - private _changeAuthorizationAll (connectValue: boolean, callBack?: (value: AuthUrls) => void) { this.#koniState.getAuthorize((value) => { assert(value, 'The source is not known'); - const accounts = this.getAccounts(); - Object.keys(value).forEach((url) => { if (!value[url].isAllowed) { return; } - const targetAccounts = this.filterAccountsByAccountAuthType(accounts, value[url].accountAuthType); + for (const address in this.#koniState.keyringService.accounts) { + const singleAddress = this.#koniState.keyringService.accounts[address]; - targetAccounts.forEach((address) => { - value[url].isAllowedMap[address] = connectValue; - }); + if (!singleAddress.json.meta.isReadOnly) { + value[url].isAllowedMap[address] = connectValue; + } + } }); this.#koniState.setAuthorize(value, () => { callBack && callBack(value); @@ -932,12 +904,14 @@ export default class KoniExtension { this.#koniState.getAuthorize((value) => { assert(value[url], 'The source is not known'); - const accounts = this.getAccounts(); - const targetAccounts = this.filterAccountsByAccountAuthType(accounts, value[url].accountAuthType); + for (const address in this.#koniState.keyringService.accounts) { + const singleAddress = this.#koniState.keyringService.accounts[address]; + + if (!singleAddress.json.meta.isReadOnly) { + value[url].isAllowedMap[address] = connectValue; + } + } - targetAccounts.forEach((address) => { - value[url].isAllowedMap[address] = connectValue; - }); this.#koniState.setAuthorize(value, () => { callBack && callBack(value); }); @@ -972,11 +946,12 @@ export default class KoniExtension { return true; } + // deprecated private _changeAuthorizationPerAcc (address: string, connectValue: boolean, url: string, callBack?: (value: AuthUrls) => void) { this.#koniState.getAuthorize((value) => { assert(value, 'The source is not known'); - if (this.isAddressValidWithAuthType(address, value[url].accountAuthType)) { + if (isAddressValidWithAuthType(address, value[url].accountAuthType)) { value[url].isAllowedMap[address] = connectValue; this.#koniState.setAuthorize(value, () => { @@ -988,6 +963,27 @@ export default class KoniExtension { }); } + private _changeAuthorizationPerAccountProxy (proxyId: string, connectValue: boolean, url: string, callBack?: (value: AuthUrls) => void) { + this.#koniState.getAuthorize((value) => { + assert(value, 'The source is not known'); + + this.#koniState.setAuthorize(value, () => { + const accounts = this.#koniState.keyringService.accounts; + + for (const address in accounts) { + const singleAddress = accounts[address]; + const _proxyId = (singleAddress.json.meta.proxyId || '') as string; + + if (_proxyId && _proxyId === proxyId && !singleAddress.json.meta.isReadOnly) { + value[url].isAllowedMap[singleAddress.json.address] = connectValue; + } + } + + callBack && callBack(value); + }); + }); + } + private _changeAuthorizationBlock (connectValue: boolean, id: string) { this.#koniState.getAuthorize((value) => { assert(value, 'The source is not known'); @@ -1022,6 +1018,20 @@ export default class KoniExtension { return true; } + private changeAuthorizationPerAccountProxy (data: RequestAuthorizationPerAccountProxy, id: string, port: chrome.runtime.Port): boolean { + const cb = createSubscription<'pri(authorize.changeSitePerAccountProxy)'>(id, port); + + this._changeAuthorizationPerAccountProxy(data.proxyId, data.connectValue, data.url, (items) => { + cb(items); + }); + + port.onDisconnect.addListener((): void => { + this.cancelSubscription(id); + }); + + return true; + } + private changeAuthorizationPerSite (data: RequestAuthorizationPerSite): boolean { this._changeAuthorizationPerSite(data.values, data.id); @@ -1362,7 +1372,7 @@ export default class KoniExtension { this.#koniState.getAuthorize((value) => { if (value && Object.keys(value).length) { Object.keys(value).forEach((url) => { - if (this.isAddressValidWithAuthType(address, value[url].accountAuthType)) { + if (isAddressValidWithAuthType(address, value[url].accountAuthType)) { value[url].isAllowedMap[address] = isAllowed; } }); @@ -1377,7 +1387,7 @@ export default class KoniExtension { if (value && Object.keys(value).length) { Object.keys(value).forEach((url) => { addresses.forEach((address) => { - if (this.isAddressValidWithAuthType(address, value[url].accountAuthType)) { + if (isAddressValidWithAuthType(address, value[url].accountAuthType)) { value[url].isAllowedMap[address] = isAllowed; } }); @@ -1939,8 +1949,6 @@ export default class KoniExtension { to, network }); - - console.log('getBitcoinTransactionObject', transaction); } else { const substrateApi = this.#koniState.getSubstrateApi(chain); @@ -2030,6 +2038,119 @@ export default class KoniExtension { }); } + private async makeTransferAfterConfirmation (inputData: RequestSubmitTransferWithId): Promise { + const { chain, feeCustom, feeOption, from, id, to, tokenSlug, transferAll, value } = inputData; + const [errors, , , tokenInfo] = this.validateTransfer(tokenSlug, from, to, value, transferAll); + + const warnings: TransactionWarning[] = []; + + const chainInfo = this.#koniState.getChainInfo(chain); + const nativeTokenInfo = this.#koniState.getNativeTokenInfo(chain); + const nativeTokenSlug: string = nativeTokenInfo.slug; + const isTransferNativeToken = nativeTokenSlug === tokenSlug; + let chainType = ChainType.SUBSTRATE; + + const tokenBaseAmount: AmountData = { value: '0', symbol: tokenInfo.symbol, decimals: tokenInfo.decimals || 0 }; + const transferAmount: AmountData = { ...tokenBaseAmount }; + + let transaction: ValidateTransactionResponseInput['transaction']; + + // Get native token amount + const freeBalance = await this.getAddressFreeBalance({ address: from, networkKey: chain, token: tokenSlug }); + + const getChainFee: GetFeeFunction = (id, chain, type) => { + return this.#koniState.feeService.subscribeChainFee(id, chain, type); + }; + + const txVal: string = transferAll ? freeBalance.value : (value || '0'); + + try { + if (_isChainBitcoinCompatible(chainInfo)) { + chainType = ChainType.BITCOIN; + + const bitcoinApi = this.#koniState.getBitcoinApi(chain); // Get Bitcoin API map + const network = chainInfo.isTestnet ? bitcoin.networks.testnet : bitcoin.networks.bitcoin; + + [ + transaction, + transferAmount.value + ] = await getBitcoinTransactionObject({ + bitcoinApi, + from, + getChainFee, + chain: chain, + feeCustom, + feeOption, + transferAll, + value: txVal, + to, + network + }); + } + } catch (e) { + const error = e as Error; + + if (error.message.includes('transfer amount exceeds balance')) { + error.message = t('Insufficient balance'); + } + + throw error; + } + + const transferNativeAmount = isTransferNativeToken ? transferAmount.value : '0'; + + return this.#koniState.transactionService.handleTransactionAfterConfirmation({ + id, + errors, + warnings, + address: from, + chain: chain, + feeCustom, + feeOption, + chainType, + transferNativeAmount, + transaction, + data: inputData, + extrinsicType: isTransferNativeToken ? ExtrinsicType.TRANSFER_BALANCE : ExtrinsicType.TRANSFER_TOKEN, + ignoreWarnings: transferAll, + isTransferAll: isTransferNativeToken ? transferAll : false, + edAsWarning: isTransferNativeToken + }); + } + + private async getBitcoinTransactionData (inputData: RequestSubmitTransfer): Promise { + const { chain, feeCustom, feeOption, from, to, transferAll, value } = inputData; + + const chainInfo = this.#koniState.getChainInfo(chain); + const bitcoinApi = this.#koniState.getBitcoinApi(chain); // Get Bitcoin API map + const network = chainInfo.isTestnet ? bitcoin.networks.testnet : bitcoin.networks.bitcoin; + + const getChainFee: GetFeeFunction = (id, chain, type) => { + return this.#koniState.feeService.subscribeChainFee(id, chain, type); + }; + + const [ + transaction + ] = await getBitcoinTransactionObject({ + bitcoinApi, + from, + getChainFee, + chain: chain, + feeCustom, + feeOption, + transferAll, + value: value || '0', + to, + network + }); + + return { + data: transaction, + dataBase64: transaction.toBase64(), + dataToHex: transaction.toHex() + }; + } + private validateCrossChainTransfer ( destinationNetworkKey: string, sendingTokenSlug: string, @@ -4178,7 +4299,7 @@ export default class KoniExtension { function convertRs (rs: Record): Record { return Object.fromEntries(Object.entries(rs).map(([key, value]) => { - const { additionalValidator, eventsHandler, transaction, ...transactionResult } = value; + const { additionalValidator, emitterTransaction, eventsHandler, transaction, ...transactionResult } = value; return [key, transactionResult]; })); @@ -5087,6 +5208,8 @@ export default class KoniExtension { return this.changeAuthorization(request as RequestAuthorization, id, port); case 'pri(authorize.changeSitePerAccount)': return this.changeAuthorizationPerAcc(request as RequestAuthorizationPerAccount, id, port); + case 'pri(authorize.changeSitePerAccountProxy)': + return this.changeAuthorizationPerAccountProxy(request as RequestAuthorizationPerAccountProxy, id, port); case 'pri(authorize.changeSitePerSite)': return this.changeAuthorizationPerSite(request as RequestAuthorizationPerSite); case 'pri(authorize.changeSiteBlock)': @@ -5390,8 +5513,12 @@ export default class KoniExtension { /// Transfer case 'pri(accounts.transfer)': return await this.makeTransfer(request as RequestSubmitTransfer); + case 'pri(accounts.transfer.after.confirmation)': + return await this.makeTransferAfterConfirmation(request as RequestSubmitTransfer); case 'pri(accounts.crossChainTransfer)': return await this.makeCrossChainTransfer(request as RequestCrossChainTransfer); + case 'pri(accounts.getBitcoinTransactionData)': + return await this.getBitcoinTransactionData(request as RequestSubmitTransfer); /// Sign QR case 'pri(qr.transaction.parse.substrate)': diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index a668f0ba4c5..97d67c5c4bb 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -2,14 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _MultiChainAsset } from '@subwallet/chain-list/types'; +import { BitcoinProviderError } from '@subwallet/extension-base/background/errors/BitcoinProviderError'; import { EvmProviderError } from '@subwallet/extension-base/background/errors/EvmProviderError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { isSubscriptionRunning, unsubscribe } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AccountRefMap, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, BasicTxErrorType, ChainStakingMetadata, ChainType, ConfirmationsQueue, CrowdloanItem, CrowdloanJson, CurrentAccountInfo, CurrentAccountProxyInfo, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestCheckPublicAndSecretKey, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCheckPublicAndSecretKey, ServiceInfo, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountRefMap, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, BasicTxErrorType, BitcoinOutputUtox, BitcoinProviderErrorType, BitcoinSendTransactionParams, BitcoinSendTransactionRequest, BitcoinSignatureRequest, BitcoinSignPsbtPayload, BitcoinSignPsbtRawRequest, BitcoinSignPsbtRequest, BitcoinTransactionConfig, ChainStakingMetadata, ChainType, ConfirmationsQueue, CrowdloanItem, CrowdloanJson, CurrentAccountInfo, CurrentAccountProxyInfo, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestCheckPublicAndSecretKey, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCheckPublicAndSecretKey, ServiceInfo, SignMessageBitcoinResult, SignPsbtBitcoinResult, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; import { AccountJson, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning } from '@subwallet/extension-base/background/types'; import { ALL_ACCOUNT_KEY, ALL_GENESIS_HASH, MANTA_PAY_BALANCE_INTERVAL } from '@subwallet/extension-base/constants'; import { NftService } from '@subwallet/extension-base/koni/api/nft'; import { BalanceService } from '@subwallet/extension-base/services/balance-service'; +import { getTransferableBitcoinUtxos } from '@subwallet/extension-base/services/balance-service/helpers/balance/bitcoin'; import { ServiceStatus } from '@subwallet/extension-base/services/base/types'; import BuyService from '@subwallet/extension-base/services/buy-service'; import CampaignService from '@subwallet/extension-base/services/campaign-service'; @@ -38,15 +40,17 @@ import { TransactionEventResponse } from '@subwallet/extension-base/services/tra import WalletConnectService from '@subwallet/extension-base/services/wallet-connect-service'; import { SWStorage } from '@subwallet/extension-base/storage'; import AccountRefStore from '@subwallet/extension-base/stores/AccountRef'; -import { BalanceItem, BalanceMap, EvmFeeInfo } from '@subwallet/extension-base/types'; -import { isAccountAll, keyringGetAccounts, stripUrl, targetIsWeb } from '@subwallet/extension-base/utils'; +import { BalanceItem, BalanceMap, DetermineUtxosForSpendArgs, EvmFeeInfo, UtxoResponseItem } from '@subwallet/extension-base/types'; +import { determineUtxosForSpend, filterUneconomicalUtxos, getSizeInfo, isAccountAll, keyringGetAccounts, stripUrl, targetIsWeb } from '@subwallet/extension-base/utils'; import { isContractAddress, parseContractInput } from '@subwallet/extension-base/utils/eth/parseTransaction'; import { createPromiseHandler } from '@subwallet/extension-base/utils/promise'; import { MetadataDef, ProviderMeta } from '@subwallet/extension-inject/types'; +import { isBitcoinAddress } from '@subwallet/keyring'; import { decodePair } from '@subwallet/keyring/pair/decode'; import { KeypairType } from '@subwallet/keyring/types'; import { keyring } from '@subwallet/ui-keyring'; import BigN from 'bignumber.js'; +import * as bitcoin from 'bitcoinjs-lib'; import BN from 'bn.js'; import SimpleKeyring from 'eth-simple-keyring'; import { t } from 'i18next'; @@ -55,7 +59,7 @@ import { BehaviorSubject, Subject } from 'rxjs'; import { TransactionConfig } from 'web3-core'; import { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback } from '@polkadot/rpc-provider/types'; -import { assert, hexStripPrefix, hexToU8a, isHex, logger as createLogger, u8aToHex } from '@polkadot/util'; +import { assert, hexStripPrefix, hexToU8a, isArray, isHex, logger as createLogger, u8aToHex } from '@polkadot/util'; import { Logger } from '@polkadot/util/types'; import { base64Decode, isEthereumAddress, keyExtractSuri } from '@polkadot/util-crypto'; @@ -155,17 +159,19 @@ export default class KoniState { this.chainService = new ChainService(this.dbService, this.eventService, this.keyringService); this.subscanService = SubscanService.getInstance(); this.settingService = new SettingService(); - this.requestService = new RequestService(this.chainService, this.settingService, this.keyringService); + this.feeService = new FeeService(this); + this.priceService = new PriceService(this.dbService, this.eventService, this.chainService); this.balanceService = new BalanceService(this); this.historyService = new HistoryService(this.dbService, this.chainService, this.eventService, this.keyringService); this.mintCampaignService = new MintCampaignService(this); - this.walletConnectService = new WalletConnectService(this, this.requestService); this.migrationService = new MigrationService(this, this.eventService); this.campaignService = new CampaignService(this); this.buyService = new BuyService(this); this.transactionService = new TransactionService(this); + this.requestService = new RequestService(this.chainService, this.settingService, this.keyringService, this.feeService, this.transactionService); + this.walletConnectService = new WalletConnectService(this, this.requestService); this.earningService = new EarningService(this); this.feeService = new FeeService(this); this.nftService = new NftService(); @@ -1177,6 +1183,324 @@ export default class KoniState { } as ApiMap; } + public async bitcoinSign (id: string, url: string, method: string, params: Record, allowedAccounts: string[]): Promise { + const { address, message } = params; + + if (address === '' || !message) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found address or payload to sign')); + } + + if (!isBitcoinAddress(address)) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Invalid bitcoin address')); + } + + // Check sign abiblity + if (!allowedAccounts.find((acc) => (acc.toLowerCase() === address.toLowerCase()))) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('You have rescinded allowance for this account in wallet')); + } + + const pair = keyring.getPair(address); + + if (!pair) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Unable to find account')); + } + + const account: AccountJson = { address: pair.address, ...pair.meta }; + + const hashPayload = ''; + const canSign = !account.isExternal; + + const signPayload: BitcoinSignatureRequest = { + account: account, + payload: message as unknown, + payloadJson: message, + hashPayload, + canSign, + id + }; + + return this.requestService.addConfirmationBitcoin(id, url, 'bitcoinSignatureRequest', signPayload, { + requiredPassword: false, + address + }) + .then(({ isApproved, payload }) => { + if (isApproved) { + if (payload) { + return payload; + } else { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found signature')); + } + } else { + throw new BitcoinProviderError(BitcoinProviderErrorType.USER_REJECTED_REQUEST); + } + }); + } + + public async bitcoinSignPspt (id: string, url: string, method: string, params: BitcoinSignPsbtRawRequest, allowedAccounts: string[]): Promise { + const { account: address, allowedSighash, broadcast, network, psbt, signAtIndex } = params; + + if (!psbt || !address) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found payload to sign')); + } + + if (!isHex(`0x${psbt}`)) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Psbt to be signed must be hex-encoded')); + } + + if (!(network === 'mainnet' || network === 'testnet')) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Network to try this request is must be mainnet or testnet')); + } + + if (!isBitcoinAddress(address)) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found address')); + } + + // Check sign abiblity + if (!allowedAccounts.find((acc) => (acc.toLowerCase() === address.toLowerCase()))) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('You have rescinded allowance for this account in wallet')); + } + + const pair = keyring.getPair(address); + + if (!pair) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Unable to find account')); + } + + if (network === 'mainnet') { + if (!['bitcoin-86', 'bitcoin-84'].includes(pair.type)) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Your address is not on the mainnet network')); + } + } else if (network === 'testnet') { + if (!['bittest-86', 'bittest-84'].includes(pair.type)) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Your address is not on the testnet network')); + } + } + + const account: AccountJson = { address: pair.address, ...pair.meta }; + + const psbtGenerate = bitcoin.Psbt.fromHex(psbt, { + network: network === 'testnet' ? bitcoin.networks.testnet : bitcoin.networks.bitcoin + }); + const psbtTxInputs = psbtGenerate.txInputs; + const psbtTxOutputs = psbtGenerate.txOutputs; + + const payload: BitcoinSignPsbtPayload = { + psbt: psbtGenerate, + broadcast: !!broadcast, + network, + signAtIndex: isArray(signAtIndex) && signAtIndex.length === 0 ? undefined : signAtIndex, + account: account.address, + allowedSighash, + txInput: psbtTxInputs, + txOutput: psbtTxOutputs + }; + const hashPayload = ''; + const canSign = !account.isExternal; + + const signPayload: BitcoinSignPsbtRequest = { + account, + payload, + hashPayload, + canSign + }; + + return this.requestService.addConfirmationBitcoin(id, url, 'bitcoinSignPsbtRequest', signPayload, { + requiredPassword: false + }) + .then(({ isApproved, payload }) => { + if (isApproved) { + if (payload) { + return payload; + } else { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found signature')); + } + } else { + throw new BitcoinProviderError(BitcoinProviderErrorType.USER_REJECTED_REQUEST); + } + }); + } + + public async bitcoinSendTransaction (id: string, url: string, networkKey: string, allowedAccounts: string[], transactionParams: BitcoinSendTransactionParams): Promise { + const bitcoinApi = this.getBitcoinApi(networkKey); + const apiStrategy = bitcoinApi.api; + + const autoFormatNumber = (val?: string | number): string | undefined => { + if (typeof val === 'string' && val.startsWith('0x')) { + return new BigN(val.replace('0x', ''), 16).toString(); + } else if (typeof val === 'number') { + return val.toString(); + } + + return val; + }; + + if (transactionParams.recipients.length !== 1) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Receiving address must be a single account')); + } + + if (transactionParams.account === transactionParams.recipients[0].address) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Receiving address must be different from sending address')); + } + + const tokenInfo = this.getNativeTokenInfo(networkKey); + + const transaction: BitcoinTransactionConfig = { + id, + from: transactionParams.account, + to: transactionParams.recipients[0].address, + value: autoFormatNumber(transactionParams.recipients[0].amount), + tokenSlug: tokenInfo.slug, + networkKey: transactionParams.network === 'testnet' ? 'bitcoinTestnet' : 'bitcoin' + }; + + // Address is validated in before step + const fromAddress = allowedAccounts.find((account) => (account.toLowerCase() === (transaction.from as string).toLowerCase())); + + if (!fromAddress) { + throw new EvmProviderError(EvmProviderErrorType.INVALID_PARAMS, t('You have rescinded allowance for this account in wallet')); + } + + const pair = keyring.getPair(fromAddress); + + if (!pair) { + throw new EvmProviderError(EvmProviderErrorType.INVALID_PARAMS, t('Unable to find account')); + } + + if (networkKey === 'mainnet') { + if (!['bitcoin-86', 'bitcoin-84'].includes(pair.type)) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Your address is not on the mainnet network')); + } + } else if (networkKey === 'testnet') { + if (!['bittest-86', 'bittest-84'].includes(pair.type)) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Your address is not on the testnet network')); + } + } + + const account: AccountJson = { address: pair.address, ...pair.meta }; + + // Calculate transaction data + const [feeOptions_, utxos] = await Promise.all([ + apiStrategy.getRecommendedFeeRate(), + getTransferableBitcoinUtxos(bitcoinApi, fromAddress) + ]); + + const optionDefault = feeOptions_.options.default; + let feeOptions = null; + const determineUtxosArgs: DetermineUtxosForSpendArgs = { + amount: parseInt(transaction.value as string || '0'), + feeRate: feeOptions_.options[optionDefault].feeRate, + recipient: transaction.to as string, + sender: account.address, + utxos + }; + + if (!transaction.to) { + throw new Error(); + } + + const fallbackCalculate = (recipients: string[]) => { + const utxos = filterUneconomicalUtxos({ + utxos: determineUtxosArgs.utxos, + feeRate: determineUtxosArgs.feeRate, + recipients, + sender: determineUtxosArgs.sender + }); + + const { txVBytes: vSize } = getSizeInfo({ + inputLength: utxos.length || 1, + sender: fromAddress, + recipients + }); + + return { + vSize, + maxTransferable: utxos.reduce((previous, input) => previous.plus(input.value), new BigN(0)), + estimatedFee: Math.ceil(determineUtxosArgs.feeRate * vSize).toString() + }; + }; + + const getBalance = async (senderAddress: string) => { + const filteredUtxos = await getTransferableBitcoinUtxos(bitcoinApi, senderAddress); + + let balanceValue = new BigN(0); + + filteredUtxos.forEach((utxo) => { + balanceValue = balanceValue.plus(utxo.value); + }); + + return balanceValue; + }; + + let maxTransferable = new BigN('0'); + let estimatedFee = '0'; + let inputs: UtxoResponseItem[] = []; + let outputs: BitcoinOutputUtox[] = []; + + try { + const { fee: _estimatedFee, inputs: inputsRs, outputs: outputRs } = determineUtxosForSpend(determineUtxosArgs); + + const { txVBytes: vSize } = getSizeInfo({ + inputLength: inputs.length, + sender: fromAddress, + recipients: [transaction.to] + }); + + inputs = [...inputsRs]; + outputs = [...outputRs]; + estimatedFee = new BigN(_estimatedFee).toFixed(0); + feeOptions = { + ...feeOptions_, + estimatedFee, + vSize + }; + } catch (_e) { + if (!feeOptions) { + const fb = fallbackCalculate([transaction.to, transaction.to]); + + estimatedFee = fb.estimatedFee; + + feeOptions = { + ...feeOptions_, + estimatedFee, + vSize: fb.vSize + }; + } + } + + maxTransferable = await getBalance(fromAddress); + + // Validate balance + if (maxTransferable.lt(new BigN(estimatedFee).plus(new BigN(autoFormatNumber(transactionParams.recipients[0].amount) || '0')))) { + throw new EvmProviderError(EvmProviderErrorType.INVALID_PARAMS, t('Insufficient balance')); + } + + const requestPayload: BitcoinSendTransactionRequest = { + ...transaction, + hashPayload: JSON.stringify(transaction), + fee: feeOptions, + inputs, + canSign: true, + outputs, + account: account + }; + + // Custom handle this instead of general handler transaction + return this.requestService.addConfirmationBitcoin(id, url, 'bitcoinSendTransactionRequestAfterConfirmation', requestPayload, { + requiredPassword: false + }) + .then(({ isApproved, payload }) => { + if (isApproved) { + if (payload) { + return payload; + } else { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found signature')); + } + } else { + throw new BitcoinProviderError(BitcoinProviderErrorType.USER_REJECTED_REQUEST); + } + }); + } + public refreshSubstrateApi (key: string) { this.chainService.refreshSubstrateApi(key); diff --git a/packages/extension-base/src/koni/background/handlers/Tabs.ts b/packages/extension-base/src/koni/background/handlers/Tabs.ts index be3803c075b..e6cbdb6c403 100644 --- a/packages/extension-base/src/koni/background/handlers/Tabs.ts +++ b/packages/extension-base/src/koni/background/handlers/Tabs.ts @@ -4,11 +4,12 @@ import type { InjectedAccount } from '@subwallet/extension-inject/types'; import { _AssetType } from '@subwallet/chain-list/types'; +import { BitcoinProviderError } from '@subwallet/extension-base/background/errors/BitcoinProviderError'; import { EvmProviderError } from '@subwallet/extension-base/background/errors/EvmProviderError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { AuthUrlInfo } from '@subwallet/extension-base/background/handlers/State'; import { createSubscription, unsubscribe } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AddNetworkRequestExternal, AddTokenRequestExternal, EvmAppState, EvmEventType, EvmProviderErrorType, EvmSendTransactionParams, PassPhishing, RequestAddPspToken, RequestEvmProviderSend, RequestSettingsType, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes'; +import { AddNetworkRequestExternal, AddTokenRequestExternal, BitcoinAppState, BitcoinProviderErrorType, BitcoinSendTransactionParams, BitcoinSignPsbtRawRequest, EvmAppState, EvmEventType, EvmProviderErrorType, EvmSendTransactionParams, PassPhishing, RequestAddPspToken, RequestEvmProviderSend, RequestSettingsType, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes'; import RequestBytesSign from '@subwallet/extension-base/background/RequestBytesSign'; import RequestExtrinsicSign from '@subwallet/extension-base/background/RequestExtrinsicSign'; import { AccountAuthType, MessageTypes, RequestAccountList, RequestAccountSubscribe, RequestAccountUnsubscribe, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestTypes, ResponseRpcListProviders, ResponseSigning, ResponseTypes, SubscriptionMessageTypes } from '@subwallet/extension-base/background/types'; @@ -20,8 +21,10 @@ import { _NetworkUpsertParams } from '@subwallet/extension-base/services/chain-s import { _generateCustomProviderKey } from '@subwallet/extension-base/services/chain-service/utils'; import { AuthUrls } from '@subwallet/extension-base/services/request-service/types'; import { DEFAULT_CHAIN_PATROL_ENABLE } from '@subwallet/extension-base/services/setting-service/constants'; +import { AuthAddress, RequestAddressesResult, Response } from '@subwallet/extension-base/types/dApp'; import { canDerive, getEVMChainInfo, stripUrl } from '@subwallet/extension-base/utils'; import { InjectedMetadataKnown, MetadataDef, ProviderMeta } from '@subwallet/extension-inject/types'; +import { getDerivePath, getKeypairTypeByAddress } from '@subwallet/keyring'; import { KeypairType, KeyringPair } from '@subwallet/keyring/types'; import keyring from '@subwallet/ui-keyring'; import { SingleAddress, SubjectInfo } from '@subwallet/ui-keyring/observable/types'; @@ -35,7 +38,8 @@ import { JsonRpcPayload } from 'web3-core-helpers'; import { checkIfDenied } from '@polkadot/phishing'; import { JsonRpcResponse } from '@polkadot/rpc-provider/types'; import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; -import { assert, isNumber } from '@polkadot/util'; +import { assert, hexStripPrefix, isNumber, u8aToHex } from '@polkadot/util'; +import { isEthereumAddress } from '@polkadot/util-crypto'; interface AccountSub { subscription: Subscription; @@ -60,6 +64,8 @@ function transformAccountsV2 (accounts: SubjectInfo, anyType = false, authInfo?: keyringTypes = ['ed25519', 'sr25519', 'ecdsa']; } else if (accountAuthType === 'evm') { keyringTypes = ['ethereum']; + } else if (accountAuthType === 'bitcoin') { + keyringTypes = ['bitcoin-84', 'bitcoin-86', 'bittest-84', 'bittest-86']; } const authTypeFilter = ({ type }: SingleAddress) => (!!type && keyringTypes.includes(type)); @@ -79,6 +85,61 @@ function transformAccountsV2 (accounts: SubjectInfo, anyType = false, authInfo?: })); } +const getAuthAddresses = (addresses: string[]) => { + const result: AuthAddress[] = []; + + addresses.forEach((address) => { + const keypairType = getKeypairTypeByAddress(address); + + if (!['ethereum', 'bitcoin-84', 'bitcoin-86', 'bittest-84', 'bittest-86'].includes(keypairType)) { + return; + } + + const pair = keyring.getPair(address); + + if (pair.meta.isReadOnly) { + return; + } + + const item: AuthAddress = { + address, + type: (() => { + if (keypairType === 'ethereum') { + return 'ethereum'; + } + + if (['bitcoin-86', 'bittest-86'].includes(keypairType)) { + return 'p2tr'; + } + + if (['bitcoin-84', 'bittest-84'].includes(keypairType)) { + return 'p2wpkh'; + } + + return 'unknown'; + })() + }; + + if (['bittest-84', 'bittest-86'].includes(keypairType)) { + item.isTestnet = true; + } + + const deriFunc = getDerivePath(keypairType); + const index = parseInt((pair.meta.suri as string)?.split('//')[1]) || 0; + + item.derivationPath = deriFunc(index); + item.publicKey = hexStripPrefix(u8aToHex(pair.publicKey)); + + if (pair.publicKey.length !== 32) { + item.tweakedPublicKey = hexStripPrefix(u8aToHex(pair.publicKey.slice(1, 33))); + } + + result.push(item); + }); + + return result; +}; + interface ChainPatrolResponse { reason: string; reports: Array<{ createdAt: string, id: number }>; @@ -819,6 +880,39 @@ export default class KoniTabs { return true; } + private async getBitcoinState (url?: string, defaultChain?: string): Promise { + let autoActiveChain = false; + + if (url) { + const authInfo = await this.getAuthInfo(url); + + if (authInfo?.isAllowed) { + autoActiveChain = true; + } + } + + const currentBitcoinNetwork = this.#koniState.requestService.getDAppChainInfo({ + autoActive: autoActiveChain, + accessType: 'bitcoin', + defaultChain: defaultChain === 'mainnet' ? 'bitcoin' : 'bitcoinTestnet', + url + }); + + if (currentBitcoinNetwork) { + const { slug } = currentBitcoinNetwork; + const bitcoinApi = this.#koniState.getBitcoinApi(slug); + const bitcoinApiStrategy = bitcoinApi?.api; + + return { + networkKey: slug, + strategy: bitcoinApiStrategy, + listenEvents: [] + }; + } else { + return {}; + } + } + private checkAndHandleProviderStatus (provider: WebsocketProvider | HttpProvider | undefined) { if ((!provider || !provider?.connected) && provider?.supportsSubscriptions()) { // excludes HttpProvider Object.values(this.evmEventEmitterMap).forEach((m) => { @@ -1058,27 +1152,221 @@ export default class KoniTabs { return await this.#koniState.addTokenConfirm(id, url, tokenInfo); } - public async handle (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port: chrome.runtime.Port): Promise { - console.log('handle', type, request, url, port); + async bitcoinGetAddresses (url: string, request: RequestArguments): Promise> { + try { + await this.#koniState.authorizeUrlV2(url, { + origin: '', + accountAuthType: 'bitcoin' + }); - if (type === 'pub(phishing.redirectIfDenied)') { - return this.redirectIfPhishing(url); + const authInfo = await this.getAuthInfo(url); + + if (!authInfo) { + return { + result: { + addresses: [] + } + }; + } + + const { proxyId: currentAccountProxy } = this.#koniState.keyringService.currentAccountProxy; + + const addressesAllowed = + getAuthAddresses(Object.keys(authInfo.isAllowedMap) + .filter((k) => authInfo.isAllowedMap[k])) + .filter(({ address }) => !isEthereumAddress(address)) + .reduce((listSorted, account) => { + const pair = keyring.getPair(account.address); + + console.log(pair.meta.proxyId, currentAccountProxy, account.address); + + if (pair.meta.proxyId === currentAccountProxy) { + listSorted.unshift(account); + } else { + listSorted.push(account); + } + + return listSorted; + }, [] as AuthAddress[]); + + return { + result: { + addresses: addressesAllowed + } + }; + } catch (e) { + throw new BitcoinProviderError(BitcoinProviderErrorType.USER_REJECTED_REQUEST); } + } - // Wait for account ready and chain ready - await Promise.all([this.#koniState.eventService.waitAccountReady, this.#koniState.eventService.waitChainReady]); + private async getBitcoinCurrentAccount (url: string): Promise { + return await new Promise((resolve) => { + this.getAuthInfo(url).then((authInfo) => { + const allAccounts = this.#koniState.keyringService.accounts; + const accountList = transformAccountsV2(allAccounts, false, authInfo, 'bitcoin').map((a) => a.address); + let accounts: string[] = []; - if (type !== 'pub(authorize.tabV2)' && !this.isEvmPublicRequest(type, request as RequestArguments)) { - await this.#koniState.ensureUrlAuthorizedV2(url) - .catch((e: Error) => { - if (type.startsWith('evm')) { - throw new EvmProviderError(EvmProviderErrorType.INTERNAL_ERROR, e.message); + const address = this.#koniState.keyringService.currentAccount.address; + + if (address === ALL_ACCOUNT_KEY || !address) { + accounts = accountList; + } else { + if (accountList.includes(address)) { + const result = accountList.filter((adr) => adr !== address); + + result.unshift(address); + accounts = result; } else { - throw e; + accounts = accountList; } + } + + resolve(accounts); + }).catch(console.error); + }); + } + + private async bitcoinSign (id: string, url: string, { method, params }: RequestArguments) { + const allowedAccounts = (await this.getBitcoinCurrentAccount(url)); + + const signResult = await this.#koniState.bitcoinSign(id, url, method, params as Record, allowedAccounts); + + if (signResult) { + return signResult; + } else { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, 'Failed to sign message'); + } + } + + private async bitcoinSignPspt (id: string, url: string, { method, params }: RequestArguments) { + const allowedAccounts = (await this.getBitcoinCurrentAccount(url)); + + const signResult = await this.#koniState.bitcoinSignPspt(id, url, method, params as BitcoinSignPsbtRawRequest, allowedAccounts); + + if (signResult) { + return signResult; + } else { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, 'Failed to sign message'); + } + } + + private async bitcoinSendTransfer (id: string, url: string, { params }: RequestArguments) { + const transactionParams = params as BitcoinSendTransactionParams; + const canUseAccount = transactionParams.account && this.canUseAccount(transactionParams.account, url); + const bitcoinState = await this.getBitcoinState(url, transactionParams.network); + const networkKey = bitcoinState.networkKey; + + if (!canUseAccount) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('You have rescinded allowance for this account in wallet')); + } + + if (!networkKey) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Network unavailable. Please switch network or manually add network to wallet')); + } + + const senderAccountType = getKeypairTypeByAddress(transactionParams.account); + + if ((transactionParams.network === 'mainnet' && senderAccountType !== 'bitcoin-84') || (transactionParams.network === 'testnet' && senderAccountType !== 'bittest-84')) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('The account or the network is incorrect')); + } + + if (!networkKey) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Network unavailable. Please switch network or manually add network to wallet')); + } + + if (!transactionParams.recipients?.length) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Please provide the recipient and the amount')); + } + + if (transactionParams.recipients?.length > 1) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t("We don't support multiple recipients yet. Please provide only one for now.")); + } + + if (transactionParams.account === transactionParams.recipients[0].address) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t("The recipient address cannot be the same as the sender's")); + } + + const recipientAccountType = getKeypairTypeByAddress(transactionParams.recipients[0].address); + + if (senderAccountType !== recipientAccountType) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t("The recipient address type must be the same as the sender's")); + } + + const allowedAccounts = await this.getBitcoinCurrentAccount(url); + const transactionHash = await this.#koniState.bitcoinSendTransaction(id, url, networkKey, allowedAccounts, transactionParams); + + if (!transactionHash) { + throw new BitcoinProviderError(BitcoinProviderErrorType.USER_REJECTED_REQUEST); + } + + return transactionHash; + } + + private async handleBitcoinRequest (id: string, url: string, request: RequestArguments, port: chrome.runtime.Port): Promise { + const { method } = request; + + try { + switch (method) { + case 'getAddresses': + return await this.bitcoinGetAddresses(url, request); + + case 'signMessage': + return await this.bitcoinSign(id, url, request); + + case 'signPsbt': + return await this.bitcoinSignPspt(id, url, request); + + case 'sendTransfer': + return await this.bitcoinSendTransfer(id, url, request); + + default: + return this.performWeb3Method(id, url, request); + } + } catch (e) { + // @ts-ignore + if (e.code) { + throw e; + } else { + console.error(e); + throw new BitcoinProviderError(BitcoinProviderErrorType.INTERNAL_ERROR, e?.toString()); + } + } + } + + public async handleBitcoin (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port: chrome.runtime.Port): Promise { + switch (type) { + case 'bitcoin(request)': + return await this.handleBitcoinRequest(id, url, request as RequestArguments, port); + default: + throw new Error(`Unable to handle message of type ${type}`); + } + } + + public async handleEvm (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port: chrome.runtime.Port): Promise { + if (!this.isEvmPublicRequest(type, request as RequestArguments)) { + await this.#koniState.ensureUrlAuthorizedV2(url) + .catch((e: Error) => { + throw new EvmProviderError(EvmProviderErrorType.INTERNAL_ERROR, e.message); }); } + switch (type) { + case 'evm(events.subscribe)': + return await this.evmSubscribeEvents(url, id, port); + case 'evm(request)': + return await this.handleEvmRequest(id, url, request as RequestArguments); + case 'evm(provider.send)': + return await this.handleEvmSend(id, url, port, request as RequestEvmProviderSend); + default: + throw new Error(`Unable to handle message of type ${type}`); + } + } + + public async handleSubstrate (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port: chrome.runtime.Port): Promise { + if (type !== 'pub(authorize.tabV2)' && !this.isEvmPublicRequest(type, request as RequestArguments)) { + await this.#koniState.ensureUrlAuthorizedV2(url); + } + switch (type) { /// Clone from PolkadotJs case 'pub(bytes.sign)': @@ -1117,23 +1405,39 @@ export default class KoniTabs { case 'pub(token.add)': return this.addPspToken(id, url, request as RequestAddPspToken); - /// case 'pub(authorize.tabV2)': return this.authorizeV2(url, request as RequestAuthorizeTab); + case 'pub(accounts.listV2)': return this.accountsListV2(url, request as RequestAccountList); + case 'pub(accounts.subscribeV2)': return this.accountsSubscribeV2(url, request as RequestAccountSubscribe, id, port); + case 'pub(accounts.unsubscribe)': return this.accountsUnsubscribe(url, request as RequestAccountUnsubscribe); - case 'evm(events.subscribe)': - return await this.evmSubscribeEvents(url, id, port); - case 'evm(request)': - return await this.handleEvmRequest(id, url, request as RequestArguments); - case 'evm(provider.send)': - return await this.handleEvmSend(id, url, port, request as RequestEvmProviderSend); + default: throw new Error(`Unable to handle message of type ${type}`); } } + + public async handle (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port: chrome.runtime.Port): Promise { + if (type === 'pub(phishing.redirectIfDenied)') { + return this.redirectIfPhishing(url); + } + + // Wait for account ready and chain ready + await Promise.all([this.#koniState.eventService.waitAccountReady, this.#koniState.eventService.waitChainReady]); + + if (type.startsWith('bitcoin(')) { + return this.handleBitcoin(id, type, request, url, port); + } else if (type.startsWith('evm(')) { + return this.handleEvm(id, type, request, url, port); + } else if (type.startsWith('pub(')) { + return this.handleSubstrate(id, type, request, url, port); + } else { + throw new Error(`Unable to handle message of type ${type}`); + } + } } diff --git a/packages/extension-base/src/page/bitcoin/OpenBitProvider.ts b/packages/extension-base/src/page/bitcoin/OpenBitProvider.ts new file mode 100644 index 00000000000..76c30068358 --- /dev/null +++ b/packages/extension-base/src/page/bitcoin/OpenBitProvider.ts @@ -0,0 +1,58 @@ +// Copyright 2019-2022 @subwallet/extension authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { BitcoinProviderError } from '@subwallet/extension-base/background/errors/BitcoinProviderError'; +import { version } from '@subwallet/extension-base/page/params'; +import { OpenBitProviderType } from '@subwallet/extension-inject/types'; + +import { sendMessage } from '../message'; + +export const OpenBitProvider: OpenBitProviderType = { + isOpenBit: true, + getURL: () => { + // Implement this method + throw new Error('Method not implemented.'); + }, + authenticationRequest: (payload: string) => { + // Implement this method + throw new Error('Method not implemented.'); + }, + signatureRequest: (payload: string) => { + // Implement this method + throw new Error('Method not implemented.'); + }, + structuredDataSignatureRequest: (payload: string) => { + // Implement this method + throw new Error('Method not implemented.'); + }, + transactionRequest: (payload: string) => { + // Implement this method + throw new Error('Method not implemented.'); + }, + psbtRequest: (payload: string) => { + // Implement this method + throw new Error('Method not implemented.'); + }, + profileUpdateRequest: (payload: string) => { + // Implement this method + throw new Error('Method not implemented.'); + }, + request: (method: string, params?: any[] | undefined) => { + // Implement this method + return new Promise((resolve, reject) => { + sendMessage('bitcoin(request)', { method, params }) + .then((result) => { + resolve(result as Record); + }) + .catch((e: BitcoinProviderError) => { + reject(e); + }); + }); + }, + getProductInfo: () => { + return { + name: 'OpenBit', + version: version + }; + } +}; diff --git a/packages/extension-base/src/page/SubWalleEvmProvider.ts b/packages/extension-base/src/page/evm/OpenBitEvmProvider.ts similarity index 100% rename from packages/extension-base/src/page/SubWalleEvmProvider.ts rename to packages/extension-base/src/page/evm/OpenBitEvmProvider.ts diff --git a/packages/extension-base/src/page/index.ts b/packages/extension-base/src/page/index.ts index 616ac992ecf..f9fcb76fb8a 100644 --- a/packages/extension-base/src/page/index.ts +++ b/packages/extension-base/src/page/index.ts @@ -1,57 +1,12 @@ -// Copyright 2019-2022 @polkadot/extension authors & contributors +// Copyright 2019-2022 @subwallet/extension authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { MessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithNullRequest, MessageTypesWithSubscriptions, RequestTypes, ResponseTypes, SubscriptionMessageTypes, TransportRequestMessage, TransportResponseMessage } from '../background/types'; - -import { ProviderError } from '@subwallet/extension-base/background/errors/ProviderError'; -import { ProviderErrorType } from '@subwallet/extension-base/background/KoniTypes'; -import { OpenBitEvmProvider } from '@subwallet/extension-base/page/SubWalleEvmProvider'; +import { OpenBitEvmProvider } from '@subwallet/extension-base/page/evm/OpenBitEvmProvider'; import { EvmProvider } from '@subwallet/extension-inject/types'; -import { MESSAGE_ORIGIN_PAGE } from '../defaults'; -import { getId } from '../utils/getId'; -import Injected from './Injected'; -// when sending a message from the injector to the extension, we -// - create an event - this we send to the loader -// - the loader takes this event and uses port.postMessage to background -// - on response, the loader creates a reponse event -// - this injector, listens on the events, maps it to the original -// - resolves/rejects the promise with the result (or sub data) - -export interface Handler { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - resolve: (data?: any) => void; - reject: (error: Error) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - subscriber?: (data: any) => void; -} - -export type Handlers = Record; - -const handlers: Handlers = {}; - -// a generic message sender that creates an event, returning a promise that will -// resolve once the event is resolved (by the response listener just below this) -export function sendMessage(message: TMessageType): Promise; -export function sendMessage(message: TMessageType, request: RequestTypes[TMessageType]): Promise; -export function sendMessage(message: TMessageType, request: RequestTypes[TMessageType], subscriber: (data: SubscriptionMessageTypes[TMessageType]) => void): Promise; - -export function sendMessage (message: TMessageType, request?: RequestTypes[TMessageType], subscriber?: (data: unknown) => void): Promise { - return new Promise((resolve, reject): void => { - const id = getId(); - - handlers[id] = { reject, resolve, subscriber }; - - const transportRequestMessage: TransportRequestMessage = { - id, - message, - origin: MESSAGE_ORIGIN_PAGE, - request: request || null as RequestTypes[TMessageType] - }; - - window.postMessage(transportRequestMessage, '*'); - }); -} +import Injected from './substrate/Injected'; +import { sendMessage } from './message'; +import { version } from './params'; // the enable function, called by the dapp to allow access export async function enable (origin: string): Promise { @@ -60,29 +15,9 @@ export async function enable (origin: string): Promise { return new Injected(sendMessage); } -export function handleResponse (data: TransportResponseMessage & { subscription?: string }): void { - const handler = handlers[data.id]; - - if (!handler) { - console.error(`Unknown response: ${JSON.stringify(data)}`); - - return; - } - - if (!handler.subscriber) { - delete handlers[data.id]; - } - - if (data.subscription) { - // eslint-disable-next-line @typescript-eslint/ban-types - (handler.subscriber as Function)(data.subscription); - } else if (data.error) { - handler.reject(new ProviderError(ProviderErrorType.INTERNAL_ERROR, data.error, data.errorCode)); - } else { - handler.resolve(data.response); - } -} - -export function initEvmProvider (version: string): EvmProvider { +export function initEvmProvider (): EvmProvider { return new OpenBitEvmProvider(sendMessage, version); } + +export * from './message'; +export * from './bitcoin/OpenBitProvider'; diff --git a/packages/extension-base/src/page/message.ts b/packages/extension-base/src/page/message.ts new file mode 100644 index 00000000000..702609a51fd --- /dev/null +++ b/packages/extension-base/src/page/message.ts @@ -0,0 +1,74 @@ +// Copyright 2019-2022 @subwallet/extension authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { MessageTypes, MessageTypesWithNoSubscriptions, MessageTypesWithNullRequest, MessageTypesWithSubscriptions, RequestTypes, ResponseTypes, SubscriptionMessageTypes, TransportRequestMessage, TransportResponseMessage } from '../background/types'; + +import { ProviderError } from '@subwallet/extension-base/background/errors/ProviderError'; +import { ProviderErrorType } from '@subwallet/extension-base/background/KoniTypes'; + +import { MESSAGE_ORIGIN_PAGE } from '../defaults'; +import { getId } from '../utils/getId'; +// when sending a message from the injector to the extension, we +// - create an event - this we send to the loader +// - the loader takes this event and uses port.postMessage to background +// - on response, the loader creates a reponse event +// - this injector, listens on the events, maps it to the original +// - resolves/rejects the promise with the result (or sub data) + +export interface Handler { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve: (data?: any) => void; + reject: (error: Error) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + subscriber?: (data: any) => void; +} + +export type Handlers = Record; + +const handlers: Handlers = {}; + +// a generic message sender that creates an event, returning a promise that will +// resolve once the event is resolved (by the response listener just below this) +export function sendMessage(message: TMessageType): Promise; +export function sendMessage(message: TMessageType, request: RequestTypes[TMessageType]): Promise; +export function sendMessage(message: TMessageType, request: RequestTypes[TMessageType], subscriber: (data: SubscriptionMessageTypes[TMessageType]) => void): Promise; + +export function sendMessage (message: TMessageType, request?: RequestTypes[TMessageType], subscriber?: (data: unknown) => void): Promise { + return new Promise((resolve, reject): void => { + const id = getId(); + + handlers[id] = { reject, resolve, subscriber }; + + const transportRequestMessage: TransportRequestMessage = { + id, + message, + origin: MESSAGE_ORIGIN_PAGE, + request: request || null as RequestTypes[TMessageType] + }; + + window.postMessage(transportRequestMessage, '*'); + }); +} + +export function handleResponse (data: TransportResponseMessage & { subscription?: string }): void { + const handler = handlers[data.id]; + + if (!handler) { + console.error(`Unknown response: ${JSON.stringify(data)}`); + + return; + } + + if (!handler.subscriber) { + delete handlers[data.id]; + } + + if (data.subscription) { + // eslint-disable-next-line @typescript-eslint/ban-types + (handler.subscriber as Function)(data.subscription); + } else if (data.error) { + handler.reject(new ProviderError(ProviderErrorType.INTERNAL_ERROR, data.error, data.errorCode)); + } else { + handler.resolve(data.response); + } +} diff --git a/packages/extension-base/src/page/params.ts b/packages/extension-base/src/page/params.ts new file mode 100644 index 00000000000..993a8df1db2 --- /dev/null +++ b/packages/extension-base/src/page/params.ts @@ -0,0 +1,4 @@ +// Copyright 2019-2022 @subwallet/extension authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export const version = process.env.PKG_VERSION as string || '0.0.0'; diff --git a/packages/extension-base/src/page/Accounts.ts b/packages/extension-base/src/page/substrate/Accounts.ts similarity index 95% rename from packages/extension-base/src/page/Accounts.ts rename to packages/extension-base/src/page/substrate/Accounts.ts index 92639a678c2..a9f0cca3fb6 100644 --- a/packages/extension-base/src/page/Accounts.ts +++ b/packages/extension-base/src/page/substrate/Accounts.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { InjectedAccount, InjectedAccounts, Unsubcall } from '@subwallet/extension-inject/types'; -import type { SendRequest } from './types'; +import type { SendRequest } from '../types'; // External to class, this.# is not private enough (yet) let sendRequest: SendRequest; diff --git a/packages/extension-base/src/page/Injected.ts b/packages/extension-base/src/page/substrate/Injected.ts similarity index 93% rename from packages/extension-base/src/page/Injected.ts rename to packages/extension-base/src/page/substrate/Injected.ts index 0779db4addc..fa738e3f53e 100644 --- a/packages/extension-base/src/page/Injected.ts +++ b/packages/extension-base/src/page/substrate/Injected.ts @@ -1,8 +1,8 @@ // Copyright 2019-2022 @polkadot/extension authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { SendRequest } from '@subwallet/extension-base/page/types'; import type { Injected } from '@subwallet/extension-inject/types'; -import type { SendRequest } from './types'; import Accounts from './Accounts'; import Metadata from './Metadata'; diff --git a/packages/extension-base/src/page/Metadata.ts b/packages/extension-base/src/page/substrate/Metadata.ts similarity index 92% rename from packages/extension-base/src/page/Metadata.ts rename to packages/extension-base/src/page/substrate/Metadata.ts index 8cf0ac5e66a..52af76d37ee 100644 --- a/packages/extension-base/src/page/Metadata.ts +++ b/packages/extension-base/src/page/substrate/Metadata.ts @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import type { InjectedMetadata, InjectedMetadataKnown, MetadataDef } from '@subwallet/extension-inject/types'; -import type { SendRequest } from './types'; import { RequestAddPspToken } from '@subwallet/extension-base/background/KoniTypes'; +import { SendRequest } from '@subwallet/extension-base/page/types'; // External to class, this.# is not private enough (yet) let sendRequest: SendRequest; diff --git a/packages/extension-base/src/page/PostMessageProvider.ts b/packages/extension-base/src/page/substrate/PostMessageProvider.ts similarity index 98% rename from packages/extension-base/src/page/PostMessageProvider.ts rename to packages/extension-base/src/page/substrate/PostMessageProvider.ts index 23a0f1287cd..936b69286ea 100644 --- a/packages/extension-base/src/page/PostMessageProvider.ts +++ b/packages/extension-base/src/page/substrate/PostMessageProvider.ts @@ -1,10 +1,10 @@ // Copyright 2019-2022 @polkadot/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { SendRequest } from '@subwallet/extension-base/page/types'; import type { InjectedProvider, ProviderList, ProviderMeta } from '@subwallet/extension-inject/types'; import type { ProviderInterfaceEmitCb, ProviderInterfaceEmitted } from '@polkadot/rpc-provider/types'; import type { AnyFunction } from '@polkadot/types/types'; -import type { SendRequest } from './types'; import EventEmitter from 'eventemitter3'; diff --git a/packages/extension-base/src/page/Signer.ts b/packages/extension-base/src/page/substrate/Signer.ts similarity index 95% rename from packages/extension-base/src/page/Signer.ts rename to packages/extension-base/src/page/substrate/Signer.ts index 92ce69b5654..2dcbc5ad918 100644 --- a/packages/extension-base/src/page/Signer.ts +++ b/packages/extension-base/src/page/substrate/Signer.ts @@ -1,9 +1,9 @@ // Copyright 2019-2022 @polkadot/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { SendRequest } from '@subwallet/extension-base/page/types'; import type { Signer as SignerInterface, SignerResult } from '@polkadot/api/types'; import type { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; -import type { SendRequest } from './types'; // External to class, this.# is not private enough (yet) let sendRequest: SendRequest; diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/index.ts index 6652dc81f53..f86c4ba48d3 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/index.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/index.ts @@ -231,6 +231,19 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement return eventEmitter; } + simpleSendRawTransaction (rawTransaction: string) { + return this.addRequest(async (): Promise => { + const _rs = await postRequest(this.getUrl('tx'), rawTransaction, this.headers, false); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.simpleSendRawTransaction', rs.message); + } + + return rs.result; + }, 0); + } + async getRunes (address: string) { const runesFullList: RunesInfoByAddress[] = []; const pageSize = 60; diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/types.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/types.ts index e104e6f1397..d92c0012724 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/types.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/types.ts @@ -22,6 +22,7 @@ export interface BitcoinApiStrategy extends Omit; getTxHex (txHash: string): Promise; sendRawTransaction (rawTransaction: string): EventEmitter; + simpleSendRawTransaction (rawTransaction: string): Promise; } export interface BitcoinTransactionEventMap { diff --git a/packages/extension-base/src/services/request-service/handler/AuthRequestHandler.ts b/packages/extension-base/src/services/request-service/handler/AuthRequestHandler.ts index b33e4158f59..961572296ad 100644 --- a/packages/extension-base/src/services/request-service/handler/AuthRequestHandler.ts +++ b/packages/extension-base/src/services/request-service/handler/AuthRequestHandler.ts @@ -5,7 +5,7 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { AuthRequestV2, ResultResolver } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType, AuthorizeRequest, RequestAuthorizeTab, Resolver } from '@subwallet/extension-base/background/types'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; -import { _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { _isChainBitcoinCompatible, _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { KeyringService } from '@subwallet/extension-base/services/keyring-service'; import RequestService from '@subwallet/extension-base/services/request-service'; import { PREDEFINED_CHAIN_DAPP_CHAIN_MAP, WEB_APP_URL } from '@subwallet/extension-base/services/request-service/constants'; @@ -13,6 +13,7 @@ import { AuthUrls } from '@subwallet/extension-base/services/request-service/typ import AuthorizeStore from '@subwallet/extension-base/stores/Authorize'; import { createPromiseHandler, getDomainFromUrl, PromiseHandler, stripUrl } from '@subwallet/extension-base/utils'; import { getId } from '@subwallet/extension-base/utils/getId'; +import { isBitcoinAddress } from '@subwallet/keyring'; import { BehaviorSubject } from 'rxjs'; import { isEthereumAddress } from '@polkadot/util-crypto'; @@ -116,6 +117,18 @@ export default class AuthRequestHandler { } } + if (['bitcoin'].includes(options.accessType)) { + const bitcoinChains = Object.values(chainInfoMaps).filter(_isChainBitcoinCompatible); + + chainInfo = (defaultChain ? chainInfoMaps[defaultChain] : bitcoinChains.find((chain) => chainStateMap[chain.slug]?.active)) || bitcoinChains[0]; + + if (options.autoActive) { + if (!needEnableChains.includes(chainInfo?.slug)) { + needEnableChains.push(chainInfo?.slug); + } + } + } + needEnableChains = needEnableChains.filter((slug) => !chainStateMap[slug]?.active); needEnableChains.length > 0 && this.#chainService.enableChains(needEnableChains); @@ -214,7 +227,7 @@ export default class AuthRequestHandler { private authorizePromiseMap: Record> = {}; public async authorizeUrlV2 (url: string, request: RequestAuthorizeTab): Promise { let authList = await this.getAuthList(); - let accountAuthType = request.accountAuthType || 'substrate'; + let accountAuthType = request.accountAuthType || 'both'; request.accountAuthType = accountAuthType; @@ -291,8 +304,8 @@ export default class AuthRequestHandler { if (accountAuthType === 'evm') { allowedListByRequestType = allowedListByRequestType.filter((a) => isEthereumAddress(a)); - } else if (accountAuthType === 'substrate') { - allowedListByRequestType = allowedListByRequestType.filter((a) => !isEthereumAddress(a)); + } else if (accountAuthType === 'bitcoin') { + allowedListByRequestType = allowedListByRequestType.filter((a) => isBitcoinAddress(a)); } if (!confirmAnotherType && !request.reConfirm && allowedListByRequestType.length !== 0) { diff --git a/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts b/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts index 1cd5ddafa8d..7d1ec9eb2e7 100644 --- a/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts +++ b/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts @@ -1,31 +1,48 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { ConfirmationDefinitionsBitcoin, ConfirmationsQueueBitcoin, ConfirmationsQueueItemOptions, ConfirmationTypeBitcoin, RequestConfirmationCompleteBitcoin } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinProviderError } from '@subwallet/extension-base/background/errors/BitcoinProviderError'; +import { BitcoinProviderErrorType, ConfirmationDefinitionsBitcoin, ConfirmationsQueueBitcoin, ConfirmationsQueueItemOptions, ConfirmationTypeBitcoin, ExtrinsicDataTypeMap, RequestConfirmationCompleteBitcoin, SignMessageBitcoinResult, SignPsbtBitcoinResult } from '@subwallet/extension-base/background/KoniTypes'; import { ConfirmationRequestBase, Resolver } from '@subwallet/extension-base/background/types'; +import { getBitcoinTransactionObject } from '@subwallet/extension-base/services/balance-service/helpers'; +import { ChainService } from '@subwallet/extension-base/services/chain-service'; +import FeeService from '@subwallet/extension-base/services/fee-service/service'; import RequestService from '@subwallet/extension-base/services/request-service'; +import TransactionService from '@subwallet/extension-base/services/transaction-service'; +import { TransactionEventResponse } from '@subwallet/extension-base/services/transaction-service/types'; +import { GetFeeFunction } from '@subwallet/extension-base/types'; +import { createPromiseHandler } from '@subwallet/extension-base/utils'; import { isInternalRequest } from '@subwallet/extension-base/utils/request'; import keyring from '@subwallet/ui-keyring'; import { Psbt } from 'bitcoinjs-lib'; +import * as bitcoin from 'bitcoinjs-lib'; import { t } from 'i18next'; import { BehaviorSubject } from 'rxjs'; -import { logger as createLogger } from '@polkadot/util'; +import { isArray, logger as createLogger } from '@polkadot/util'; import { Logger } from '@polkadot/util/types'; export default class BitcoinRequestHandler { readonly #requestService: RequestService; + readonly #chainService: ChainService; + readonly #transactionService: TransactionService; + readonly #feeService: FeeService; readonly #logger: Logger; private readonly confirmationsQueueSubjectBitcoin = new BehaviorSubject({ bitcoinSignatureRequest: {}, bitcoinSendTransactionRequest: {}, - bitcoinWatchTransactionRequest: {} + bitcoinWatchTransactionRequest: {}, + bitcoinSendTransactionRequestAfterConfirmation: {}, + bitcoinSignPsbtRequest: {} }); private readonly confirmationsPromiseMap: Record, validator?: (rs: any) => Error | undefined }> = {}; - constructor (requestService: RequestService) { + constructor (requestService: RequestService, chainService: ChainService, feeService: FeeService, transactionService: TransactionService) { this.#requestService = requestService; + this.#chainService = chainService; + this.#feeService = feeService; + this.#transactionService = transactionService; this.#logger = createLogger('BitcoinRequestHandler'); } @@ -56,7 +73,7 @@ export default class BitcoinRequestHandler { const payloadJson = JSON.stringify(payload); const isInternal = isInternalRequest(url); - if (['bitcoinSignatureRequest', 'bitcoinSendTransactionRequest'].includes(type)) { + if (['bitcoinSignatureRequest', 'bitcoinSendTransactionRequest', 'bitcoinSendTransactionRequestAfterConfirmation'].includes(type)) { const isAlwaysRequired = await this.#requestService.settingService.isAlwaysRequired; if (isAlwaysRequired) { @@ -134,7 +151,7 @@ export default class BitcoinRequestHandler { this.confirmationsQueueSubjectBitcoin.next(confirmations); } - signMessageBitcoin (confirmation: ConfirmationDefinitionsBitcoin['bitcoinSignatureRequest'][0]): string { + signMessageBitcoin (confirmation: ConfirmationDefinitionsBitcoin['bitcoinSignatureRequest'][0]): SignMessageBitcoinResult { const { account, payload } = confirmation.payload; const address = account.address; const pair = keyring.getPair(address); @@ -146,13 +163,21 @@ export default class BitcoinRequestHandler { // Check if payload is a string if (typeof payload === 'string') { // Assume BitcoinSigner is an instance that implements the BitcoinSigner interface - return pair.bitcoin.signMessage(payload, false); // Assuming compressed = false + return { + signature: pair.bitcoin.signMessage(payload), + message: payload, + address + }; // Assuming compressed = false } else if (payload instanceof Uint8Array) { // Check if payload is a byte array (Uint8Array) // Convert Uint8Array to string const payloadString = Buffer.from(payload).toString('hex'); // Assume BitcoinSigner is an instance that implements the BitcoinSigner interface - return pair.bitcoin.signMessage(payloadString, false); // Assuming compressed = false + return { + signature: pair.bitcoin.signMessage(payloadString), + message: payload.toString(), + address + }; // Assuming compressed = false } else { // Handle the case where payload is invalid throw new Error('Invalid payload type'); @@ -171,7 +196,6 @@ export default class BitcoinRequestHandler { keyring.unlockPair(pair.address); } - // Create a new Psbt object const psbt = Psbt.fromHex(hashPayload); // Finalize all inputs in the Psbt @@ -184,20 +208,142 @@ export default class BitcoinRequestHandler { return signedTransaction.extractTransaction().toHex(); } - private async decorateResultBitcoin (t: T, request: ConfirmationDefinitionsBitcoin[T][0], result: ConfirmationDefinitionsBitcoin[T][1]) { - if (!result.payload) { - if (t === 'bitcoinSignatureRequest') { - result.payload = this.signMessageBitcoin(request as ConfirmationDefinitionsBitcoin['bitcoinSignatureRequest'][0]); - } else if (t === 'bitcoinSendTransactionRequest') { - result.payload = this.signTransactionBitcoin(request as ConfirmationDefinitionsBitcoin['bitcoinSendTransactionRequest'][0]); + private async signTransactionBitcoinWithPayload (request: ConfirmationDefinitionsBitcoin['bitcoinSendTransactionRequestAfterConfirmation'][0]): Promise { + const transaction = this.#transactionService.getTransaction(request.id); + const { chain, emitterTransaction, feeCustom, feeOption, id } = transaction; + const { from, to, value } = transaction.data as ExtrinsicDataTypeMap['transfer.balance']; + + if (!emitterTransaction) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INTERNAL_ERROR); + } + + const chainInfo = this.#chainService.getChainInfoByKey(chain); + const bitcoinApi = this.#chainService.getBitcoinApi(chain); + const eventData: TransactionEventResponse = { + id, + errors: [], + warnings: [], + extrinsicHash: id + }; + + const network = chainInfo.isTestnet ? bitcoin.networks.testnet : bitcoin.networks.bitcoin; + + const getChainFee: GetFeeFunction = (id, chain, type) => { + return this.#feeService.subscribeChainFee(id, chain, type); + }; + + const [psbt] = await getBitcoinTransactionObject({ + bitcoinApi, + from, + getChainFee, + chain: chain, + feeCustom, + feeOption, + transferAll: false, + value: value || '0', + to, + network + }); + + const pair = keyring.getPair(from); + + // Unlock the pair if it is locked + if (pair.isLocked) { + keyring.unlockPair(pair.address); + } + + // Finalize all inputs in the Psbt + + // Sign the Psbt using the pair's bitcoin object + const signedTransaction = pair.bitcoin.signTransaction(psbt, psbt.txInputs.map((v, i) => i)); + + signedTransaction.finalizeAllInputs(); + + const signature = signedTransaction.extractTransaction().toHex(); + + this.#transactionService.emitterEventTransaction(emitterTransaction, eventData, chainInfo.slug, signature); + + const { promise, reject, resolve } = createPromiseHandler(); + + emitterTransaction.on('extrinsicHash', (data) => { + if (!data.extrinsicHash) { + reject(BitcoinProviderErrorType.INTERNAL_ERROR); + } else { + resolve(data.extrinsicHash); } + }); - if (t === 'bitcoinSignatureRequest' || t === 'bitcoinSendTransactionRequest') { - const isAlwaysRequired = await this.#requestService.settingService.isAlwaysRequired; + emitterTransaction.on('error', (error) => { + reject(error); + }); - if (isAlwaysRequired) { - this.#requestService.keyringService.lock(); - } + return promise; + } + + private async signPsbt (request: ConfirmationDefinitionsBitcoin['bitcoinSignPsbtRequest'][0]): Promise { + // Extract necessary information from the BitcoinSendTransactionRequest + const { account, payload } = request.payload; + const { allowedSighash, broadcast, network, psbt, signAtIndex } = payload; + + // todo: validate type of the account + + if (Object.keys(account).length === 0) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, 'Please connect to Wallet to try this request'); + } + + const pair = keyring.getPair(account.address); + + // Unlock the pair if it is locked + if (pair.isLocked) { + keyring.unlockPair(pair.address); + } + + const signAtIndexGenerate = signAtIndex ? (isArray(signAtIndex) ? signAtIndex : [signAtIndex]) : [...(Array(psbt.inputCount) as number[])].map((_, i) => i); + + console.log(signAtIndexGenerate); + // Sign the Psbt using the pair's bitcoin object + const psptSignedTransaction = pair.bitcoin.signTransaction(psbt, signAtIndexGenerate, allowedSighash); + + if (!broadcast) { + for (const index of signAtIndexGenerate) { + psptSignedTransaction.finalizeInput(index); + } + + return { + psbt: psptSignedTransaction.toHex() + }; + } + + psptSignedTransaction.finalizeAllInputs(); + + const chain = network === 'mainnet' ? 'bitcoin' : 'bitcoinTestnet'; + + const txid = await this.#chainService.getBitcoinApi(chain).api.simpleSendRawTransaction(psptSignedTransaction.extractTransaction().toHex()); + + console.log('TXID', txid); + + return { + psbt: psptSignedTransaction.toHex(), + txid + }; + } + + private async decorateResultBitcoin (t: T, request: ConfirmationDefinitionsBitcoin[T][0], result: ConfirmationDefinitionsBitcoin[T][1]) { + if (t === 'bitcoinSignatureRequest') { + result.payload = this.signMessageBitcoin(request as ConfirmationDefinitionsBitcoin['bitcoinSignatureRequest'][0]); + } else if (t === 'bitcoinSendTransactionRequest') { + result.payload = this.signTransactionBitcoin(request as ConfirmationDefinitionsBitcoin['bitcoinSendTransactionRequest'][0]); + } else if (t === 'bitcoinSignPsbtRequest') { + result.payload = await this.signPsbt(request as ConfirmationDefinitionsBitcoin['bitcoinSignPsbtRequest'][0]); + } else if (t === 'bitcoinSendTransactionRequestAfterConfirmation') { + result.payload = await this.signTransactionBitcoinWithPayload(request as ConfirmationDefinitionsBitcoin['bitcoinSendTransactionRequestAfterConfirmation'][0]); + } + + if (t === 'bitcoinSignatureRequest' || t === 'bitcoinSendTransactionRequest' || t === 'bitcoinSignPsbtRequest' || t === 'bitcoinSendTransactionRequestAfterConfirmation') { + const isAlwaysRequired = await this.#requestService.settingService.isAlwaysRequired; + + if (isAlwaysRequired) { + this.#requestService.keyringService.lock(); } } } @@ -208,6 +354,7 @@ export default class BitcoinRequestHandler { for (const ct in request) { const type = ct as ConfirmationTypeBitcoin; const result = request[type] as ConfirmationDefinitionsBitcoin[typeof type][1]; + const { id, isApproved } = result; const { resolver, validator } = this.confirmationsPromiseMap[id]; const confirmation = confirmations[type][id]; @@ -218,12 +365,16 @@ export default class BitcoinRequestHandler { } if (isApproved) { - // Fill signature for some special type - await this.decorateResultBitcoin(type, confirmation, result); - const error = validator && validator(result); - - if (error) { - resolver.reject(error); + try { + // Fill signature for some special type + await this.decorateResultBitcoin(type, confirmation, result); + const error = validator && validator(result); + + if (error) { + resolver.reject(error); + } + } catch (e) { + resolver.reject(e as Error); } } diff --git a/packages/extension-base/src/services/request-service/index.ts b/packages/extension-base/src/services/request-service/index.ts index 24b59a8bd1e..47a88e70ad6 100644 --- a/packages/extension-base/src/services/request-service/index.ts +++ b/packages/extension-base/src/services/request-service/index.ts @@ -4,8 +4,10 @@ import { AuthRequestV2, ConfirmationDefinitions, ConfirmationDefinitionsBitcoin, ConfirmationsQueue, ConfirmationsQueueBitcoin, ConfirmationsQueueItemOptions, ConfirmationType, ConfirmationTypeBitcoin, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType, AccountJson, AuthorizeRequest, MetadataRequest, RequestAuthorizeTab, RequestSign, ResponseSigning, SigningRequest } from '@subwallet/extension-base/background/types'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; +import FeeService from '@subwallet/extension-base/services/fee-service/service'; import { KeyringService } from '@subwallet/extension-base/services/keyring-service'; import SettingService from '@subwallet/extension-base/services/setting-service/SettingService'; +import TransactionService from '@subwallet/extension-base/services/transaction-service'; import { WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; import { MetadataDef } from '@subwallet/extension-inject/types'; import { BehaviorSubject } from 'rxjs'; @@ -31,7 +33,7 @@ export default class RequestService { readonly #notSupportWCRequestHandler: NotSupportWCRequestHandler; // Common - constructor (chainService: ChainService, settingService: SettingService, keyringService: KeyringService) { + constructor (chainService: ChainService, settingService: SettingService, keyringService: KeyringService, feeService: FeeService, transactionService: TransactionService) { this.#chainService = chainService; this.settingService = settingService; this.keyringService = keyringService; @@ -40,7 +42,7 @@ export default class RequestService { this.#authRequestHandler = new AuthRequestHandler(this, this.#chainService, this.keyringService); this.#substrateRequestHandler = new SubstrateRequestHandler(this); this.#evmRequestHandler = new EvmRequestHandler(this); - this.#bitcoinRequestHandler = new BitcoinRequestHandler(this); + this.#bitcoinRequestHandler = new BitcoinRequestHandler(this, this.#chainService, feeService, transactionService); this.#connectWCRequestHandler = new ConnectWCRequestHandler(this); this.#notSupportWCRequestHandler = new NotSupportWCRequestHandler(this); diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 12915a8da0a..e64a8764f14 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -368,6 +368,56 @@ export default class TransactionService { return validatedTransaction; } + public async handleTransactionAfterConfirmation (transaction: SWTransactionInput): Promise { + const validatedTransaction = await this.generalValidate(transaction); + const stopByErrors = validatedTransaction.errors.length > 0; + const stopByWarnings = validatedTransaction.warnings.length > 0 && !validatedTransaction.ignoreWarnings; + + if (stopByErrors || stopByWarnings) { + // @ts-ignore + 'transaction' in validatedTransaction && delete validatedTransaction.transaction; + 'additionalValidator' in validatedTransaction && delete validatedTransaction.additionalValidator; + 'eventsHandler' in validatedTransaction && delete validatedTransaction.eventsHandler; + + return validatedTransaction; + } + + validatedTransaction.warnings = []; + + const transactionsSubject = this.transactions; + const emitter = new EventEmitter(); + + // Fill transaction default info + const transactionUpdated = this.fillTransactionDefaultInfo(transaction); + + // Add Transaction + transactionsSubject[transactionUpdated.id] = { ...transactionUpdated, emitterTransaction: emitter }; + this.transactionSubject.next({ ...transactionsSubject }); + + emitter.on('success', (data: TransactionEventResponse) => { + validatedTransaction.id = data.id; + validatedTransaction.extrinsicHash = data.extrinsicHash; + }); + + emitter.on('signed', (data: TransactionEventResponse) => { + validatedTransaction.id = data.id; + validatedTransaction.extrinsicHash = data.extrinsicHash; + }); + + emitter.on('error', (data: TransactionEventResponse) => { + if (data.errors.length > 0) { + validatedTransaction.errors.push(...data.errors); + } + }); + + // @ts-ignore + 'transaction' in validatedTransaction && delete validatedTransaction.transaction; + 'additionalValidator' in validatedTransaction && delete validatedTransaction.additionalValidator; + 'eventsHandler' in validatedTransaction && delete validatedTransaction.eventsHandler; + + return { ...validatedTransaction }; + } + private async sendTransaction (transaction: SWTransaction): Promise { let emitter: TransactionEmitter; @@ -1076,6 +1126,33 @@ export default class TransactionService { return emitter; } + public emitterEventTransaction = (emitter: TransactionEmitter, eventData: TransactionEventResponse, chain: string, payload: string) => { + // Emit signed event + emitter.emit('signed', eventData); + // Add start info + emitter.emit('send', eventData); + + const event = this.chainService.getBitcoinApi(chain).api.sendRawTransaction(payload); + + event.on('extrinsicHash', (txHash) => { + eventData.extrinsicHash = txHash; + emitter.emit('extrinsicHash', eventData); + }); + + event.on('success', (transactionStatus) => { + console.log(transactionStatus); + eventData.blockHash = transactionStatus.block_hash || undefined; + eventData.blockNumber = transactionStatus.block_height || undefined; + eventData.blockTime = transactionStatus.block_time ? (transactionStatus.block_time * 1000) : undefined; + emitter.emit('success', eventData); + }); + + event.on('error', (error) => { + eventData.errors.push(new TransactionError(BasicTxErrorType.UNABLE_TO_SEND, error)); + emitter.emit('error', eventData); + }); + }; + private async signAndSendEvmTransaction ({ address, chain, id, diff --git a/packages/extension-base/src/services/transaction-service/types.ts b/packages/extension-base/src/services/transaction-service/types.ts index ce2386e53dc..efb664b335f 100644 --- a/packages/extension-base/src/services/transaction-service/types.ts +++ b/packages/extension-base/src/services/transaction-service/types.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { BaseRequestSign, ChainType, ExtrinsicDataTypeMap, ExtrinsicStatus, ExtrinsicType, FeeData, ValidateTransactionResponse } from '@subwallet/extension-base/background/KoniTypes'; +import { BaseRequestSign, BitcoinTransactionConfig, ChainType, ExtrinsicDataTypeMap, ExtrinsicStatus, ExtrinsicType, FeeData, ValidateTransactionResponse } from '@subwallet/extension-base/background/KoniTypes'; import { TransactionFee } from '@subwallet/extension-base/types'; import { Psbt } from 'bitcoinjs-lib'; import EventEmitter from 'eventemitter3'; @@ -10,7 +10,7 @@ import { TransactionConfig } from 'web3-core'; import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; import { EventRecord } from '@polkadot/types/interfaces'; -export interface SWTransaction extends ValidateTransactionResponse, Partial>, TransactionFee { +export interface SWTransaction extends ValidateTransactionResponse, Partial>, TransactionFee, SWTransactionEmitter { id: string; url?: string; isInternal: boolean, @@ -24,13 +24,17 @@ export interface SWTransaction extends ValidateTransactionResponse, Partial Promise; eventsHandler?: (eventEmitter: TransactionEmitter) => void; } export type SWTransactionResult = Omit +export interface SWTransactionEmitter { + emitterTransaction?: TransactionEmitter +} + type SwInputBase = Pick & Partial>; @@ -47,6 +51,12 @@ export interface SWTransactionInput extends SwInputBase, Partial & Partial> & TransactionFee; +export type BitcoinTransactionData = { + data: Psbt, + dataBase64: string, + dataToHex: string, +} + export type ValidateTransactionResponseInput = SWTransactionInput; export type TransactionEmitter = EventEmitter; diff --git a/packages/extension-base/src/types/balance/transfer.ts b/packages/extension-base/src/types/balance/transfer.ts index 0f457953a86..b3b96fad746 100644 --- a/packages/extension-base/src/types/balance/transfer.ts +++ b/packages/extension-base/src/types/balance/transfer.ts @@ -31,3 +31,7 @@ export interface RequestSubmitTransfer extends BaseRequestSign, TransactionFee { transferAll: boolean; value: string; } + +export interface RequestSubmitTransferWithId extends RequestSubmitTransfer{ + id?: string; +} diff --git a/packages/extension-base/src/types/dApp.ts b/packages/extension-base/src/types/dApp.ts new file mode 100644 index 00000000000..3bfa0c1501b --- /dev/null +++ b/packages/extension-base/src/types/dApp.ts @@ -0,0 +1,19 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export type Response = { + result: T +} + +export type AuthAddress = { + address: string; + publicKey?: string; + tweakedPublicKey?: string; + derivationPath?: string; + isTestnet?: boolean; + type: 'p2tr' | 'p2wpkh' | 'p2sh' | 'ethereum' | 'unknown'; +} + +export type RequestAddressesResult = { + addresses: AuthAddress[]; +}; diff --git a/packages/extension-base/src/utils/account.ts b/packages/extension-base/src/utils/account.ts index 3c5c80b67ce..23c746e7133 100644 --- a/packages/extension-base/src/utils/account.ts +++ b/packages/extension-base/src/utils/account.ts @@ -1,8 +1,9 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AddressJson } from '@subwallet/extension-base/background/types'; +import { AccountAuthType, AddressJson } from '@subwallet/extension-base/background/types'; import { reformatAddress } from '@subwallet/extension-base/utils/index'; +import { getKeypairTypeByAddress } from '@subwallet/keyring'; import { SubjectInfo } from '@subwallet/ui-keyring/observable/types'; import { isAddress } from '@polkadot/util-crypto'; @@ -18,3 +19,25 @@ export function quickFormatAddressToCompare (address?: string) { export const convertSubjectInfoToAddresses = (subjectInfo: SubjectInfo): AddressJson[] => { return Object.values(subjectInfo).map((info): AddressJson => ({ address: info.json.address, type: info.type, ...info.json.meta })); }; + +export const isAddressValidWithAuthType = (address: string, accountAuthType?: AccountAuthType): boolean => { + const keypairType = getKeypairTypeByAddress(address); + + if (!['ethereum', 'bitcoin-84', 'bitcoin-86', 'bittest-84', 'bittest-86'].includes(keypairType)) { + return false; + } + + if (accountAuthType === 'both') { + return true; + } + + if (accountAuthType === 'evm') { + return keypairType === 'ethereum'; + } + + if (accountAuthType === 'bitcoin') { + return ['bitcoin-86', 'bittest-86', 'bitcoin-84', 'bittest-84'].includes(keypairType); + } + + return false; +}; diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index d0a24595156..ae33c50619d 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -33,8 +33,6 @@ export function filterUneconomicalUtxos ({ feeRate, return filteredAndSortUtxos.reduce((utxos, utxo, currentIndex) => { const utxosWithout = utxos.filter((u) => u.txid !== utxo.txid); - console.log(currentIndex); - const { fee: feeWithout, spendableAmount: spendableAmountWithout } = getSpendableAmount({ utxos: utxosWithout, feeRate, diff --git a/packages/extension-inject/package.json b/packages/extension-inject/package.json index 31f92e7bf9d..9c13d3ad08b 100644 --- a/packages/extension-inject/package.json +++ b/packages/extension-inject/package.json @@ -23,6 +23,7 @@ "@polkadot/util": "^12.6.2", "@polkadot/util-crypto": "^12.6.2", "@polkadot/x-global": "^12.2.1", + "@stacks/connect": "^7.7.1", "@subwallet/keyring": "0.1.5-beta.0", "web3-core": "^1.10.0" }, diff --git a/packages/extension-inject/src/bundle.ts b/packages/extension-inject/src/bundle.ts index f9741102824..aa8ca412c05 100644 --- a/packages/extension-inject/src/bundle.ts +++ b/packages/extension-inject/src/bundle.ts @@ -1,9 +1,7 @@ // Copyright 2019-2022 @polkadot/extension-inject authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { Injected, InjectedWindow, InjectOptions } from './types'; - -import { EIP6963ProviderDetail, EIP6963ProviderInfo, EvmProvider } from './types'; +import { EIP6963ProviderDetail, EIP6963ProviderInfo, EvmProvider, Injected, InjectedWindow, InjectOptions, OpenBitProviderType } from './types'; export { packageInfo } from './packageInfo'; @@ -91,3 +89,9 @@ export const inject6963EIP = (provider: EvmProvider) => { announceProvider(); }; + +export function injectBitcoinProvider (openBitProvider: OpenBitProviderType) { + const windowInject = window as Window & InjectedWindow; + + windowInject.OpenBitProvider = openBitProvider; +} diff --git a/packages/extension-inject/src/types.ts b/packages/extension-inject/src/types.ts index b861d90b453..82c2e253c70 100644 --- a/packages/extension-inject/src/types.ts +++ b/packages/extension-inject/src/types.ts @@ -5,6 +5,7 @@ import type { Signer as InjectedSigner } from '@polkadot/api/types'; import type { ProviderInterface } from '@polkadot/rpc-provider/types'; import type { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types'; +import { StacksProvider } from '@stacks/connect'; import { KeypairType } from '@subwallet/keyring/types'; // eslint-disable-next-line no-undef @@ -106,6 +107,10 @@ export interface EvmProvider { isConnected(): boolean, } +export interface OpenBitProviderType extends StacksProvider { + isOpenBit: boolean; +} + export interface InjectedWindowProvider { enable: (origin: string) => Promise; version: string; @@ -115,6 +120,7 @@ export interface InjectedWindow extends This { injectedWeb3: Record; ethereum: EvmProvider; OpenBit: EvmProvider; + OpenBitProvider: OpenBitProviderType; } export type InjectedExtension = InjectedExtensionInfo & Injected; diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx index 4d116ece5b2..8f0740e51d7 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx @@ -18,7 +18,7 @@ import styled from 'styled-components'; import { SignerPayloadJSON } from '@polkadot/types/types'; import { ConfirmationHeader } from './parts'; -import { AddNetworkConfirmation, AddTokenConfirmation, AuthorizeConfirmation, ConnectWalletConnectConfirmation, EvmSignatureConfirmation, EvmTransactionConfirmation, MetadataConfirmation, NotSupportConfirmation, NotSupportWCConfirmation, SignConfirmation, TransactionConfirmation } from './variants'; +import { AddNetworkConfirmation, AddTokenConfirmation, AuthorizeConfirmation, BitcoinSendTransactionRequestConfirmation, BitcoinSignatureConfirmation, BitcoinSignPsbtConfirmation, ConnectWalletConnectConfirmation, EvmSignatureConfirmation, EvmTransactionConfirmation, MetadataConfirmation, NotSupportConfirmation, NotSupportWCConfirmation, SignConfirmation, TransactionConfirmation } from './variants'; type Props = ThemeProps @@ -31,7 +31,9 @@ const titleMap: Record = { evmWatchTransactionRequest: detectTranslate('Transaction request'), bitcoinSignatureRequest: detectTranslate('Signature request'), bitcoinSendTransactionRequest: detectTranslate('Transaction request'), + bitcoinSendTransactionRequestAfterConfirmation: detectTranslate('Transaction request'), bitcoinWatchTransactionRequest: detectTranslate('Transaction request'), + bitcoinSignPsbtRequest: detectTranslate('Sign PSBT request'), metadataRequest: detectTranslate('Update metadata'), signingRequest: detectTranslate('Signature request'), switchNetworkRequest: detectTranslate('Add network request'), @@ -45,6 +47,9 @@ const Component = function ({ className }: Props) { const { confirmationQueue, numberOfConfirmations } = useConfirmationsInfo(); const [index, setIndex] = useState(0); const confirmation = confirmationQueue[index] || null; + + console.log(confirmation, 'confirmation'); + const { t } = useTranslation(); const { alertProps, closeAlert, openAlert } = useAlert(alertModalId); @@ -93,8 +98,8 @@ const Component = function ({ className }: Props) { account = request.payload.account; canSign = request.payload.canSign; isMessage = confirmation.type === 'evmSignatureRequest'; - } else if (['bitcoinSignatureRequest', 'bitcoinSendTransactionRequest', 'bitcoinWatchTransactionRequest'].includes(confirmation.type)) { - const request = confirmation.item as ConfirmationDefinitionsBitcoin['bitcoinSignatureRequest' | 'bitcoinSendTransactionRequest' | 'bitcoinWatchTransactionRequest'][0]; + } else if (['bitcoinSignatureRequest', 'bitcoinSendTransactionRequest', 'bitcoinWatchTransactionRequest', 'bitcoinSignPsbtRequest', 'bitcoinSendTransactionRequestAfterConfirmation'].includes(confirmation.type)) { + const request = confirmation.item as ConfirmationDefinitionsBitcoin['bitcoinSignatureRequest' | 'bitcoinSendTransactionRequest' | 'bitcoinWatchTransactionRequest' | 'bitcoinSendTransactionRequestAfterConfirmation'][0]; account = request.payload.account; canSign = request.payload.canSign; @@ -142,6 +147,27 @@ const Component = function ({ className }: Props) { type={confirmation.type} /> ); + case 'bitcoinSignatureRequest': + return ( + + ); + case 'bitcoinSignPsbtRequest': + return ( + + ); + case 'bitcoinSendTransactionRequestAfterConfirmation': + return ( + + ); case 'authorizeRequest': return ( diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx index 976049561d8..b5d24e758e5 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx @@ -1,10 +1,12 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { ConfirmationDefinitionsBitcoin, ConfirmationResult, EvmSendTransactionRequest, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinSignatureRequest, ConfirmationDefinitionsBitcoin, ConfirmationResult, EvmSendTransactionRequest, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { RequestSubmitTransferWithId } from '@subwallet/extension-base/types'; +import { wait } from '@subwallet/extension-base/utils'; import { CONFIRMATION_QR_MODAL } from '@subwallet/extension-koni-ui/constants'; import { useGetChainInfoByChainId, useLedger, useNotification, useUnlockChecker } from '@subwallet/extension-koni-ui/hooks'; -import { completeConfirmationBitcoin } from '@subwallet/extension-koni-ui/messaging'; +import { completeConfirmationBitcoin, makeTransferAfterConfirmation } from '@subwallet/extension-koni-ui/messaging'; import { AccountSignMode, BitcoinSignatureSupportType, PhosphorIcon, SigData, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { getSignMode, isBitcoinMessage, removeTransactionPersist } from '@subwallet/extension-koni-ui/utils'; import { Button, Icon, ModalContext } from '@subwallet/react-ui'; @@ -23,6 +25,8 @@ interface Props extends ThemeProps { type: BitcoinSignatureSupportType; payload: ConfirmationDefinitionsBitcoin[BitcoinSignatureSupportType][0]; extrinsicType?: ExtrinsicType; + editedPayload?: RequestSubmitTransferWithId; + canSign?: boolean; } const handleConfirm = async (type: BitcoinSignatureSupportType, id: string, payload: string) => { @@ -49,8 +53,9 @@ const handleSignature = async (type: BitcoinSignatureSupportType, id: string, si }; const Component: React.FC = (props: Props) => { - const { className, extrinsicType, id, payload, type } = props; - const { payload: { account, canSign, hashPayload } } = payload; + const { canSign, className, editedPayload, extrinsicType, id, payload, type } = props; + const { payload: { hashPayload } } = payload; + const account = (payload.payload as BitcoinSignatureRequest).account; const chainId = (payload.payload as EvmSendTransactionRequest)?.chainId || 1; const { t } = useTranslation(); @@ -105,12 +110,15 @@ const Component: React.FC = (props: Props) => { const onApprovePassword = useCallback(() => { setLoading(true); - setTimeout(() => { - handleConfirm(type, id, '').finally(() => { - setLoading(false); - }); - }, 1000); - }, [id, type]); + (type === 'bitcoinSendTransactionRequestAfterConfirmation' && editedPayload ? makeTransferAfterConfirmation(editedPayload) : wait(1000)) + .then(() => { + console.log('complete', type, id); + handleConfirm(type, id, '').finally(() => { + setLoading(false); + }); + }) + .catch(console.error); + }, [editedPayload, id, type]); const onApproveSignature = useCallback((signature: SigData) => { setLoading(true); @@ -144,7 +152,7 @@ const Component: React.FC = (props: Props) => { setLoading(true); setTimeout(() => { - const signPromise = isMessage ? ledgerSignMessage(u8aToU8a(hashPayload), account.accountIndex, account.addressOffset) : ledgerSignTransaction(hexToU8a(hashPayload), account.accountIndex, account.addressOffset); + const signPromise = isMessage ? ledgerSignMessage(u8aToU8a(hashPayload), account?.accountIndex, account?.addressOffset) : ledgerSignTransaction(hexToU8a(hashPayload), account?.accountIndex, account?.addressOffset); signPromise .then(({ signature }) => { @@ -155,7 +163,7 @@ const Component: React.FC = (props: Props) => { setLoading(false); }); }); - }, [account.accountIndex, account.addressOffset, hashPayload, isLedgerConnected, isMessage, ledger, ledgerSignMessage, ledgerSignTransaction, onApproveSignature, refreshLedger]); + }, [account?.accountIndex, account?.addressOffset, hashPayload, isLedgerConnected, isMessage, ledger, ledgerSignMessage, ledgerSignTransaction, onApproveSignature, refreshLedger]); const onConfirmInject = useCallback(() => { console.error('Not implemented yet'); @@ -249,7 +257,7 @@ const Component: React.FC = (props: Props) => { {t('Cancel')} - - - ) - } ); diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx new file mode 100644 index 00000000000..f5b6cdac360 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx @@ -0,0 +1,287 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { _ChainAsset } from '@subwallet/chain-list/types'; +import { BitcoinSendTransactionRequest, ConfirmationsQueueItem } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinFeeDetail, RequestSubmitTransferWithId, ResponseSubscribeTransfer, TransactionFee } from '@subwallet/extension-base/types'; +import { BN_ZERO, getDomainFromUrl } from '@subwallet/extension-base/utils'; +import { BitcoinFeeSelector, MetaInfo } from '@subwallet/extension-koni-ui/components'; +import { RenderFieldNodeParams } from '@subwallet/extension-koni-ui/components/Field/TransactionFee/BitcoinFeeSelector'; +import { useGetAccountByAddress } from '@subwallet/extension-koni-ui/hooks'; +import { cancelSubscription, subscribeMaxTransfer } from '@subwallet/extension-koni-ui/messaging'; +import { BitcoinSignArea } from '@subwallet/extension-koni-ui/Popup/Confirmations/parts'; +import { RootState } from '@subwallet/extension-koni-ui/stores'; +import { BitcoinSignatureSupportType, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { ActivityIndicator, Button, Icon, Number } from '@subwallet/react-ui'; +import BigN from 'bignumber.js'; +import CN from 'classnames'; +import { PencilSimpleLine } from 'phosphor-react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; + +interface Props extends ThemeProps { + type: BitcoinSignatureSupportType + request: ConfirmationsQueueItem +} + +const convertToBigN = (num: BitcoinSendTransactionRequest['value']): string | number | undefined => { + if (typeof num === 'object') { + return num.toNumber(); + } else { + return num; + } +}; + +function Component ({ className, request, type }: Props) { + const { id, payload: { account, fee, networkKey, to, tokenSlug, value } } = request; + const { t } = useTranslation(); + + const [transactionInfo, setTransactionInfo] = useState({ + id, + chain: networkKey as string, + from: account.address, + to: to as string, + tokenSlug: tokenSlug as string, + transferAll: false, + value: value?.toString() || '0' + }); + const [isFetchingInfo, setIsFetchingInfo] = useState(false); + const [isTransferAll, setIsTransferAll] = useState(false); + const [transferInfo, setTransferInfo] = useState(); + const [transactionFeeInfo, setTransactionFeeInfo] = useState({ + feeOption: fee?.options.default + }); + + const assetRegistry = useSelector((root: RootState) => root.assetRegistry.assetRegistry); + + const transferAmountValue = value?.toString() as string; + const fromValue = account.address; + const toValue = to as string; + const chainValue = networkKey as string; + const assetValue = tokenSlug as string; + + const assetInfo: _ChainAsset | undefined = useMemo(() => { + return assetRegistry[assetValue]; + }, [assetRegistry, assetValue]); + + const recipient = useGetAccountByAddress(toValue); + + // console.log(transactionRequest); + const amount = useMemo((): number => { + return new BigN(convertToBigN(request.payload.value) || 0).toNumber(); + }, [request.payload.value]); + + const renderFeeSelectorNode = useCallback((params: RenderFieldNodeParams) => { + return ( + + {params.isLoading + ? ( +
+ +
+ ) + : ( +
+ +
+ )} +
+ ); + }, [t]); + + useEffect(() => { + setTransactionInfo((prevState) => ({ ...prevState, ...transactionFeeInfo })); + }, [transactionFeeInfo]); + + useEffect(() => { + const bnTransferAmount = new BigN(transferAmountValue || '0'); + const bnMaxTransfer = new BigN(transferInfo?.maxTransferable || '0'); + + if (bnTransferAmount.gt(BN_ZERO) && bnTransferAmount.eq(bnMaxTransfer)) { + setIsTransferAll(true); + } + }, [transferInfo, transferAmountValue]); + + useEffect(() => { + let cancel = false; + let id = ''; + let timeout: NodeJS.Timeout; + + setIsFetchingInfo(true); + + const callback = (transferInfo: ResponseSubscribeTransfer) => { + if (!cancel) { + setTransferInfo(transferInfo); + id = transferInfo.id; + } else { + cancelSubscription(transferInfo.id).catch(console.error); + } + }; + + if (fromValue && assetValue) { + timeout = setTimeout(() => { + subscribeMaxTransfer({ + address: fromValue, + chain: chainValue, + token: assetValue, + isXcmTransfer: false, + destChain: chainValue, + feeOption: transactionFeeInfo?.feeOption, + feeCustom: transactionFeeInfo?.feeCustom, + value: transferAmountValue || '0', + transferAll: isTransferAll, + to: toValue + }, callback) + .then(callback) + .catch((e) => { + console.error(e); + + setTransferInfo(undefined); + }) + .finally(() => { + setIsFetchingInfo(false); + }); + }, 100); + } + + return () => { + cancel = true; + clearTimeout(timeout); + id && cancelSubscription(id).catch(console.error); + }; + }, [assetRegistry, assetValue, chainValue, fromValue, toValue, transactionFeeInfo, transferAmountValue, isTransferAll]); + + return ( + <> +
+
{getDomainFromUrl(request.url)}
+ + + + + + + + + + + + + + + {/* {!!transaction.estimateFee?.tooHigh && ( */} + {/* */} + {/* )} */} +
+ + + ); +} + +const BitcoinSendTransactionRequestConfirmation = styled(Component)(({ theme: { token } }: ThemeProps) => ({ + '&.confirmation-content.confirmation-content': { + display: 'block' + }, + + '.__origin-url': { + marginBottom: token.margin + }, + + '.__fee-editor-loading-wrapper': { + minWidth: 40, + height: 40, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }, + + '.__fee-editor.__fee-editor.__fee-editor': { + marginTop: 4, + marginRight: -10 + }, + + '.__fee-editor-value-wrapper': { + display: 'flex', + alignItems: 'center' + }, + + '.account-list': { + '.__prop-label': { + marginRight: token.marginMD, + width: '50%', + float: 'left' + } + }, + + '.network-box': { + marginTop: token.margin + }, + + '.to-account': { + marginTop: token.margin - 2 + }, + + '.__label': { + textAlign: 'left' + } +})); + +export default BitcoinSendTransactionRequestConfirmation; diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignPsbtConfirmation.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignPsbtConfirmation.tsx new file mode 100644 index 00000000000..ba6d8176b77 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignPsbtConfirmation.tsx @@ -0,0 +1,94 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { BitcoinSignPsbtRequest, ConfirmationsQueueItem } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountItemWithName, ConfirmationGeneralInfo, MetaInfo, ViewDetailIcon } from '@subwallet/extension-koni-ui/components'; +import { useOpenDetailModal } from '@subwallet/extension-koni-ui/hooks'; +import { BitcoinSignArea } from '@subwallet/extension-koni-ui/Popup/Confirmations/parts'; +import { BitcoinSignatureSupportType, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { Button } from '@subwallet/react-ui'; +import CN from 'classnames'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +import { BaseDetailModal } from '../parts'; + +interface Props extends ThemeProps { + type: BitcoinSignatureSupportType + request: ConfirmationsQueueItem +} + +function Component ({ className, request, type }: Props) { + const { id, payload } = request; + const { t } = useTranslation(); + const { account } = payload; + + const onClickDetail = useOpenDetailModal(); + + return ( + <> +
+ +
+ {t('Signature required')} +
+
+ {t('You are approving a request with the following account')} +
+ +
+ +
+
+ + + + + {JSON.stringify(request.payload.payload.txInput)} + + + {JSON.stringify(request.payload.payload.txOutput)} + + + + + ); +} + +const BitcoinSignPsbtConfirmation = styled(Component)(({ theme: { token } }: ThemeProps) => ({ + '.account-list': { + '.__prop-label': { + marginRight: token.marginMD, + width: '50%', + float: 'left' + } + }, + + '.__label': { + textAlign: 'left' + } +})); + +export default BitcoinSignPsbtConfirmation; diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignatureConfirmation.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignatureConfirmation.tsx new file mode 100644 index 00000000000..0bd863a1e99 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignatureConfirmation.tsx @@ -0,0 +1,88 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { BitcoinSignatureRequest, ConfirmationsQueueItem } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountItemWithName, ConfirmationGeneralInfo, MetaInfo, ViewDetailIcon } from '@subwallet/extension-koni-ui/components'; +import { useOpenDetailModal } from '@subwallet/extension-koni-ui/hooks'; +import { BitcoinSignArea } from '@subwallet/extension-koni-ui/Popup/Confirmations/parts'; +import { BitcoinSignatureSupportType, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { Button } from '@subwallet/react-ui'; +import CN from 'classnames'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +import { BaseDetailModal } from '../parts'; + +interface Props extends ThemeProps { + type: BitcoinSignatureSupportType + request: ConfirmationsQueueItem +} + +function Component ({ className, request, type }: Props) { + const { id, payload } = request; + const { t } = useTranslation(); + const { account } = payload; + + const onClickDetail = useOpenDetailModal(); + + return ( + <> +
+ +
+ {t('Signature required')} +
+
+ {t('You are approving a request with the following account')} +
+ +
+ +
+
+ + + + {request.payload.payload as string} + + + + ); +} + +const BitcoinSignatureConfirmation = styled(Component)(({ theme: { token } }: ThemeProps) => ({ + '.account-list': { + '.__prop-label': { + marginRight: token.marginMD, + width: '50%', + float: 'left' + } + }, + + '.__label': { + textAlign: 'left' + } +})); + +export default BitcoinSignatureConfirmation; diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/NotSupportConfirmation.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/NotSupportConfirmation.tsx index d67d6f6ef01..d66438e5aef 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/NotSupportConfirmation.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/NotSupportConfirmation.tsx @@ -6,8 +6,8 @@ import { AccountJson, ConfirmationRequestBase } from '@subwallet/extension-base/ import { AccountItemWithName, ConfirmationGeneralInfo } from '@subwallet/extension-koni-ui/components'; import { NEED_SIGN_CONFIRMATION } from '@subwallet/extension-koni-ui/constants'; import { useGetAccountTitleByAddress } from '@subwallet/extension-koni-ui/hooks'; -import { cancelSignRequest, completeConfirmation } from '@subwallet/extension-koni-ui/messaging'; -import { EvmSignatureSupportType, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { cancelSignRequest, completeConfirmation, completeConfirmationBitcoin } from '@subwallet/extension-koni-ui/messaging'; +import { BitcoinSignatureSupportType, EvmSignatureSupportType, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { Button } from '@subwallet/react-ui'; import CN from 'classnames'; import React, { useCallback, useState } from 'react'; @@ -30,6 +30,13 @@ const handleCancelEvm = async (type: EvmSignatureSupportType, id: string) => { const handleCancelSubstrate = async (id: string) => await cancelSignRequest(id); +const handleCancelBitcoin = async (type: BitcoinSignatureSupportType, id: string) => { + return await completeConfirmationBitcoin(type, { + id, + isApproved: false + } as ConfirmationResult); +}; + const Component: React.FC = (props: Props) => { const { account, className, isMessage, request, type } = props; @@ -50,6 +57,9 @@ const Component: React.FC = (props: Props) => { case 'signingRequest': promise = () => handleCancelSubstrate(request.id); break; + case 'bitcoinSignatureRequest': + promise = () => handleCancelBitcoin(type, request.id); + break; } if (promise) { @@ -82,6 +92,7 @@ const Component: React.FC = (props: Props) => { address={account?.address || ''} avatarSize={24} className='account-item' + proxyId={account?.proxyId} showUnselectIcon={true} /> 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 6447c10f23e..a5baca7a7b1 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 @@ -141,11 +141,11 @@ const Component: React.FC = (props: Props) => { ) } { - (type === 'bitcoinSignatureRequest' || type === 'bitcoinSendTransactionRequest' || type === 'bitcoinWatchTransactionRequest') && ( + (type === 'bitcoinSignatureRequest' || type === 'bitcoinSendTransactionRequest' || type === 'bitcoinWatchTransactionRequest' || type === 'bitcoinSignPsbtRequest') && ( ) diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/index.ts b/packages/extension-koni-ui/src/Popup/Confirmations/variants/index.ts index f3795bba6f6..0e87a4aa777 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/index.ts +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/index.ts @@ -12,3 +12,6 @@ export { default as NotSupportConfirmation } from './NotSupportConfirmation'; export { default as SignConfirmation } from './SignConfirmation'; export { default as TransactionConfirmation } from './Transaction'; export { default as NotSupportWCConfirmation } from './NotSupportWCConfirmation'; +export { default as BitcoinSignatureConfirmation } from './BitcoinSignatureConfirmation'; +export { default as BitcoinSignPsbtConfirmation } from './BitcoinSignPsbtConfirmation'; +export { default as BitcoinSendTransactionRequestConfirmation } from './BitcoinSendTransactionRequestConfirmation'; diff --git a/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/Detail.tsx b/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/Detail.tsx index dc5a771a7f4..2f0877d958b 100644 --- a/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/Detail.tsx +++ b/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/Detail.tsx @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import { AuthUrlInfo } from '@subwallet/extension-base/background/handlers/State'; -import { AccountJson } from '@subwallet/extension-base/background/types'; -import { AccountItemWithName, EmptyList, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; +import { AccountJson, AccountProxy } from '@subwallet/extension-base/background/types'; +import { AccountProxyItem, EmptyList, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; import { ActionItemType, ActionModal } from '@subwallet/extension-koni-ui/components/Modal/ActionModal'; import useDefaultNavigate from '@subwallet/extension-koni-ui/hooks/router/useDefaultNavigate'; -import { changeAuthorization, changeAuthorizationPerAccount, forgetSite, toggleAuthorization } from '@subwallet/extension-koni-ui/messaging'; +import { changeAuthorization, changeAuthorizationPerAccountProxy, forgetSite, toggleAuthorization } from '@subwallet/extension-koni-ui/messaging'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { updateAuthUrls } from '@subwallet/extension-koni-ui/stores/utils'; import { Theme, ThemeProps } from '@subwallet/extension-koni-ui/types'; @@ -19,8 +19,6 @@ import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import styled, { useTheme } from 'styled-components'; -import { isEthereumAddress } from '@polkadot/util-crypto'; - type Props = ThemeProps & ManageWebsiteAccessDetailParam & { authInfo: AuthUrlInfo; goBack: () => void @@ -31,23 +29,16 @@ type WrapperProps = ThemeProps; const ActionModalId = 'actionModalId'; // const FilterModalId = 'filterModalId'; -function Component ({ accountAuthType, authInfo, className = '', goBack, origin, siteName }: Props): React.ReactElement { - const accounts = useSelector((state: RootState) => state.accountState.accounts); +function Component ({ authInfo, className = '', goBack, origin, siteName }: Props): React.ReactElement { + const accountProxies = useSelector((state: RootState) => state.accountState.accountProxies); const [pendingMap, setPendingMap] = useState>({}); const { activeModal, inactiveModal } = useContext(ModalContext); const { t } = useTranslation(); const { token } = useTheme() as Theme; - const accountItems = useMemo(() => { - const accountListWithoutAll = accounts.filter((opt) => opt.address !== 'ALL'); - - if (accountAuthType === 'substrate') { - return accountListWithoutAll.filter((acc) => !isEthereumAddress(acc.address)); - } else if (accountAuthType === 'evm') { - return accountListWithoutAll.filter((acc) => isEthereumAddress(acc.address)); - } else { - return accountListWithoutAll; - } - }, [accountAuthType, accounts]); + + const accountProxyItems = useMemo(() => { + return accountProxies.filter((opt) => opt.proxyId !== 'ALL' && !opt.isReadOnly); + }, [accountProxies]); const onOpenActionModal = useCallback(() => { activeModal(ActionModalId); @@ -115,23 +106,23 @@ function Component ({ accountAuthType, authInfo, className = '', goBack, origin, return result; }, [authInfo.isAllowed, onCloseActionModal, origin, t, token]); - const renderItem = useCallback((item: AccountJson) => { - const isEnabled: boolean = authInfo.isAllowedMap[item.address]; + const renderItem = useCallback((item: AccountProxy) => { + const isEnabled: boolean = item.accounts.some((a) => authInfo.isAllowedMap[a.address]); const onClick = () => { setPendingMap((prevMap) => { return { ...prevMap, - [item.address]: !isEnabled + [item.proxyId]: !isEnabled }; }); - changeAuthorizationPerAccount(item.address, !isEnabled, origin, updateAuthUrls) + changeAuthorizationPerAccountProxy(item.proxyId, !isEnabled, origin, updateAuthUrls) .catch(console.log) .finally(() => { setPendingMap((prevMap) => { const newMap = { ...prevMap }; - delete newMap[item.address]; + delete newMap[item.proxyId]; return newMap; }); @@ -139,22 +130,21 @@ function Component ({ accountAuthType, authInfo, className = '', goBack, origin, }; return ( - )} /> ); - }, [authInfo.isAllowed, authInfo.isAllowedMap, origin, pendingMap, token.sizeLG]); + }, [authInfo.isAllowed, authInfo.isAllowedMap, origin, pendingMap]); const searchFunc = useCallback((item: AccountJson, searchText: string) => { const searchTextLowerCase = searchText.toLowerCase(); @@ -207,12 +197,10 @@ function Component ({ accountAuthType, authInfo, className = '', goBack, origin, title={siteName || authInfo.id} > ('Search account')} @@ -271,6 +259,10 @@ const ManageWebsiteAccessDetail = styled(WrapperComponent)(({ theme: { to paddingTop: token.paddingSM }, + '.account-proxy-item': { + marginBottom: token.marginXS + }, + '&.action-modal': { '.__action-item.block .ant-setting-item-name': { color: token.colorError diff --git a/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/index.tsx b/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/index.tsx index bb4c4bc62f9..d201826f5fe 100644 --- a/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Settings/Security/ManageWebsiteAccess/index.tsx @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { AuthUrlInfo } from '@subwallet/extension-base/background/handlers/State'; +import { AccountProxy } from '@subwallet/extension-base/background/types'; +import { isAccountAll } from '@subwallet/extension-base/utils'; import { ActionItemType, ActionModal, EmptyList, FilterModal, PageWrapper, WebsiteAccessItem } from '@subwallet/extension-koni-ui/components'; import { useDefaultNavigate, useFilterModal } from '@subwallet/extension-koni-ui/hooks'; import { changeAuthorizationAll, forgetAllSite } from '@subwallet/extension-koni-ui/messaging'; @@ -16,26 +18,26 @@ import { useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import styled, { useTheme } from 'styled-components'; -import { isEthereumAddress } from '@polkadot/util-crypto'; - type Props = ThemeProps; function getWebsiteItems (authUrlMap: Record): AuthUrlInfo[] { return Object.values(authUrlMap); } -function getAccountCount (item: AuthUrlInfo): number { - const authType = item.accountAuthType; +function getAccountCount (item: AuthUrlInfo, accountProxies: AccountProxy[]): number { + let result = 0; - if (authType === 'evm') { - return item.isAllowedMap ? Object.entries(item.isAllowedMap).filter(([address, rs]) => rs && isEthereumAddress(address)).length : 0; - } + accountProxies.forEach((ap) => { + if (isAccountAll(ap.proxyId) || ap.isReadOnly) { + return; + } - if (authType === 'substrate') { - return item.isAllowedMap ? Object.entries(item.isAllowedMap).filter(([address, rs]) => rs && !isEthereumAddress(address)).length : 0; - } + if (ap.accounts.some((a) => item.isAllowedMap[a.address])) { + ++result; + } + }); - return Object.values(item.isAllowedMap).filter((i) => i).length; + return result; } const ACTION_MODAL_ID = 'actionModalId'; @@ -43,6 +45,7 @@ const FILTER_MODAL_ID = 'manage-website-access-filter-id'; enum FilterValue { SUBSTRATE = 'substrate', + BITCOIN = 'bitcoin', ETHEREUM = 'ethereum', BLOCKED = 'blocked', Connected = 'connected', @@ -50,6 +53,7 @@ enum FilterValue { function Component ({ className = '' }: Props): React.ReactElement { const authUrlMap = useSelector((state: RootState) => state.settings.authUrls); + const accountProxies = useSelector((state: RootState) => state.accountState.accountProxies); const { activeModal, inactiveModal } = useContext(ModalContext); const { t } = useTranslation(); const navigate = useNavigate(); @@ -63,8 +67,8 @@ function Component ({ className = '' }: Props): React.ReactElement { } for (const filter of selectedFilters) { - if (filter === FilterValue.SUBSTRATE) { - if (item.accountAuthType === 'substrate' || item.accountAuthType === 'both') { + if (filter === FilterValue.BITCOIN) { + if (item.accountAuthType === 'bitcoin' || item.accountAuthType === 'both') { return true; } } else if (filter === FilterValue.ETHEREUM) { @@ -92,7 +96,7 @@ function Component ({ className = '' }: Props): React.ReactElement { const filterOptions = useMemo(() => { return [ - { label: t('Substrate dApp'), value: FilterValue.SUBSTRATE }, + { label: t('Bitcoin dApp'), value: FilterValue.BITCOIN }, { label: t('Ethereum dApp'), value: FilterValue.ETHEREUM }, { label: t('Blocked dApp'), value: FilterValue.BLOCKED }, { label: t('Connected dApp'), value: FilterValue.Connected } @@ -100,8 +104,6 @@ function Component ({ className = '' }: Props): React.ReactElement { }, [t]); const websiteAccessItems = useMemo(() => { - console.log('authUrlMap', authUrlMap); - return getWebsiteItems(authUrlMap); }, [authUrlMap]); @@ -162,7 +164,7 @@ function Component ({ className = '' }: Props): React.ReactElement { (item: AuthUrlInfo) => { return ( { /> ); }, - [onClickItem] + [accountProxies, onClickItem] ); const renderEmptyList = useCallback(() => { 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 63ee1ba5ff8..941d5bd6d44 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 { BN_ZERO, detectTranslate } from '@subwallet/extension-base/utils'; import { AccountSelector, AddressInput, AlertBox, AlertModal, AmountInput, BitcoinFeeSelector, HiddenInput, TokenItemType, TokenSelector } from '@subwallet/extension-koni-ui/components'; import { BITCOIN_CHAINS } from '@subwallet/extension-koni-ui/constants'; import { useAlert, useFetchChainAssetInfo, useGetChainPrefixBySlug, useHandleSubmitTransaction, useInitValidateTransaction, useNotification, usePreCheckAction, useRestoreTransaction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; -import { cancelSubscription, makeCrossChainTransfer, makeTransfer, subscribeMaxTransfer } from '@subwallet/extension-koni-ui/messaging'; +import { cancelSubscription, getBitcoinTransactionData, makeCrossChainTransfer, makeTransfer, subscribeMaxTransfer } from '@subwallet/extension-koni-ui/messaging'; import { FreeBalance } from '@subwallet/extension-koni-ui/Popup/Transaction/parts'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { FormCallbacks, Theme, ThemeProps, TransferParams } from '@subwallet/extension-koni-ui/types'; @@ -622,6 +622,25 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { return accounts.filter(accountsFilter); }, [accounts, accountsFilter]); + const getTransactionData = useCallback(() => { + const { asset, chain, from, to, value } = form.getFieldsValue(); + + getBitcoinTransactionData({ + from, + chain, + to: to, + tokenSlug: asset, + value: value, + transferAll: false, + feeOption: transactionFeeInfo?.feeOption, + feeCustom: transactionFeeInfo?.feeCustom + }).then((rs) => { + console.log('getBitcoinTransactionData', rs); + }).catch((e) => { + console.log('getBitcoinTransactionData E', e); + }); + }, [form, transactionFeeInfo?.feeCustom, transactionFeeInfo?.feeOption]); + useEffect(() => { const bnTransferAmount = new BigN(transferAmountValue || '0'); const bnMaxTransfer = new BigN(transferInfo?.maxTransferable || '0'); @@ -813,6 +832,18 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { + { + BITCOIN_CHAINS.includes(chainValue) && ( + + ) + } +