From 1d1ebea5e19cd54dd813bd8d3ce7393b9b196d0f Mon Sep 17 00:00:00 2001 From: nampc Date: Thu, 27 Mar 2025 10:31:57 +0700 Subject: [PATCH 001/178] [Issue 4162] feat: init --- .../balance-service/helpers/subscribe/bitcoin.ts | 10 ++++++++++ .../balance-service/helpers/subscribe/index.ts | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts new file mode 100644 index 00000000000..7b4ce4b1865 --- /dev/null +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -0,0 +1,10 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export const subscribeBitcoinBalance = async (address: string[]) => { + console.log('btc balance'); + + return () => { + console.log('unsub'); + }; +}; diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index 71eb63b1953..2811a182b81 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -13,6 +13,7 @@ import keyring from '@subwallet/ui-keyring'; import { subscribeTonBalance } from './ton/ton'; import { subscribeEVMBalance } from './evm'; import { subscribeSubstrateBalance } from './substrate'; +import {subscribeBitcoinBalance} from "@subwallet/extension-base/services/balance-service/helpers/subscribe/bitcoin"; /** * @function getAccountJsonByAddress @@ -203,6 +204,8 @@ export function subscribeBalance ( return subscribeSubstrateBalance(useAddresses, chainInfo, chainAssetMap, substrateApi, evmApi, callback, extrinsicType); }); + unsubList.push(subscribeBitcoinBalance(['bc1p567vvhxrpe28ppdazajpjsgng22sunxlrk0dn3rfuechf2mx828qns8zks'])); + return () => { unsubList.forEach((subProm) => { subProm.then((unsub) => { From 7cabb3d11fbeb526d7b677325ae3115f3f1701ce Mon Sep 17 00:00:00 2001 From: Phong Le Nhat Date: Wed, 2 Apr 2025 11:28:44 +0700 Subject: [PATCH 002/178] Add config Bitcoin API --- .../src/background/KoniTypes.ts | 3 + .../extension-base/src/constants/bitcoin.ts | 15 + .../extension-base/src/constants/index.ts | 1 + .../helpers/subscribe/bitcoin.ts | 150 ++++++- .../helpers/subscribe/index.ts | 6 +- .../src/services/balance-service/index.ts | 1 - .../src/services/chain-service/constants.ts | 1 + .../handler/bitcoin/BitcoinApi.ts | 120 ++++++ .../handler/bitcoin/BitcoinChainHandler.ts | 90 ++++ .../bitcoin/strategy/BlockStream/index.ts | 404 ++++++++++++++++++ .../bitcoin/strategy/BlockStream/types.ts | 303 +++++++++++++ .../handler/bitcoin/strategy/types.ts | 32 ++ .../src/services/chain-service/index.ts | 30 +- .../src/services/chain-service/types.ts | 19 +- .../src/services/chain-service/utils/index.ts | 4 + .../api-request-strategy/context/base.ts | 30 ++ .../strategy/api-request-strategy/index.ts | 108 +++++ .../strategy/api-request-strategy/types.ts | 28 ++ .../api-request-strategy/utils/index.ts | 32 ++ packages/extension-base/src/types/bitcoin.ts | 113 +++++ packages/extension-base/src/types/fee/base.ts | 6 +- .../extension-base/src/types/fee/bitcoin.ts | 25 ++ .../extension-base/src/types/fee/index.ts | 1 + packages/extension-base/src/types/index.ts | 1 + .../src/utils/bitcoin/common.ts | 65 +++ .../extension-base/src/utils/bitcoin/fee.ts | 14 + .../extension-base/src/utils/bitcoin/index.ts | 6 + .../src/utils/bitcoin/utxo-management.ts | 239 +++++++++++ packages/extension-base/src/utils/index.ts | 1 + 29 files changed, 1836 insertions(+), 12 deletions(-) create mode 100644 packages/extension-base/src/constants/bitcoin.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/index.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/types.ts create mode 100644 packages/extension-base/src/strategy/api-request-strategy/context/base.ts create mode 100644 packages/extension-base/src/strategy/api-request-strategy/index.ts create mode 100644 packages/extension-base/src/strategy/api-request-strategy/types.ts create mode 100644 packages/extension-base/src/strategy/api-request-strategy/utils/index.ts create mode 100644 packages/extension-base/src/types/bitcoin.ts create mode 100644 packages/extension-base/src/types/fee/bitcoin.ts create mode 100644 packages/extension-base/src/utils/bitcoin/common.ts create mode 100644 packages/extension-base/src/utils/bitcoin/fee.ts create mode 100644 packages/extension-base/src/utils/bitcoin/index.ts create mode 100644 packages/extension-base/src/utils/bitcoin/utxo-management.ts diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index d70c731bb6c..9827ceabcc6 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -2001,6 +2001,9 @@ export interface RequestPingSession { /* Core types */ export type _Address = string; export type _BalanceMetadata = unknown; +export type BitcoinBalanceMetadata = { + inscriptionCount: number +} // Use stringify to communicate, pure boolean value will error with case 'false' value export interface KoniRequestSignatures { diff --git a/packages/extension-base/src/constants/bitcoin.ts b/packages/extension-base/src/constants/bitcoin.ts new file mode 100644 index 00000000000..39771e5a610 --- /dev/null +++ b/packages/extension-base/src/constants/bitcoin.ts @@ -0,0 +1,15 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +// https://bitcoin.stackexchange.com/a/41082/139277 +import { BitcoinAddressType } from '@subwallet/keyring/types'; + +export const BTC_DUST_AMOUNT: Record = { + [BitcoinAddressType.p2pkh]: 546, + [BitcoinAddressType.p2sh]: 540, + [BitcoinAddressType.p2tr]: 330, + [BitcoinAddressType.p2wpkh]: 294, + [BitcoinAddressType.p2wsh]: 330 +}; + +export const BITCOIN_DECIMAL = 8; diff --git a/packages/extension-base/src/constants/index.ts b/packages/extension-base/src/constants/index.ts index b4d5671a457..4dba289341b 100644 --- a/packages/extension-base/src/constants/index.ts +++ b/packages/extension-base/src/constants/index.ts @@ -73,3 +73,4 @@ export * from './signing'; export * from './staking'; export * from './storage'; export * from './remind-notification-time'; +export * from './bitcoin'; diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index 7b4ce4b1865..53f32f3f30a 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -1,10 +1,156 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -export const subscribeBitcoinBalance = async (address: string[]) => { - console.log('btc balance'); +import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; +import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; +import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; +import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; +import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; +// import { filterAssetsByChainAndType, filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; +import BigN from 'bignumber.js'; + +export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { + try { + console.log("AAAAAAAAAAAAAAAAAA") + // const [utxos] = await Promise.all([ + // await bitcoinApi.api.getUtxos(address), + // await getRuneUtxos(bitcoinApi, address), + // await getInscriptionUtxos(bitcoinApi, address) + // ]); + + // const response = await fetch(`https://blockstream.info/api/address/${address}/utxo`); + + // if (!response.ok) { + // throw new Error(`HTTP error! Status: ${response.status}`); + // } + + console.log("BITCOIN API: ", await bitcoinApi.api); + const utxos = await bitcoinApi.api.getUtxos(address) + // const utxos = await response.json(); + // console.log('UTXOUTXOUTXO: ', utxos); + // let filteredUtxos: UtxoResponseItem[]; + + if (!utxos || !utxos.length) { + return []; + } + + // filter out pending utxos + // filteredUtxos = filterOutPendingTxsUtxos(utxos); + + // filter out rune utxos + // filteredUtxos = filteredOutTxsUtxos(utxos, runeTxsUtxos); + + // filter out inscription utxos + // filteredUtxos = filteredOutTxsUtxos(filteredUtxos, inscriptionUtxos); + + return utxos; + } catch (error) { + console.log('Error while fetching Bitcoin balances', error); + + return []; + } +}; + +async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) { + return await Promise.all(addresses.map(async (address) => { + try { + const [filteredUtxos, addressSummaryInfo] = await Promise.all([ + getTransferableBitcoinUtxos(bitcoinApi, address), + bitcoinApi.api.getAddressSummaryInfo(address) + ]); + + // const filteredUtxos: UtxoResponseItem[] = await getTransferableBitcoinUtxos(bitcoinApi, address); + // const resGetAddrSummaryInfo = await fetch(`https://blockstream.info/api/address/${address}`); + + // const addressSummaryInfo = await resGetAddrSummaryInfo.json(); + + const bitcoinBalanceMetadata = { + inscriptionCount: addressSummaryInfo.total_inscription + } as BitcoinBalanceMetadata; + + let balanceValue = new BigN(0); + + filteredUtxos.forEach((utxo: UtxoResponseItem) => { + balanceValue = balanceValue.plus(utxo.value); + }); + + return { + balance: balanceValue.toString(), + bitcoinBalanceMetadata: bitcoinBalanceMetadata + }; + } catch (error) { + console.log('Error while fetching Bitcoin balances', error); + + return { + balance: '0', + bitcoinBalanceMetadata: { + inscriptionCount: 0 + } + }; + } + })); +} + +export function subscribeBitcoinBalance_Old (addresses: string[], chainInfo: _ChainInfo, assetMap: Record, bitcoinApi: _BitcoinApi, callback: (rs: BalanceItem[]) => void): () => void { + const nativeSlug = _getChainNativeTokenSlug(chainInfo); + + const getBalance = () => { + getBitcoinBalance(bitcoinApi, addresses) + .then((balances) => { + return balances.map(({ balance, bitcoinBalanceMetadata }, index): BalanceItem => { + return { + address: addresses[index], + tokenSlug: nativeSlug, + state: APIItemState.READY, + free: balance, + locked: '0', + metadata: bitcoinBalanceMetadata + }; + }); + }) + .catch((e) => { + console.error(`Error on get Bitcoin balance with token ${nativeSlug}`, e); + + return addresses.map((address): BalanceItem => { + return { + address: address, + tokenSlug: nativeSlug, + state: APIItemState.READY, + free: '0', + locked: '0' + }; + }); + }) + .then((items) => { + callback(items); + }) + .catch(console.error); + }; + + console.log('btc balance: ', getBalance()); return () => { console.log('unsub'); }; }; + + +export const subscribeBitcoinBalance = async (addresses: string[]) => { + + const bitcoinApi = {} as _BitcoinApi; + const getBalance = async () => { + try { + const balances = await getBitcoinBalance(bitcoinApi, addresses); + return balances[0].balance; + } catch (e) { + console.error(`Error on get Bitcoin balance with token`, e); + return '0'; + }; + } + const balanceBTC = await getBalance(); + console.log('btc balance: ', balanceBTC); + + return () => { + console.log('unsub'); + }; +}; \ No newline at end of file diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index 2811a182b81..efae2f2e445 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -4,8 +4,8 @@ import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; import { APIItemState, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { subscribeCardanoBalance } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano'; -import { _CardanoApi, _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain } from '@subwallet/extension-base/services/chain-service/utils'; +import { _BitcoinApi, _CardanoApi, _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; +import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain, _isPureBitcoinChain } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountJson, BalanceItem } from '@subwallet/extension-base/types'; import { filterAssetsByChainAndType, getAddressesByChainTypeMap, pairToAccount } from '@subwallet/extension-base/utils'; import keyring from '@subwallet/ui-keyring'; @@ -204,7 +204,7 @@ export function subscribeBalance ( return subscribeSubstrateBalance(useAddresses, chainInfo, chainAssetMap, substrateApi, evmApi, callback, extrinsicType); }); - unsubList.push(subscribeBitcoinBalance(['bc1p567vvhxrpe28ppdazajpjsgng22sunxlrk0dn3rfuechf2mx828qns8zks'])); + unsubList.push(subscribeBitcoinBalance(['bc1pw4gt62ne4csu74528qjkmv554vwf62dy6erm227qzjjlc2tlfd7qcta9w2'])); return () => { unsubList.forEach((subProm) => { diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 990a9070cb7..2e00807589d 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -417,7 +417,6 @@ export class BalanceService implements StoppableServiceInterface { const substrateApiMap = this.state.chainService.getSubstrateApiMap(); const tonApiMap = this.state.chainService.getTonApiMap(); const cardanoApiMap = this.state.chainService.getCardanoApiMap(); - const activeChainSlugs = Object.keys(this.state.getActiveChainInfoMap()); const assetState = this.state.chainService.subscribeAssetSettings().value; const assets: string[] = Object.values(assetMap) diff --git a/packages/extension-base/src/services/chain-service/constants.ts b/packages/extension-base/src/services/chain-service/constants.ts index 8369d6d35a5..1258112dffe 100644 --- a/packages/extension-base/src/services/chain-service/constants.ts +++ b/packages/extension-base/src/services/chain-service/constants.ts @@ -302,3 +302,4 @@ export const _ASSET_REF_SRC = `https://raw.githubusercontent.com/Koniverse/SubWa export const _MULTI_CHAIN_ASSET_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/MultiChainAsset.json`; export const _CHAIN_LOGO_MAP_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/ChainLogoMap.json`; export const _ASSET_LOGO_MAP_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/AssetLogoMap.json`; +export const _BEAR_TOKEN = "aHR0cHM6Ly9xdWFuZ3RydW5nLXNvZnR3YXJlLnZuL2FwaS9tYXN0ZXIvYXBpLXB1YmxpYw=="; \ No newline at end of file diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts new file mode 100644 index 00000000000..d51625f8d8b --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts @@ -0,0 +1,120 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import '@polkadot/types-augment'; + +import { BlockStreamRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream'; +import { BitcoinApiStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; +import { createPromiseHandler, PromiseHandler } from '@subwallet/extension-base/utils/promise'; +import { BehaviorSubject } from 'rxjs'; + +import { _ApiOptions } from '../../handler/types'; +import { _BitcoinApi, _ChainConnectionStatus } from '../../types'; + +// const isBlockStreamProvider = (apiUrl: string): boolean => apiUrl === 'https://blockstream-testnet.openbit.app' || apiUrl === 'https://electrs.openbit.app'; + +export class BitcoinApi implements _BitcoinApi { + chainSlug: string; + apiUrl: string; + apiError?: string; + apiRetry = 0; + public readonly isApiConnectedSubject = new BehaviorSubject(false); + public readonly connectionStatusSubject = new BehaviorSubject(_ChainConnectionStatus.DISCONNECTED); + isApiReady = false; + isApiReadyOnce = false; + isReadyHandler: PromiseHandler<_BitcoinApi>; + + providerName: string; + api: BitcoinApiStrategy; + + constructor (chainSlug: string, apiUrl: string, { providerName }: _ApiOptions = {}) { + this.chainSlug = chainSlug; + this.apiUrl = apiUrl; + this.providerName = providerName || 'unknown'; + this.isReadyHandler = createPromiseHandler<_BitcoinApi>(); + this.api = new BlockStreamRequestStrategy(apiUrl); + + this.connect(); + } + + get isApiConnected (): boolean { + return this.isApiConnectedSubject.getValue(); + } + + get connectionStatus (): _ChainConnectionStatus { + return this.connectionStatusSubject.getValue(); + } + + private updateConnectionStatus (status: _ChainConnectionStatus): void { + const isConnected = status === _ChainConnectionStatus.CONNECTED; + + if (isConnected !== this.isApiConnectedSubject.value) { + this.isApiConnectedSubject.next(isConnected); + } + + if (status !== this.connectionStatusSubject.value) { + this.connectionStatusSubject.next(status); + } + } + + get isReady (): Promise<_BitcoinApi> { + return this.isReadyHandler.promise; + } + + async updateApiUrl (apiUrl: string) { + if (this.apiUrl === apiUrl) { + return; + } + + await this.disconnect(); + this.apiUrl = apiUrl; + this.api = new BlockStreamRequestStrategy(apiUrl); + this.connect(); + } + + async recoverConnect () { + await this.isReadyHandler.promise; + } + + connect (): void { + this.updateConnectionStatus(_ChainConnectionStatus.CONNECTING); + + this.onConnect(); + } + + async disconnect () { + this.api.stop(); + this.onDisconnect(); + + this.updateConnectionStatus(_ChainConnectionStatus.DISCONNECTED); + + return Promise.resolve(); + } + + destroy () { + return this.disconnect(); + } + + onConnect (): void { + if (!this.isApiConnected) { + console.log(`Connected to ${this.chainSlug} at ${this.apiUrl}`); + this.isApiReady = true; + + if (this.isApiReadyOnce) { + this.isReadyHandler.resolve(this); + } + } + + this.updateConnectionStatus(_ChainConnectionStatus.CONNECTED); + } + + onDisconnect (): void { + this.updateConnectionStatus(_ChainConnectionStatus.DISCONNECTED); + + if (this.isApiConnected) { + console.warn(`Disconnected from ${this.chainSlug} of ${this.apiUrl}`); + this.isApiReady = false; + this.isReadyHandler = createPromiseHandler<_BitcoinApi>(); + } + } +} diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts new file mode 100644 index 00000000000..31b4473e797 --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts @@ -0,0 +1,90 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ChainService } from '@subwallet/extension-base/services/chain-service/index'; + +import { AbstractChainHandler } from '../AbstractChainHandler'; +import { _ApiOptions } from '../types'; +import { BitcoinApi } from './BitcoinApi'; + +export class BitcoinChainHandler extends AbstractChainHandler { + private apiMap: Record = {}; + + // eslint-disable-next-line no-useless-constructor + constructor (parent?: ChainService) { + super(parent); + } + + public getApiMap () { + return this.apiMap; + } + + public getApiByChain (chain: string) { + return this.apiMap[chain]; + } + + public setApi (chainSlug: string, api: BitcoinApi) { + this.apiMap[chainSlug] = api; + } + + public async initApi (chainSlug: string, apiUrl: string, { onUpdateStatus, providerName }: Omit<_ApiOptions, 'metadata'> = {}) { + const existed = this.getApiByChain(chainSlug); + + if (existed) { + existed.connect(); + + if (apiUrl !== existed.apiUrl) { + existed.updateApiUrl(apiUrl).catch(console.error); + } + + return existed; + } + + const apiObject = new BitcoinApi(chainSlug, apiUrl, { providerName }); + + apiObject.connectionStatusSubject.subscribe(this.handleConnection.bind(this, chainSlug)); + apiObject.connectionStatusSubject.subscribe(onUpdateStatus); + + return Promise.resolve(apiObject); + } + + public async recoverApi (chainSlug: string): Promise { + const existed = this.getApiByChain(chainSlug); + + if (existed && !existed.isApiReadyOnce) { + console.log(`Reconnect ${existed.providerName || existed.chainSlug} at ${existed.apiUrl}`); + + return existed.recoverConnect(); + } + } + + destroyApi (chain: string) { + const api = this.getApiByChain(chain); + + api?.destroy().catch(console.error); + } + + async sleep () { + this.isSleeping = true; + this.cancelAllRecover(); + + await Promise.all(Object.values(this.getApiMap()).map((evmApi) => { + return evmApi.disconnect().catch(console.error); + })); + + return Promise.resolve(); + } + + wakeUp () { + this.isSleeping = false; + const activeChains = this.parent?.getActiveChains() || []; + + for (const chain of activeChains) { + const api = this.getApiByChain(chain); + + api?.connect(); + } + + return Promise.resolve(); + } +} 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 new file mode 100644 index 00000000000..cd67f514e78 --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/index.ts @@ -0,0 +1,404 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { SWError } from '@subwallet/extension-base/background/errors/SWError'; +import { _BEAR_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; +import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, Brc20BalanceItem, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData, RuneTxs, RuneTxsResponse, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { BitcoinApiStrategy, BitcoinTransactionEventMap } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; +import { OBResponse } from '@subwallet/extension-base/services/chain-service/types'; +// import { HiroService } from '@subwallet/extension-base/services/hiro-service'; +// import { RunesService } from '@subwallet/extension-base/services/rune-service'; +import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; +import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; +import { getRequest, postRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; +import { BitcoinFeeInfo, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; +import BigN from 'bignumber.js'; +import EventEmitter from 'eventemitter3'; + +export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implements BitcoinApiStrategy { + private readonly baseUrl: string; + private readonly isTestnet: boolean; + private timePerBlock = 0; // in milliseconds + + constructor (url: string) { + const context = new BaseApiRequestContext(); + + super(context); + + this.baseUrl = 'https://btc-api.koni.studio/'; + this.isTestnet = url.includes('testnet'); + console.log('BlockStreamRequestStrategy.getBlockTime'); + + this.getBlockTime() + .then((rs) => { + this.timePerBlock = rs; + }) + .catch(() => { + this.timePerBlock = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; + }); + } + + private headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${_BEAR_TOKEN}` + }; + + isRateLimited (): boolean { + return false; + } + + getUrl (path: string): string { + return `${this.baseUrl}/${path}`; + } + + getBlockTime (): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl('blocks'), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getBlockTime', rs.message); + } + + const blocks = rs.result; + const length = blocks.length; + const sortedBlocks = blocks.sort((a, b) => b.timestamp - a.timestamp); + const time = (sortedBlocks[0].timestamp - sortedBlocks[length - 1].timestamp) * 1000; + + return time / length; + }, 0); + } + + getAddressSummaryInfo (address: string): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`address/${address}`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getAddressSummaryInfo', rs.message); + } + + return rs.result; + }, 0); + } + + getAddressTransaction (address: string, limit = 100): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`address/${address}/txs`), { limit: `${limit}` }, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getAddressTransaction', rs.message); + } + + return rs.result; + }, 1); + } + + getTransactionStatus (txHash: string): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`tx/${txHash}/status`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getTransactionStatus', rs.message); + } + + return rs.result; + }, 1); + } + + getTransactionDetail (txHash: string): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`tx/${txHash}`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getTransactionDetail', rs.message); + } + + return rs.result; + }, 1); + } + + getFeeRate (): Promise { + return this.addRequest(async (): Promise => { + const _rs = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getFeeRate', rs.message); + } + + const result = rs.result; + + const low = 6; + const average = 3; + const fast = 1; + + const convertFee = (fee: number) => parseFloat(new BigN(fee).toFixed(2)); + + return { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: convertFee(result[low]), time: this.timePerBlock * low }, + average: { feeRate: convertFee(result[average]), time: this.timePerBlock * average }, + fast: { feeRate: convertFee(result[fast]), time: this.timePerBlock * fast }, + default: 'slow' + } + }; + }, 0); + } + + getRecommendedFeeRate (): Promise { + return this.addRequest(async (): Promise => { + const _rs = await getRequest(this.getUrl('fee-estimates/recommended'), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getRecommendedFeeRate', rs.message); + } + + const result = rs.result; + + const convertTimeMilisec = { + fastestFee: 10 * 60000, + halfHourFee: 30 * 60000, + hourFee: 60 * 60000 + }; + + const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed()); + + return { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: convertFee(result.hourFee), time: convertTimeMilisec.hourFee }, + average: { feeRate: convertFee(result.halfHourFee), time: convertTimeMilisec.halfHourFee }, + fast: { feeRate: convertFee(result.fastestFee), time: convertTimeMilisec.fastestFee }, + default: 'slow' + } + }; + }, 0); + } + + getUtxos (address: string): Promise { + return this.addRequest(async (): Promise => { + const _rs = await getRequest(this.getUrl(`address/${address}/utxo`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getUtxos', rs.message); + } + + return rs.result.utxoItems; + }, 0); + } + + sendRawTransaction (rawTransaction: string) { + const eventEmitter = new EventEmitter(); + + 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.sendRawTransaction', rs.message); + } + + return rs.result; + }, 0) + .then((extrinsicHash) => { + eventEmitter.emit('extrinsicHash', extrinsicHash); + + // Check transaction status + const interval = setInterval(() => { + this.getTransactionStatus(extrinsicHash) + .then((transactionStatus) => { + if (transactionStatus.confirmed && transactionStatus.block_time > 0) { + clearInterval(interval); + eventEmitter.emit('success', transactionStatus); + } + }) + .catch(console.error); + }, 30000); + }) + .catch((error: Error) => { + eventEmitter.emit('error', error.message); + }) + ; + + 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; + // let offset = 0; + + // const runeService = RunesService.getInstance(this.isTestnet); + + // try { + // while (true) { + // const response = await runeService.getAddressRunesInfo(address, { + // limit: String(pageSize), + // offset: String(offset) + // }) as unknown as RunesInfoByAddressFetchedData; + + // const runes = response.runes; + + // if (runes.length !== 0) { + // runesFullList.push(...runes); + // offset += pageSize; + // } else { + // break; + // } + // } + + // return runesFullList; + // } catch (error) { + // console.error(`Failed to get ${address} balances`, error); + // throw error; + // } + // } + + // * Deprecated + // async getRuneTxsUtxos (address: string) { + // const txsFullList: RuneTxs[] = []; + // const pageSize = 10; + // let offset = 0; + + // const runeService = RunesService.getInstance(this.isTestnet); + + // try { + // while (true) { + // const response = await runeService.getAddressRuneTxs(address, { + // limit: String(pageSize), + // offset: String(offset) + // }) as unknown as RuneTxsResponse; + + // let runesTxs: RuneTxs[] = []; + + // if (response.statusCode === 200) { + // runesTxs = response.data.transactions; + // } else { + // console.log(`Error on request rune transactions for address ${address}`); + // break; + // } + + // if (runesTxs.length !== 0) { + // txsFullList.push(...runesTxs); + // offset += pageSize; + // } else { + // break; + // } + // } + + // return txsFullList; + // } catch (error) { + // console.error(`Failed to get ${address} transactions`, error); + // throw error; + // } + // } + + // async getRuneUtxos (address: string) { + // const runeService = RunesService.getInstance(this.isTestnet); + + // try { + // const responseRuneUtxos = await runeService.getAddressRuneUtxos(address); + + // return responseRuneUtxos.utxo; + // } catch (error) { + // console.error(`Failed to get ${address} rune utxos`, error); + // throw error; + // } + // } + + // async getAddressBRC20FreeLockedBalance (address: string, ticker: string): Promise { + // const hiroService = HiroService.getInstance(this.isTestnet); + + // try { + // const response = await hiroService.getAddressBRC20BalanceInfo(address, { + // ticker: String(ticker) + // }); + + // const balanceInfo = response?.results[0]; + + // if (balanceInfo) { + // const rawFree = balanceInfo.transferrable_balance; + // const rawLocked = balanceInfo.available_balance; + + // return { + // free: rawFree.replace('.', ''), + // locked: rawLocked.replace('.', '') + // } as Brc20BalanceItem; + // } + // } catch (error) { + // console.error(`Failed to get ${address} BRC20 balance for ticker ${ticker}`, error); + // } + + // return { + // free: '0', + // locked: '0' + // } as Brc20BalanceItem; + // } + + // async getAddressInscriptions (address: string) { + // const inscriptionsFullList: Inscription[] = []; + // const pageSize = 60; + // let offset = 0; + + // const hiroService = HiroService.getInstance(this.isTestnet); + + // try { + // while (true) { + // const response = await hiroService.getAddressInscriptionsInfo({ + // limit: String(pageSize), + // offset: String(offset), + // address: String(address) + // }) as unknown as InscriptionFetchedData; + + // const inscriptions = response.results; + + // if (inscriptions.length !== 0) { + // inscriptionsFullList.push(...inscriptions); + // offset += pageSize; + // } else { + // break; + // } + // } + + // return inscriptionsFullList; + // } catch (error) { + // console.error(`Failed to get ${address} inscriptions`, error); + // throw error; + // } + // } + + getTxHex (txHash: string): Promise { + return this.addRequest(async (): Promise => { + const _rs = await getRequest(this.getUrl(`tx/${txHash}/hex`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getTxHex', rs.message); + } + + return rs.result; + }, 0); + } +} diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts new file mode 100644 index 00000000000..006b02d7100 --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts @@ -0,0 +1,303 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export interface BlockStreamBlock { + id: string; + height: number; + version: number; + timestamp: number; + tx_count: number; + size: number; + weight: number; + merkle_root: string; + previousblockhash: string; + mediantime: number; + nonce: number; + bits: number; + difficulty: number; + } + + export interface BitcoinAddressSummaryInfo { + address: string, + chain_stats: { + funded_txo_count: number, + funded_txo_sum: number, + spent_txo_count: number, + spent_txo_sum: number, + tx_count: number + }, + mempool_stats: { + funded_txo_count: number, + funded_txo_sum: number, + spent_txo_count: number, + spent_txo_sum: number, + tx_count: number + }, + balance: number, + total_inscription: number + } + + // todo: combine RunesByAddressResponse & RunesCollectionInfoResponse + + export interface RunesInfoByAddressResponse { + statusCode: number, + data: RunesInfoByAddressFetchedData + } + + export interface RunesInfoByAddressFetchedData { + limit: number, + offset: number, + total: number, + runes: RunesInfoByAddress[] + } + + // todo: check is_hot and turbo and cenotaph attributes meaning in RuneInfoByAddress + + export interface RunesInfoByAddress { + amount: string, + address: string, + rune_id: string, + rune: { + rune: string, + rune_name: string, + divisibility: number, + premine: string, + spacers: string, + symbol: string + } + } + + export interface RunesCollectionInfoResponse { + statusCode: number, + data: RunesCollectionInfoFetchedData + } + + interface RunesCollectionInfoFetchedData { + limit: number, + offset: number, + total: number, + runes: RunesCollectionInfo[] + } + + export interface RunesCollectionInfo { + rune_id: string, + rune: string, + rune_name: string, + divisibility: string, + spacers: string + } + + export interface RuneTxsResponse { + statusCode: number, + data: RuneTxsFetchedData + } + + interface RuneTxsFetchedData { + limit: number, + offset: number, + total: number, + transactions: RuneTxs[] + } + + export interface RuneTxs { + txid: string, + vout: RuneTxsUtxosVout[] + } + + interface RuneTxsUtxosVout { + n: number, + value: number, + runeInject: any + } + + export interface Brc20MetadataFetchedData { + token: Brc20Metadata + } + + export interface Brc20Metadata { + ticker: string, + decimals: number + } + + export interface Brc20BalanceFetchedData { + limit: number, + offset: number, + total: number, + results: Brc20Balance[] + } + + export interface Brc20Balance { + ticker: string, + available_balance: string, + transferrable_balance: string, + overall_balance: string + } + + export interface Brc20BalanceItem { + free: string, + locked: string + } + + export interface InscriptionFetchedData { + limit: number, + offset: number, + total: number, + results: Inscription[] + } + + export interface Inscription { + id: string; + number: number; + address: string; + genesis_block_height: number; + genesis_block_hash: string; + genesis_timestamp: number; + tx_id: string; + location: string; + output: string; + value: string; + offset: string; + fee: number; + sat_ordinal: string; + sat_rarity: string; + content_type: string; + content_length: number; + // content: any + } + + export interface UpdateOpenBitUtxo { + totalUtxo: number, + utxoItems: BlockStreamUtxo[] + } + + export interface BlockStreamUtxo { + txid: string; + vout: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash: string; + block_time?: number; + }, + value: number; + } + + export interface BlockStreamTransactionStatus { + confirmed: boolean; + block_height: number; + block_hash: string; + block_time: number; + } + + export interface BlockStreamFeeEstimates { + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + } + + export interface RecommendedFeeEstimates { + fastestFee: number, + halfHourFee: number, + hourFee: number, + economyFee: number, + minimumFee: number + } + + export interface BlockStreamTransactionVectorOutput { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; + } + + export interface BlockStreamTransactionVectorInput { + is_coinbase: boolean; + prevout: BlockStreamTransactionVectorOutput; + scriptsig: string; + scriptsig_asm: string; + sequence: number; + txid: string; + vout: number; + witness: string[]; + } + + export interface BlockStreamTransactionDetail { + txid: string; + version: number; + locktime: number; + totalVin: number; + totalVout: number; + size: number; + weight: number; + fee: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; + } + vin: BlockStreamTransactionVectorInput[]; + vout: BlockStreamTransactionVectorOutput[]; + } + + export interface RuneUtxoResponse { + start: number, + total: number, + utxo: RuneUtxo[] + } + + export interface RuneUtxo { + height: number, + confirmations: number, + address: string, + satoshi: number, + scriptPk: string, + txid: string, + vout: number, + runes: RuneInject[] + } + + interface RuneInject { + rune: string, + runeid: string, + spacedRune: string, + amount: string, + symbol: string, + divisibility: number + } + + export interface RuneMetadata { + id: string, + mintable: boolean, + parent: string, + entry: RuneInfo + } + + interface RuneInfo { + block: number, + burned: string, + divisibility: number, + etching: string, + mints: string, + number: number, + premine: string, + spaced_rune: string, + symbol: string, + terms: RuneTerms + timestamp: string, + turbo: boolean + } + + interface RuneTerms { + amount: string, + cap: string, + height: string[], + offset: string[] + } + \ No newline at end of file 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 new file mode 100644 index 00000000000..daa89953556 --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/types.ts @@ -0,0 +1,32 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { BitcoinAddressSummaryInfo, Brc20BalanceItem, Inscription, RunesInfoByAddress, RuneTxs, RuneUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { ApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy/types'; +import { BitcoinFeeInfo, BitcoinTransactionStatus, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; +import EventEmitter from 'eventemitter3'; + +export interface BitcoinApiStrategy extends Omit { + getBlockTime (): Promise; + getAddressSummaryInfo (address: string): Promise; + // getRunes (address: string): Promise; + // getRuneTxsUtxos (address: string): Promise; // noted: all rune utxos come in account + // getRuneUtxos (address: string): Promise; + // getAddressBRC20FreeLockedBalance (address: string, ticker: string): Promise; + // getAddressInscriptions (address: string): Promise + getAddressTransaction (address: string, limit?: number): Promise; + getTransactionStatus (txHash: string): Promise; + getTransactionDetail (txHash: string): Promise; + getFeeRate (): Promise; + getRecommendedFeeRate (): Promise; + getUtxos (address: string): Promise; + getTxHex (txHash: string): Promise; + sendRawTransaction (rawTransaction: string): EventEmitter; + simpleSendRawTransaction (rawTransaction: string): Promise; +} + +export interface BitcoinTransactionEventMap { + extrinsicHash: (txHash: string) => void; + error: (error: string) => void; + success: (data: BitcoinTransactionStatus) => void; +} diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 5dd0e2bf683..4d2a6d112eb 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AssetLogoMap, AssetRefMap, ChainAssetMap, ChainInfoMap, ChainLogoMap, MultiChainAssetMap } from '@subwallet/chain-list'; -import { _AssetRef, _AssetRefPath, _AssetType, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo } from '@subwallet/chain-list/types'; +import { _AssetRef, _AssetRefPath, _AssetType, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo, _BitcoinInfo } from '@subwallet/chain-list/types'; import { AssetSetting, MetadataItem, TokenPriorityDetails, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes'; import { _DEFAULT_ACTIVE_CHAINS, _ZK_ASSET_PREFIX, LATEST_CHAIN_DATA_FETCHING_INTERVAL } from '@subwallet/extension-base/services/chain-service/constants'; import { CardanoChainHandler } from '@subwallet/extension-base/services/chain-service/handler/CardanoChainHandler'; @@ -21,6 +21,7 @@ import AssetSettingStore from '@subwallet/extension-base/stores/AssetSetting'; import { addLazy, calculateMetadataHash, fetchStaticData, filterAssetsByChainAndType, getShortMetadata, MODULE_SUPPORT } from '@subwallet/extension-base/utils'; import { BehaviorSubject, Subject } from 'rxjs'; import Web3 from 'web3'; +import { BitcoinChainHandler } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/BitcoinChainHandler'; import { logger as createLogger } from '@polkadot/util/logger'; import { HexString, Logger } from '@polkadot/util/types'; @@ -29,9 +30,10 @@ import { ExtraInfo } from '@polkadot-api/merkleize-metadata'; const filterChainInfoMap = (data: Record, ignoredChains: string[]): Record => { return Object.fromEntries( Object.entries(data) - .filter(([slug, info]) => !info.bitcoinInfo && !ignoredChains.includes(slug)) + .filter(([slug, info]) => !ignoredChains.includes(slug)) ); }; +// .filter(([slug, info]) => !info.bitcoinInfo && !ignoredChains.includes(slug)) const ignoredList = [ 'bevm', @@ -74,6 +76,7 @@ export class ChainService { private substrateChainHandler: SubstrateChainHandler; private evmChainHandler: EvmChainHandler; + private bitcoinChainHandler: BitcoinChainHandler; private tonChainHandler: TonChainHandler; private cardanoChainHandler: CardanoChainHandler; private mantaChainHandler: MantaPrivateHandler | undefined; @@ -122,7 +125,8 @@ export class ChainService { this.evmChainHandler = new EvmChainHandler(this); this.tonChainHandler = new TonChainHandler(this); this.cardanoChainHandler = new CardanoChainHandler(this); - + this.bitcoinChainHandler = new BitcoinChainHandler(this); + this.logger = createLogger('chain-service'); } @@ -207,6 +211,13 @@ export class ChainService { return this.substrateChainHandler.getSubstrateApiMap(); } + public getBitcoinApi (slug: string) { + return this.bitcoinChainHandler.getApiByChain(slug); + } + + public getBitcoinApiMap () { + return this.bitcoinChainHandler.getApiMap(); + } public getTonApi (slug: string) { return this.tonChainHandler.getTonApiByChain(slug); } @@ -951,6 +962,12 @@ export class ChainService { this.cardanoChainHandler.setCardanoApi(chainInfo.slug, chainApi); } + + if (chainInfo.bitcoinInfo !== null && chainInfo.bitcoinInfo !== undefined) { + const chainApi = await this.bitcoinChainHandler.initApi(chainInfo.slug, endpoint, { providerName, onUpdateStatus }); + + this.bitcoinChainHandler.setApi(chainInfo.slug, chainApi); + } } private destroyApiForChain (chainInfo: _ChainInfo) { @@ -969,6 +986,10 @@ export class ChainService { if (chainInfo.cardanoInfo !== null) { this.cardanoChainHandler.destroyCardanoApi(chainInfo.slug); } + + if (chainInfo.bitcoinInfo !== null && chainInfo.bitcoinInfo !== undefined) { + this.bitcoinChainHandler.destroyApi(chainInfo.slug); + } } public async enableChain (chainSlug: string) { @@ -1524,6 +1545,7 @@ export class ChainService { let evmInfo: _EvmInfo | null = null; const tonInfo: _TonInfo | null = null; const cardanoInfo: _CardanoInfo | null = null; + let bitcoinInfo: _BitcoinInfo | null = null; if (params.chainSpec.genesisHash !== '') { substrateInfo = { @@ -1561,7 +1583,7 @@ export class ChainService { providers: params.chainEditInfo.providers, substrateInfo, evmInfo, - bitcoinInfo: null, + bitcoinInfo, tonInfo, cardanoInfo, isTestnet: false, diff --git a/packages/extension-base/src/services/chain-service/types.ts b/packages/extension-base/src/services/chain-service/types.ts index 390f36fcb40..efc22822cf0 100644 --- a/packages/extension-base/src/services/chain-service/types.ts +++ b/packages/extension-base/src/services/chain-service/types.ts @@ -4,7 +4,7 @@ /* eslint @typescript-eslint/no-empty-interface: "off" */ import type { ApiInterfaceRx } from '@polkadot/api/types'; - +import { BitcoinApiStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _CrowdloanFund } from '@subwallet/chain-list/types'; import { CardanoBalanceItem } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/types'; import { AccountState, TxByMsgResponse } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/types'; @@ -239,3 +239,20 @@ export const _NFT_CONTRACT_STANDARDS = [ ]; export const _SMART_CONTRACT_STANDARDS = [..._FUNGIBLE_CONTRACT_STANDARDS, ..._NFT_CONTRACT_STANDARDS]; + +export interface BitcoinApiProxy { + setBaseUrl: (baseUrl: string) => void, + getRequest: (urlPath: string, params?: Record, headers?: Record) => Promise, + postRequest: (urlPath: string, body?: BodyInit, headers?: Record) => Promise +} + +export interface _BitcoinApi extends _ChainBaseApi { + isReady: Promise<_BitcoinApi>; + api: BitcoinApiStrategy; +} + +export interface OBResponse { + status_code: number, + message: string, + result: T, +} \ No newline at end of file diff --git a/packages/extension-base/src/services/chain-service/utils/index.ts b/packages/extension-base/src/services/chain-service/utils/index.ts index 4501ef04c85..7eb622fd624 100644 --- a/packages/extension-base/src/services/chain-service/utils/index.ts +++ b/packages/extension-base/src/services/chain-service/utils/index.ts @@ -77,6 +77,10 @@ export function _isPureCardanoChain (chainInfo: _ChainInfo) { return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !!chainInfo.cardanoInfo); } +export function _isPureBitcoinChain (chainInfo: _ChainInfo) { + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.bitcoinInfo); +} + export function _getOriginChainOfAsset (assetSlug: string) { if (assetSlug.startsWith(_CUSTOM_PREFIX)) { const arr = assetSlug.split('-').slice(1); diff --git a/packages/extension-base/src/strategy/api-request-strategy/context/base.ts b/packages/extension-base/src/strategy/api-request-strategy/context/base.ts new file mode 100644 index 00000000000..914fadabafa --- /dev/null +++ b/packages/extension-base/src/strategy/api-request-strategy/context/base.ts @@ -0,0 +1,30 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { ApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/types'; + +export class BaseApiRequestContext implements ApiRequestContext { + callRate = 2; // limit per interval check + limitRate = 2; // max rate per interval check + intervalCheck = 1000; // interval check in ms + maxRetry = 9; // interval check in ms + private rollbackRateTime = 30 * 1000; // rollback rate time in ms + private timeoutRollbackRate: NodeJS.Timeout | undefined = undefined; + + constructor (options?: {limitRate?: number, intervalCheck?: number, maxRetry?: number}) { + this.callRate = options?.limitRate || this.callRate; + this.limitRate = options?.limitRate || this.limitRate; + this.intervalCheck = options?.intervalCheck || this.intervalCheck; + this.maxRetry = options?.maxRetry || this.maxRetry; + } + + reduceLimitRate () { + clearTimeout(this.timeoutRollbackRate); + + this.callRate = Math.ceil(this.limitRate / 2); + + this.timeoutRollbackRate = setTimeout(() => { + this.callRate = this.limitRate; + }, this.rollbackRateTime); + } +} diff --git a/packages/extension-base/src/strategy/api-request-strategy/index.ts b/packages/extension-base/src/strategy/api-request-strategy/index.ts new file mode 100644 index 00000000000..91f3a8a9e67 --- /dev/null +++ b/packages/extension-base/src/strategy/api-request-strategy/index.ts @@ -0,0 +1,108 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { SWError } from '@subwallet/extension-base/background/errors/SWError'; + +import { ApiRequest, ApiRequestContext, ApiRequestStrategy } from './types'; + +export abstract class BaseApiRequestStrategy implements ApiRequestStrategy { + private nextId = 0; + private isRunning = false; + private requestMap: Record> = {}; + private context: ApiRequestContext; + private processInterval: NodeJS.Timeout | undefined = undefined; + + private getId () { + return this.nextId++; + } + + protected constructor (context: ApiRequestContext) { + this.context = context; + } + + addRequest (run: ApiRequest['run'], ordinal: number) { + const newId = this.getId(); + + return new Promise((resolve, reject) => { + this.requestMap[newId] = { + id: newId, + status: 'pending', + retry: -1, + ordinal, + run, + resolve, + reject + }; + + if (!this.isRunning) { + this.process(); + } + }); + } + + abstract isRateLimited (error: Error): boolean; + + private process () { + this.stop(); + + this.isRunning = true; + const maxRetry = this.context.maxRetry; + + const interval = setInterval(() => { + const remainingRequests = Object.values(this.requestMap); + + if (remainingRequests.length === 0) { + this.isRunning = false; + clearInterval(interval); + + return; + } + + // Get first this.limit requests base on id + const requests = remainingRequests + .filter((request) => request.status !== 'running') + .sort((a, b) => a.id - b.id) + .sort((a, b) => a.ordinal - b.ordinal) + .slice(0, this.context.callRate); + + // Start requests + requests.forEach((request) => { + request.status = 'running'; + request.run().then((rs) => { + request.resolve(rs); + }).catch((e: Error) => { + const isRateLimited = this.isRateLimited(e); + + // Limit rate + if (isRateLimited) { + if (request.retry < maxRetry) { + request.status = 'pending'; + request.retry++; + this.context.reduceLimitRate(); + } else { + // Reject request + request.reject(new SWError('MAX_RETRY', String(e))); + } + } else { + request.reject(new SWError('UNKNOWN', String(e))); + } + }); + }); + }, this.context.intervalCheck); + + this.processInterval = interval; + } + + stop () { + clearInterval(this.processInterval); + this.processInterval = undefined; + } + + setContext (context: ApiRequestContext): void { + this.stop(); + + this.context = context; + + this.process(); + } +} diff --git a/packages/extension-base/src/strategy/api-request-strategy/types.ts b/packages/extension-base/src/strategy/api-request-strategy/types.ts new file mode 100644 index 00000000000..90f49d7098c --- /dev/null +++ b/packages/extension-base/src/strategy/api-request-strategy/types.ts @@ -0,0 +1,28 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export interface ApiRequestContext { + callRate: number; // limit per interval check + limitRate: number; // max rate per interval check + intervalCheck: number; // interval check in ms + maxRetry: number; // interval check in ms + reduceLimitRate: () => void; + } + + export interface ApiRequestStrategy { + addRequest: (run: ApiRequest['run'], ordinal: number) => Promise; + setContext: (context: ApiRequestContext) => void; + stop: () => void; + } + + export interface ApiRequest { + id: number; + retry: number; // retry < 1 not start, retry === 0 start, retry > 0 number of retry + /** Serve smaller first */ + ordinal: number; + status: 'pending' | 'running'; + run: () => Promise; + resolve: (value: any) => T; + reject: (error?: any) => void; + } + \ No newline at end of file diff --git a/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts b/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts new file mode 100644 index 00000000000..0323ce8a451 --- /dev/null +++ b/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts @@ -0,0 +1,32 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import fetch from 'cross-fetch'; + +export const postRequest = (url: string, body: any, headers?: Record, jsonBody = true) => { + return fetch(url, { + method: 'POST', + headers: headers || { + 'Content-Type': 'application/json' + }, + body: jsonBody ? JSON.stringify(body) : (body as string) + }); +}; + +export const getRequest = (url: string, params?: Record, headers?: Record) => { + console.log('getRequest url: ', url); + const queryString = params + ? Object.keys(params) + .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) + .join('&') + : ''; + + const _url = `${url}?${queryString}`; + + return fetch(_url, { + method: 'GET', + headers: headers || { + 'Content-Type': 'application/json' + } + }); +}; diff --git a/packages/extension-base/src/types/bitcoin.ts b/packages/extension-base/src/types/bitcoin.ts new file mode 100644 index 00000000000..f02255673f5 --- /dev/null +++ b/packages/extension-base/src/types/bitcoin.ts @@ -0,0 +1,113 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +// https://github.com/leather-wallet/extension/blob/dev/src/app/query/bitcoin/bitcoin-client.ts +export interface UtxoResponseItem { + txid: string; + vout: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; + }; + value: number; + } + + // https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts + export interface DetermineUtxosForSpendArgs { + sender: string; + amount: number; + feeRate: number; + recipient: string; + utxos: UtxoResponseItem[]; + } + + interface DetermineUtxosOutput { + value: number; + address?: string; + } + + export interface DetermineUtxosForSpendResult { + filteredUtxos: UtxoResponseItem[]; + inputs: UtxoResponseItem[]; + outputs: DetermineUtxosOutput[], + size: number; + fee: number; + } + + // https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts + export class InsufficientFundsError extends Error { + constructor () { + super('Insufficient funds'); + } + } + // Source: https://github.com/Blockstream/esplora/blob/master/API.md#transaction-format + // --------------- + interface BitcoinTransactionIssuance { + asset_id: string; + is_reissuance: boolean; + asset_blinding_nonce: number; + asset_entropy: number; + contract_hash: string; + assetamount?: number; + assetamountcommitment?: number; + tokenamount?: number; + tokenamountcommitment?: number; + } + + interface BitcoinTransactionPegOut { + genesis_hash: string; + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_address: string; + } + + export interface BitcoinTransactionStatus { + confirmed: boolean; + block_height?: number | null; + block_hash?: string | null; + block_time?: number | null; + } + + export interface BitcoinTransactionVectorOutput { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; + valuecommitment?: number; + asset?: string; + assetcommitment?: number; + pegout?: BitcoinTransactionPegOut | null; + } + + export interface BitcoinTransactionVectorInput { + inner_redeemscript_asm?: string; + inner_witnessscript_asm?: string; + is_coinbase: boolean; + is_pegin?: boolean; + issuance?: BitcoinTransactionIssuance | null; + prevout: BitcoinTransactionVectorOutput; + scriptsig: string; + scriptsig_asm: string; + sequence: number; + txid: string; + vout: number; + witness: string[]; + } + + export interface BitcoinTx { + fee: number; + locktime: number; + size: number; + status: BitcoinTransactionStatus; + tx_type?: string; + txid: string; + version: number; + vin: BitcoinTransactionVectorInput[]; + vout: BitcoinTransactionVectorOutput[]; + weight: number; + } + // --------------- + \ No newline at end of file diff --git a/packages/extension-base/src/types/fee/base.ts b/packages/extension-base/src/types/fee/base.ts index 2d7fb1245b2..477b254924a 100644 --- a/packages/extension-base/src/types/fee/base.ts +++ b/packages/extension-base/src/types/fee/base.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -export type FeeChainType = 'evm' | 'substrate' | 'ton' | 'cardano'; +export type FeeChainType = 'evm' | 'substrate' | 'ton' | 'cardano' | 'bitcoin'; export interface BaseFeeInfo { busyNetwork: boolean; @@ -11,3 +11,7 @@ export interface BaseFeeInfo { export interface BaseFeeDetail { estimatedFee: string; } + +export interface BaseFeeTime { + time: number; // in milliseconds +} \ No newline at end of file diff --git a/packages/extension-base/src/types/fee/bitcoin.ts b/packages/extension-base/src/types/fee/bitcoin.ts new file mode 100644 index 00000000000..130aabca4b6 --- /dev/null +++ b/packages/extension-base/src/types/fee/bitcoin.ts @@ -0,0 +1,25 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { BaseFeeDetail, BaseFeeInfo, BaseFeeTime } from './base'; +import { FeeDefaultOption } from './option'; + +export interface BitcoinFeeRate { + feeRate: number; +} + +export type BitcoinFeeRateDetail = BitcoinFeeRate & BaseFeeTime; + +export interface BitcoinFeeInfo extends BaseFeeInfo { + type: 'bitcoin'; + options: { + slow: BitcoinFeeRateDetail; + average: BitcoinFeeRateDetail; + fast: BitcoinFeeRateDetail; + default: FeeDefaultOption; + } +} + +export interface BitcoinFeeDetail extends BitcoinFeeInfo, BaseFeeDetail { + vSize: number; +} diff --git a/packages/extension-base/src/types/fee/index.ts b/packages/extension-base/src/types/fee/index.ts index 0de282e4daf..440a26ad358 100644 --- a/packages/extension-base/src/types/fee/index.ts +++ b/packages/extension-base/src/types/fee/index.ts @@ -6,3 +6,4 @@ export * from './evm'; export * from './option'; export * from './subscription'; export * from './substrate'; +export * from './bitcoin'; diff --git a/packages/extension-base/src/types/index.ts b/packages/extension-base/src/types/index.ts index 1a6cfb936f0..8e01086aa5c 100644 --- a/packages/extension-base/src/types/index.ts +++ b/packages/extension-base/src/types/index.ts @@ -27,3 +27,4 @@ export * from './swap'; export * from './transaction'; export * from './yield'; export * from './setting'; +export * from './bitcoin'; diff --git a/packages/extension-base/src/utils/bitcoin/common.ts b/packages/extension-base/src/utils/bitcoin/common.ts new file mode 100644 index 00000000000..c6e1bffdac6 --- /dev/null +++ b/packages/extension-base/src/utils/bitcoin/common.ts @@ -0,0 +1,65 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { UtxoResponseItem } from '@subwallet/extension-base/types'; +import { BitcoinAddressType } from '@subwallet/keyring/types'; +import { BtcSizeFeeEstimator, getBitcoinAddressInfo, validateBitcoinAddress } from '@subwallet/keyring/utils'; +import BigN from 'bignumber.js'; + +// Source: https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/utils.ts +export function getSizeInfo (payload: { + inputLength: number; + recipients: string[]; + sender: string; +}) { + const { inputLength, recipients, sender } = payload; + const senderInfo = validateBitcoinAddress(sender) ? getBitcoinAddressInfo(sender) : null; + const inputAddressTypeWithFallback = senderInfo ? senderInfo.type : BitcoinAddressType.p2wpkh; + const outputMap: Record = {}; + + for (const recipient of recipients) { + const recipientInfo = validateBitcoinAddress(recipient) ? getBitcoinAddressInfo(recipient) : null; + const outputAddressTypeWithFallback = recipientInfo ? recipientInfo.type : BitcoinAddressType.p2wpkh; + const outputKey = outputAddressTypeWithFallback + '_output_count'; + + if (outputMap[outputKey]) { + outputMap[outputKey]++; + } else { + outputMap[outputKey] = 1; + } + } + + const txSizer = new BtcSizeFeeEstimator(); + + return txSizer.calcTxSize({ + input_script: inputAddressTypeWithFallback, + input_count: inputLength, + ...outputMap + }); +} + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/utils.ts +export function getSpendableAmount ({ feeRate, + recipients, + sender, + utxos }: { + utxos: UtxoResponseItem[]; + feeRate: number; + recipients: string[]; + sender: string; +}) { + const balance = utxos.map((utxo) => utxo.value).reduce((prevVal, curVal) => prevVal + curVal, 0); + + const size = getSizeInfo({ + inputLength: utxos.length, + recipients, + sender + }); + const fee = Math.ceil(size.txVBytes * feeRate); + const bigNumberBalance = new BigN(balance); + + return { + spendableAmount: BigN.max(0, bigNumberBalance.minus(fee)), + fee + }; +} diff --git a/packages/extension-base/src/utils/bitcoin/fee.ts b/packages/extension-base/src/utils/bitcoin/fee.ts new file mode 100644 index 00000000000..f6af5c56ca6 --- /dev/null +++ b/packages/extension-base/src/utils/bitcoin/fee.ts @@ -0,0 +1,14 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { BitcoinFeeInfo, BitcoinFeeRate, FeeOption } from '@subwallet/extension-base/types'; + +export const combineBitcoinFee = (feeInfo: BitcoinFeeInfo, feeOptions?: FeeOption, feeCustom?: BitcoinFeeRate): BitcoinFeeRate => { + if (feeOptions && feeOptions !== 'custom') { + return feeInfo.options?.[feeOptions]; + } else if (feeOptions === 'custom' && feeCustom) { + return feeCustom; + } else { + return feeInfo.options?.[feeInfo.options.default]; + } +}; diff --git a/packages/extension-base/src/utils/bitcoin/index.ts b/packages/extension-base/src/utils/bitcoin/index.ts new file mode 100644 index 00000000000..974d4d14891 --- /dev/null +++ b/packages/extension-base/src/utils/bitcoin/index.ts @@ -0,0 +1,6 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export * from './common'; +export * from './fee'; +export * from './utxo-management'; diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts new file mode 100644 index 00000000000..01626c9c3af --- /dev/null +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -0,0 +1,239 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { BTC_DUST_AMOUNT } from '@subwallet/extension-base/constants'; +import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; +import { DetermineUtxosForSpendArgs, InsufficientFundsError, UtxoResponseItem } from '@subwallet/extension-base/types'; +import { BitcoinAddressType } from '@subwallet/keyring/types'; +import { getBitcoinAddressInfo, validateBitcoinAddress } from '@subwallet/keyring/utils'; +import BigN from 'bignumber.js'; + +import { getSizeInfo, getSpendableAmount } from './common'; + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/utils.ts +// Check if the spendable amount drops when adding a utxo. If it drops, don't use that utxo. +// Method might be not particularly efficient as it would +// go through the utxo array multiple times, but it's reliable. +export function filterUneconomicalUtxos ({ feeRate, + recipients, + sender, + utxos }: { + utxos: UtxoResponseItem[]; + feeRate: number; + sender: string; + recipients: string[]; +}) { + const addressInfo = validateBitcoinAddress(sender) ? getBitcoinAddressInfo(sender) : null; + const inputAddressTypeWithFallback = addressInfo ? addressInfo.type : BitcoinAddressType.p2wpkh; + + const filteredAndSortUtxos = utxos + .filter((utxo) => utxo.value >= BTC_DUST_AMOUNT[inputAddressTypeWithFallback]) + .sort((a, b) => a.value - b.value); // ascending order + + return filteredAndSortUtxos.reduce((utxos, utxo, currentIndex) => { + const utxosWithout = utxos.filter((u) => u.txid !== utxo.txid); + + const { fee: feeWithout, spendableAmount: spendableAmountWithout } = getSpendableAmount({ + utxos: utxosWithout, + feeRate, + recipients, + sender + }); + + const { fee, spendableAmount } = getSpendableAmount({ + utxos, + feeRate, + recipients, + sender + }); + + console.log(utxosWithout, feeWithout, spendableAmountWithout.toString()); + console.log(utxos, fee, spendableAmount.toString()); + + if (spendableAmount.lte(0)) { + return utxosWithout; + } else { + // if spendable amount becomes bigger, do not use that utxo + return spendableAmountWithout.gt(spendableAmount) ? utxosWithout : utxos; + } + }, [...filteredAndSortUtxos]).reverse(); +} + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts +export function determineUtxosForSpendAll ({ feeRate, + recipient, + sender, + utxos }: DetermineUtxosForSpendArgs) { + if (!validateBitcoinAddress(recipient)) { + throw new Error('Cannot calculate spend of invalid address type'); + } + + const recipients = [recipient]; + + const filteredUtxos = filterUneconomicalUtxos({ utxos, feeRate, recipients, sender }); + + const sizeInfo = getSizeInfo({ + sender, + inputLength: filteredUtxos.length, + recipients + }); + + const amount = filteredUtxos.reduce((acc, utxo) => acc + utxo.value, 0) - Math.ceil(sizeInfo.txVBytes * feeRate); + + if (amount <= 0) { + throw new InsufficientFundsError(); + } + + // Fee has already been deducted from the amount with send all + const outputs = [{ value: amount, address: recipient }]; + + const fee = Math.ceil(sizeInfo.txVBytes * feeRate); + + return { + inputs: filteredUtxos, + outputs, + size: sizeInfo.txVBytes, + fee + }; +} + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts +export function determineUtxosForSpend ({ amount, + feeRate, + recipient, + sender, + utxos }: DetermineUtxosForSpendArgs) { + if (!validateBitcoinAddress(recipient)) { + throw new Error('Cannot calculate spend of invalid address type'); + } + + const orderedUtxos = utxos.sort((a, b) => b.value - a.value); + const recipients = [recipient, sender]; + const filteredUtxos = filterUneconomicalUtxos({ + utxos: orderedUtxos, + feeRate, + recipients, + sender + }); + + const neededUtxos = []; + let sum = new BigN(0); + let sizeInfo = null; + + for (const utxo of filteredUtxos) { + sizeInfo = getSizeInfo({ + inputLength: neededUtxos.length, + sender, + recipients + }); + + const currentValue = new BigN(amount).plus(Math.ceil(sizeInfo.txVBytes * feeRate)); + + if (sum.gte(currentValue)) { + break; + } + + sum = sum.plus(utxo.value); + neededUtxos.push(utxo); + + // re calculate size info, some case array end + sizeInfo = getSizeInfo({ + inputLength: neededUtxos.length, + sender, + recipients + }); + } + + if (!sizeInfo) { + throw new InsufficientFundsError(); + } + + const fee = Math.ceil(sizeInfo.txVBytes * feeRate); + + const amountLeft = sum.minus(amount).minus(fee); + + if (amountLeft.lte(0)) { + throw new InsufficientFundsError(); + } + + const outputs = [ + // outputs[0] = the desired amount going to recipient + { value: amount, address: recipient }, + // outputs[1] = the remainder to be returned to a change address + { value: amountLeft.toNumber(), address: sender } + ]; + + return { + filteredUtxos, + inputs: neededUtxos, + outputs, + size: sizeInfo.txVBytes, + fee + }; +} + +export function filterOutPendingTxsUtxos (utxos: UtxoResponseItem[]): UtxoResponseItem[] { + return utxos.filter((utxo) => utxo.status.confirmed); +} + +export function filteredOutTxsUtxos (allTxsUtxos: UtxoResponseItem[], filteredOutTxsUtxos: UtxoResponseItem[]): UtxoResponseItem[] { + if (!filteredOutTxsUtxos.length) { + return allTxsUtxos; + } + + const listFilterOut = filteredOutTxsUtxos.map((utxo) => { + return `${utxo.txid}:${utxo.vout}`; + }); + + return allTxsUtxos.filter((element) => !listFilterOut.includes(`${element.txid}:${element.vout}`)); +} + +// export async function getRuneUtxos (bitcoinApi: _BitcoinApi, address: string) { +// const responseRuneUtxos = await bitcoinApi.api.getRuneUtxos(address); +// const runeUtxos: UtxoResponseItem[] = []; + +// responseRuneUtxos.forEach((responseRuneUtxo) => { +// const txid = responseRuneUtxo.txid; +// const vout = responseRuneUtxo.vout; +// const utxoValue = responseRuneUtxo.satoshi; + +// if (txid && vout && utxoValue) { +// const item = { +// txid, +// vout, +// status: { +// confirmed: true // not use in filter out rune utxos +// }, +// value: utxoValue +// } as UtxoResponseItem; + +// runeUtxos.push(item); +// } +// }); + +// return runeUtxos; +// } + +// export async function getInscriptionUtxos (bitcoinApi: _BitcoinApi, address: string) { +// try { +// const inscriptions = await bitcoinApi.api.getAddressInscriptions(address); + +// return inscriptions.map((inscription) => { +// const [txid, vout] = inscription.output.split(':'); + +// return { +// txid, +// vout: parseInt(vout), +// status: { +// confirmed: true, // not use in filter out inscription utxos +// block_height: inscription.genesis_block_height, +// block_hash: inscription.genesis_block_hash, +// block_time: inscription.genesis_timestamp +// }, +// value: parseInt(inscription.value) +// } as UtxoResponseItem; +// }); +// } catch (e) { +// return []; +// } +// } diff --git a/packages/extension-base/src/utils/index.ts b/packages/extension-base/src/utils/index.ts index 96a1f5d4bd4..a26bd20f238 100644 --- a/packages/extension-base/src/utils/index.ts +++ b/packages/extension-base/src/utils/index.ts @@ -409,3 +409,4 @@ export * from './promise'; export * from './registry'; export * from './swap'; export * from './translate'; +export * from './bitcoin'; \ No newline at end of file From 572e9cedf3d1939650763e44a806cd6f82abae95 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 2 Apr 2025 11:57:01 +0700 Subject: [PATCH 003/178] [Issue-4200] feat: Add pair type for Bitcoin when creating a new account --- .../src/services/keyring-service/context/handlers/Mnemonic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts b/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts index 402321ea543..a3a0599d23b 100644 --- a/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts +++ b/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts @@ -89,7 +89,7 @@ export class AccountMnemonicHandler extends AccountBaseHandler { const addressDict = {} as Record; let changedAccount = false; const hasMasterPassword = keyring.keyring.hasMasterPassword; - const types: KeypairType[] = type ? [type] : ['sr25519', 'ethereum', 'ton', 'cardano']; + const types: KeypairType[] = type ? [type] : ['sr25519', 'ethereum', 'ton', 'cardano', 'bitcoin-44', 'bitcoin-84', 'bitcoin-86', 'bittest-44', 'bittest-84', 'bittest-86']; if (!hasMasterPassword) { if (!password) { From 0b37883e9484f67c756378f779188c8e0f10227f Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 2 Apr 2025 17:31:42 +0700 Subject: [PATCH 004/178] [Issue-4200] feat: Retrieve the Bitcoin address --- .../extension-koni-ui/src/utils/account/account.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index 6b444034250..b60e08a72b0 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -13,8 +13,8 @@ import { MODE_CAN_SIGN } from '@subwallet/extension-koni-ui/constants/signing'; import { AccountAddressType, AccountSignMode, AccountType } from '@subwallet/extension-koni-ui/types'; import { getNetworkKeyByGenesisHash } from '@subwallet/extension-koni-ui/utils/chain/getNetworkJsonByGenesisHash'; import { AccountInfoByNetwork } from '@subwallet/extension-koni-ui/utils/types'; -import { isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; -import { KeypairType } from '@subwallet/keyring/types'; +import { getKeypairTypeByAddress, isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; +import { BitcoinTestnetKeypairTypes, KeypairType } from '@subwallet/keyring/types'; import { Web3LogoMap } from '@subwallet/react-ui/es/config-provider/context'; import { decodeAddress, encodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; @@ -182,6 +182,16 @@ export function getReformatedAddressRelatedToChain (accountJson: AccountJson, ch return reformatAddress(accountJson.address, chainInfo.isTestnet ? 0 : 1); } else if (accountJson.chainType === AccountChainType.CARDANO && chainInfo.cardanoInfo) { return reformatAddress(accountJson.address, chainInfo.isTestnet ? 0 : 1); + } else if (accountJson.chainType === AccountChainType.BITCOIN && chainInfo.bitcoinInfo) { + const isTestnet = chainInfo.isTestnet; + const isBitcoinTestnet = BitcoinTestnetKeypairTypes.includes(accountJson.type); + + // Both must be testnet or both must be mainnet + if (isTestnet !== isBitcoinTestnet) { + return undefined; + } + + return accountJson.address; } return undefined; From 49d460215ba5772c947d1d3786959a7113c0cf56 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 2 Apr 2025 19:26:39 +0700 Subject: [PATCH 005/178] [Issue-4200] feat: Retrieve the Bitcoin address (2) --- .../src/assets/logo/index.ts | 1 + .../src/assets/subwallet/index.ts | 1 + .../AccountProxy/AccountChainAddressItem.tsx | 2 +- .../account/useGetAccountChainAddresses.tsx | 48 ++++++++++++++--- .../extension-koni-ui/src/types/account.ts | 7 +++ .../src/utils/account/account.ts | 51 +++++++++++++++++- .../public/images/projects/ordinal_rune.png | Bin 0 -> 6567 bytes 7 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 packages/extension-koni/public/images/projects/ordinal_rune.png diff --git a/packages/extension-koni-ui/src/assets/logo/index.ts b/packages/extension-koni-ui/src/assets/logo/index.ts index b770265593a..9f84963c821 100644 --- a/packages/extension-koni-ui/src/assets/logo/index.ts +++ b/packages/extension-koni-ui/src/assets/logo/index.ts @@ -23,6 +23,7 @@ export const DefaultLogosMap: Record = { hydradx: '/images/projects/hydradx.png', simple_swap: '/images/projects/simple-swap.png', uniswap: '/images/projects/uniswap.png', + ordinal_rune: './images/projects/ordinal_rune.png', polkadot_assethub: '/images/projects/polkadot-asset-hub.png', kusama_assethub: '/images/projects/kusama-asset-hub.png', rococo_assethub: '/images/projects/rococo-asset-hub.png', diff --git a/packages/extension-koni-ui/src/assets/subwallet/index.ts b/packages/extension-koni-ui/src/assets/subwallet/index.ts index bdf1674b61c..253232e72cc 100644 --- a/packages/extension-koni-ui/src/assets/subwallet/index.ts +++ b/packages/extension-koni-ui/src/assets/subwallet/index.ts @@ -23,6 +23,7 @@ const SwLogosMap: Record = { uniswap: DefaultLogosMap.uniswap, hydradx_mainnet: DefaultLogosMap.hydradx, hydradx_testnet: DefaultLogosMap.hydradx, + ordinal_rune: DefaultLogosMap.ordinal_rune, [SUBSTRATE_GENERIC_KEY]: DefaultLogosMap[SUBSTRATE_GENERIC_KEY], [SUBSTRATE_MIGRATION_KEY]: DefaultLogosMap[SUBSTRATE_MIGRATION_KEY], [SwapProviderId.POLKADOT_ASSET_HUB.toLowerCase()]: DefaultLogosMap.polkadot_assethub, diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx index 73d56ef470a..8c88f1de6f3 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx @@ -45,7 +45,7 @@ function Component (props: Props): React.ReactElement { >
diff --git a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx index 08558301d6a..dc198da4b2b 100644 --- a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx +++ b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx @@ -1,14 +1,45 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { KeypairType } from '@subwallet/keyring/types'; + +import { _ChainInfo } from '@subwallet/chain-list/types'; import { AccountProxy } from '@subwallet/extension-base/types'; import { useReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { AccountChainAddress } from '@subwallet/extension-koni-ui/types'; -import { getChainsByAccountType } from '@subwallet/extension-koni-ui/utils'; +import { getBitcoinAccountDetails, getChainsByAccountType } from '@subwallet/extension-koni-ui/utils'; import { useMemo } from 'react'; // todo: // - order the result + +// Helper function to create an AccountChainAddress object +const createChainAddress = ( + accountType: KeypairType, + chainInfo: _ChainInfo, + address: string, + isBitcoin: boolean +): AccountChainAddress => { + if (isBitcoin) { + const bitcoinInfo = getBitcoinAccountDetails(accountType); + + return { + name: bitcoinInfo.name, + logoKey: bitcoinInfo.logoKey, + slug: chainInfo.slug, + address, + accountType + }; + } + + return { + name: chainInfo.name, + slug: chainInfo.slug, + address, + accountType + }; +}; + const useGetAccountChainAddresses = (accountProxy: AccountProxy): AccountChainAddress[] => { const chainInfoMap = useSelector((state) => state.chainStore.chainInfoMap); const getReformatAddress = useReformatAddress(); @@ -23,12 +54,15 @@ const useGetAccountChainAddresses = (accountProxy: AccountProxy): AccountChainAd const reformatedAddress = getReformatAddress(a, chainInfo); if (reformatedAddress) { - result.push({ - name: chainInfo.name, - slug: chainInfo.slug, - address: reformatedAddress, - accountType: a.type - }); + const isBitcoin = chain.includes('bitcoin'); + const chainAddress = createChainAddress( + a.type, + chainInfo, + reformatedAddress, + isBitcoin + ); + + result.push(chainAddress); } } }); diff --git a/packages/extension-koni-ui/src/types/account.ts b/packages/extension-koni-ui/src/types/account.ts index 88598e0bf09..f59e0625c16 100644 --- a/packages/extension-koni-ui/src/types/account.ts +++ b/packages/extension-koni-ui/src/types/account.ts @@ -33,6 +33,13 @@ export type AccountChainAddress = { slug: string; address: string; accountType: KeypairType; + logoKey?: string +} + +export interface BitcoinAccountInfo { + name: string; + logoKey?: string; + order: number; } export type AccountAddressItemType = { diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index b60e08a72b0..c21cb625012 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -10,10 +10,10 @@ import { AbstractAddressJson, AccountChainType, AccountJson, AccountProxy, Accou import { isAccountAll, reformatAddress, uniqueStringArray } from '@subwallet/extension-base/utils'; import { DEFAULT_ACCOUNT_TYPES, EVM_ACCOUNT_TYPE, SUBSTRATE_ACCOUNT_TYPE, TON_ACCOUNT_TYPE } from '@subwallet/extension-koni-ui/constants'; import { MODE_CAN_SIGN } from '@subwallet/extension-koni-ui/constants/signing'; -import { AccountAddressType, AccountSignMode, AccountType } from '@subwallet/extension-koni-ui/types'; +import { AccountAddressType, AccountSignMode, AccountType, BitcoinAccountInfo } from '@subwallet/extension-koni-ui/types'; import { getNetworkKeyByGenesisHash } from '@subwallet/extension-koni-ui/utils/chain/getNetworkJsonByGenesisHash'; import { AccountInfoByNetwork } from '@subwallet/extension-koni-ui/utils/types'; -import { getKeypairTypeByAddress, isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; +import { isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; import { BitcoinTestnetKeypairTypes, KeypairType } from '@subwallet/keyring/types'; import { Web3LogoMap } from '@subwallet/react-ui/es/config-provider/context'; @@ -163,6 +163,53 @@ export const convertKeyTypes = (authTypes: AccountAuthType[]): KeypairType[] => return _rs.length ? _rs : DEFAULT_ACCOUNT_TYPES; }; +export function getBitcoinAccountDetails (type: KeypairType): BitcoinAccountInfo { + const result: BitcoinAccountInfo = { + name: 'Unknown', + order: 99 + }; + + switch (type) { + case 'bitcoin-44': + result.logoKey = 'bitcoin'; + result.name = 'Bitcoin (BIP44)'; + result.order = 1; + break; + + case 'bitcoin-84': + result.logoKey = 'bitcoin'; + result.name = 'Bitcoin'; + result.order = 2; + break; + + case 'bitcoin-86': + result.logoKey = 'ordinal_rune'; + result.name = 'Ordinal, Runes'; + result.order = 3; + break; + + case 'bittest-44': + result.logoKey = 'bitcoinTestnet'; + result.name = 'Bitcoin testnet'; + result.order = 4; + break; + + case 'bittest-84': + result.logoKey = 'bitcoinTestnet'; + result.name = 'Bitcoin testnet'; + result.order = 5; + break; + + case 'bittest-86': + result.logoKey = 'bitcoinTestnet'; + result.name = 'Bitcoin testnet (BIP86)'; + result.order = 6; + break; + } + + return result; +} + // todo: // - support bitcoin export function getReformatedAddressRelatedToChain (accountJson: AccountJson, chainInfo: _ChainInfo): string | undefined { diff --git a/packages/extension-koni/public/images/projects/ordinal_rune.png b/packages/extension-koni/public/images/projects/ordinal_rune.png new file mode 100644 index 0000000000000000000000000000000000000000..7b9b83e1bbdd5989ef4eac3b52804ec00af4051f GIT binary patch literal 6567 zcmV;Y8Cd3tP)005u}1^@s6i_d2*00009a7bBm000ZH z000ZH0U9_EasU7T0drDELIAGL9O(c600d`2O+f$vv5yPUpM*egLruoA|xx3#0R z1Q0Ng@ZH~IUtBi#-uK@2P3~{zv76nO&9d*m=bm%!x#zZ%HbhxjSw)8q9mwXI2@MJvF#HLGfBYN}Kh z2BB3-@rf)|JYlNai3qc?u@ObcnipVBE?% zGK8{@HR>2!Xksh~0hX7SSE&!@IA`f{Mm1ubIdf)>frUpl3GiuJ70I_HM1*Mx(21|jleP#`7GON!m?}*J(CiD>OhF(Wlq721RET9{D9ifO9 z9fu-R6uk1vD-TgGIz$|fa8!hiibD;-YWV$0obTs|27PYU2})204#8MIapH4Chs9@s zmGFTU&{;o68U$KFs|9q{&ygnn+HWIP0hW}MM9S$n9LcEQ^+4P6Mghi`QdWq=kseiX zEom_d(4}1*nL=slIOB7=2jjJVbHw4u48~wYYXRH0Z$D5Rj?7^UMzjL7nM{Wx6->cM ziy_zzzm9C7hT)#c!*B}==!RcMw$N#-g#cCVuSy(_ETFX2FZr<+LvSHZy)`v8$&w{Y zyo39Vjg8(d$I_)sy+ipJ-nr^O*?g#PoOdXf!n<|kow^w_X7o&aU^xLQ_pdU()H1L5 zf@^4Kz;E#Grwf8qk(jKo!U}qRd>MU~_jgtu_vLRqwE zQS*?RZ4$-E(A}w1C+Xa|vm2&!LzOy4`adiuKy@*;k{sa+k0aC8F4gye5b1;G$R0{# z%LO9MQ80a8BuBvq)^7If+49dn|D+^7tQJD)*|VptvdSv5+G?xG|Ni$s%EzQ@*RCmB zGTzsmIdkN;+ivsTpCc=j#kJ2er&?@a0TE~K-o3rw1w{Gdk3Y)0@4hRKJ@%MPo;*3w^8WC{4|2#MhsZ@2 zT_jgsb(I^MGfT79ny+YidHIBFY@@)1W~fC3K`@N=@84gB4I3sq?68Ami(h~JRi1wO zX}SCEyXB{!ehNH){q@(E=bwLGw%B3|akOOMz=86?2Ooq!Um2knX3m`H&5AVpfSMh` z(+2U)ojW&Bd@HWFqMUl_sq)cBAITF>JdsuL0V$UA&p%&2`|LA$=%I&X?X}mop3iW; z=bn4Ylqr#W?>ICh2D_dG%tiqM;$z+!zsv^T9)`HBx87Rv1@9AoZ_Jo6qQ05N^SX8G zCS%8r_54Dt=;X=azxBlkU(}s;>Kk}oR0{}xVS*>X_?{jUBtsy&`ucjS`0(-Wyz|bK zoqPg{>C>ls(JB^}*^TbqyL+#js=tP)@H=07?KR^$y#X3d%6?x&zlaj!E1c{@yAkK zT`jM?@{0WS+iw!~Jur-N@cZ}Na}O5Y6XT(W9_mH5_?#w4>EIG&tHLQe(} z^1jMf?&(=xM|Av{qa^W%bl}31W-;a3n0vkV-g~{_D@c3b>#x7$nrp6+4L97-oAI#a z0}(5}O+`h8_IZAH_MG~h%wM-c*5RL$#2-=xp^)p-f%xva>n=I|_~VVgubfdYEwaZR zdw8jlTtJY--|XABuYB^!C&u&e3nxyT=m}OwL&IV<^YV{4BJ-aG^vz)^_6+m70*8VY~vIfz8--AQEfiKeK_~rbLGk_uQU$(zOPoc|)u!Q7rk!@4ovk^xt~wsi(Zx zee}^swE@$aXPzk&CQR@Greqc@T%?lVoveYD<3Gqhm!z74@z-nUxbVUYW#^rD);>o% zgo_wf5JaLx&{VT9Z%ZDJz`&L`ClS5NF1yHOmtE!wk-v#>!wLg`kN<Fr3oejvzx`JG9MQdQDsfr)*+OxIqO24ZN<^@J zItQQO@`xk!bEM3IPzV~tKKtzB`E5*}WdjNpsC;3r7=hWW6{!_`)wkYyt3~w7>ySre zm``SbimJPI?V_gj^bzx#Yvy*v-Q2nL)>K)(VD!Z)iX=dUXUcgo#idJA{PD*ha??#W zNf1o(Uw{4el4fQo5F*^6Lx+0vzTDdUQ34JU={l=PiU~0%cO7eNT(;`2bLUPieh2?` z@|2Gym)v&iEh^7?ixe74D?K9nfQV0L{WjZdGueOt{aZZFxeUuhrZ%vAq;0Y-zC?Th z1wnNTGbhPVABbC_W6>g)m<~c|qbKGn{HkIRqxC!BfCIc?DH-x+&pr2?*kG=eNLQD8 z%Qg(7MCCS^DkK8e!4#v;>MvaMuS-k^;0Gb0;RDqL9}HzjAAPj;cgP14fLPvy7jcHE z*UJ`IXe_u5){p4?MShP(Q^_pL6og;>*H$Ts5`-W=-M5;>IxLmco&U@;&xj3%umcaY zHd4>6r30Csy#JhNL`c0KuJCTN9lK`nU)?r&)0ekJWm$%To5I{f- z#WoP2Zc#lojBBj1MvKRZhFwzHPDTW%vmBgjtFr?tlnV(gIm59DB4OSCiD;O$!^tdb zGv@?oqaQuk%+C z5MF>LUxq0y9kEzUY!sUH^P$kdhUx#sG^CCt&Ihy-$pY#ym8nzMX+xJgD+DMoF#Vr^ zh455RCu5qs&S&q#Vjyh`Xj)@SRk976v!8El6*!1bxBacDO|284ok&rz4wD~eoBaSH z$3jInrRYO+ud{%x{?p-R;#Z55abQ<Z&@^P4E|zX;Dv@;iy<`h4nJt3HcV&fMGNz{HC2s2nD`^YRbg@kRRD9ll|9vS$ zYHMq)?ty7Js440E=h-E+MF^JRK4W36@}*gamvB%wZ=(Zcx|XhVxyQ1Ur!&6w{DunxLoa zl?^%;=ob6ri(7BK)ta}X?ol^`gK+8Gkc_t}q8oy(WJG|b!qdYJJItB~V;7?bAAC>> z0qXpoefC-FIfDle);`V`P&Ye*^>cYZD|HAkc&C;)CIJ%B(v2z^DrH!1X@hmZ_=EX` zSsVHj+VX$YJ<{J=hiTnQ=T7?RIY%?~ku0FU3wbhJI8*e<+FKhWq%Xes;*4xcohn$q zpv6-JAG!oI{ijV4{R%5M0S18pBROHF;se@g=%%o$s;Z2|nm*&kjq_S}F?h!91x8)Y(BLHhOC zlDtHpv|*!(PA(QJ5xMTS-+qCGv#fWy)yS@}N81+-r4_yzd*LrQ;~;M|8o84~3pIG$$w-F=B+V#x7mx z6G$T@+W7`S?-<>JBLWv18|mnzd_RU@9dxQH#705+fYCSfR9qrhK$CyNaI1qkRGteG zJC^Xq8*lXXkPEYwY83EM*(2nbV~z+6w!S^z$rKRf^4hx z&O7gDpCgT7vKq;}{PN4*bRL39x3?Yc!T9mxy#?g7kPE8T2Da*=S-CFt#4bx%dL5ka zqTg_ML5TV%CMkdbbym>d%+GX@ufF=~z_qtN2oJ%myY9MP^CNt&u={Z^L=t3>XJiN+ zG-!~w`%)O#7lbV&Of`)R%O>mGeZ{UW_y~#moMP=%g0)h8Yp1620*K92h)MpBZ4Y#Q z9BB*oVhd{Bp?SmJf}uWr`gl#y2@qJ|&fmZ%<-JfAMKn%hNv6l5ZuiCFB7|;=dWrVg z+eG&TBk)*hr5>$DfLY$I6}E1tE@*`sqEdFYNJ8s!Xj}wR6eb@EOUAw%_)>)e7LV;0 zOya{L;}+C>mjBn?cf+XyDuh=u}B9VE0#*)R)I2nky{ zTrotO#K#wnoS|vx?bfZUt9*$FJEr@)W&SKFg41ohNgv zm`f`{a6(BK`+?}zdHB%0(lk}LZ0oS@F7SwsXY~BtGE=sI@!$8Hchg0fHf@?$8))m6 zK%S6-0E8G+N0=^js%O`B%E1N<7+`$gMAGC838qZ+?AgPZbaq(rjGjLP$0SY24cbIV zN`%Hgwu)n*VI>hPktoBrtxG`^@HvT|v1D}aV#g-inib}UNhj&17JcyhI(I6(4qwH9 z@Ai3LUGg!K0l1*}b)-XdmBpk*h?tn^pt#dC)rN?fJLX`nmN{ffp0K1W{t254#Huna zNHq?#8r%;vRI;1WdFP#HUAedErkhCJoWfn(T<9o+kA`F)dgB%EtVVo9e9Ned#kjHudSYXy86VR|M)ZflEtbfM)Wu3~U zmtN}4c9`JHGho1OGW@D5#E}}xz42z?eCH)583Yk>G3lD16CsZ=6cPZiKM0!%hJj&} zSrLv*L$E|jh$%Z8Y(+MD^k|tf`2#Oc)|%ODv&|)2sNAt<0h>)jd3kv(Y`fmDV40XP zur2f<3Z^S54Z{2O&HV+%(ov3&sBCl2yif9)>}isz(W12wH4%-=r#^SDvuR>UeywB} zEEa1iY>8$H1t?&kV=B{g+z;#Hr2`g^9g^6-m3{1N;v>K!5@ydhC%*Ja%sV&d4>A>q zBtu>g-;4`vD|<(DEDC38ZmX@f^6CQFUbT1c%o%0opxOCy%$PA=QaMb6Bz#nA8G{yA zIl`j^SmWPMOkic)0{UP92?p>9DPXV-$yh|{?9Mpj4A1fqxgtd7VA&RnVgw>>n^pDw zPXxx)8PPKvU(?~xpl#6=$a@wf(wlWV>r!s z6$!#)KHGmkws~PkdZiSH4CaIorCD3(o>44u(psqG?CYY{1aXYz%+j4YfI= zCh>>m1gM6?ntY`h@C`Ze<=7AqK{7mNQjCbq@A1svaw(PEqpm^Caz7`ZkV`)>2!ap< ziB;s_K97YJrQb>fp)t&J@K}(wquk#S9;%AMEF4Sr|3K$_Q6qnx6aa{YBiT~Se-O!k~lI?V*bxZCqPar2mzY#1?DI^S_HW*uA&<1?6dI>M~djQ)k1)E z%Qix*5r-qQOsvyvvZo!#)L30OYb716g=*b{S^=u`2Pa2v+2L@QFu8VG0meQMUeFas|CbyR0_UB9F7zSvVIUaS?a8yBNf6}KM0&qja$J$B|996 zgw;&wauF91JHisy`a$TD5@!V+VNqHc6!zTEtw|RwSP<*lwJVDy`iR3}=8hRNW>kkg zIa0+Y0ZGS>9j7XVSBS%r1Ve8_L&MglX*V5_sycb$rlzLj#o^Gv&>Ka35V<=zLvfcb zUFwzE_ZEjEj?%*jHS|6i_4iV0Pp?EcMfrn7IhEpYh*T@_4U-f|d11xk#f#ZZ;`^e7N={B_;i%`hCm2hNKBQEvdj{f{TpX7QXn> z-RLMI{V1FW+ZLG;A7o^4EpLQ3ZT|fEbsai%7_0ok)x<@|trBHm z#;Sm0@XVPrr%Q&&=q`ba6e>kCzFW6$4DD6oXf-JJYM4s3T*5V$43f2;WW9Rzs#M>Y zan3I+Zg8uZ{Zdx1MzTQG_DNNiaC&@5?kui=p;)l=(Irwen`4m5Q6j8T^N>~Q$E}LIsZcW|Lp!z)JRRE232YSGGH?ZES3$2&7hh4mHZa#iR&n zRoeqGY?qdnVkyhw!mLmaS8%Hy%jjm@*#?oR_Z(AqW8B6ojSyR`P>0i0XjA)B#r?{P zxIn#I9_ChA5*{myKjxjz&;IX56fnbKec}UgF(E9Hy{s Date: Thu, 3 Apr 2025 12:38:50 +0700 Subject: [PATCH 006/178] [Issue-4200] refactor: Support Bitcoin for new unified account --- .../extension-base/src/services/chain-service/constants.ts | 5 ++++- .../services/keyring-service/context/handlers/Mnemonic.ts | 4 ++-- .../components/AccountProxy/list/AccountChainAddressList.tsx | 2 +- .../src/hooks/account/useGetAccountChainAddresses.tsx | 3 ++- packages/extension-koni-ui/src/utils/account/account.ts | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/constants.ts b/packages/extension-base/src/services/chain-service/constants.ts index 1258112dffe..94b2e8cf7ff 100644 --- a/packages/extension-base/src/services/chain-service/constants.ts +++ b/packages/extension-base/src/services/chain-service/constants.ts @@ -296,10 +296,13 @@ export const LATEST_CHAIN_DATA_FETCHING_INTERVAL = 120000; // TODO: review const TARGET_BRANCH = process.env.NODE_ENV !== 'production' ? 'koni-dev' : 'master'; +export const _BITCOIN_CHAIN_SLUG = 'bitcoin'; +export const _BITCOIN_TESTNET_CHAIN_SLUG = 'bitcoinTestnet'; + export const _CHAIN_INFO_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/ChainInfo.json`; export const _CHAIN_ASSET_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/ChainAsset.json`; export const _ASSET_REF_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/AssetRef.json`; export const _MULTI_CHAIN_ASSET_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/MultiChainAsset.json`; export const _CHAIN_LOGO_MAP_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/ChainLogoMap.json`; export const _ASSET_LOGO_MAP_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/AssetLogoMap.json`; -export const _BEAR_TOKEN = "aHR0cHM6Ly9xdWFuZ3RydW5nLXNvZnR3YXJlLnZuL2FwaS9tYXN0ZXIvYXBpLXB1YmxpYw=="; \ No newline at end of file +export const _BEAR_TOKEN = 'aHR0cHM6Ly9xdWFuZ3RydW5nLXNvZnR3YXJlLnZuL2FwaS9tYXN0ZXIvYXBpLXB1YmxpYw=='; diff --git a/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts b/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts index a3a0599d23b..9424abe8978 100644 --- a/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts +++ b/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts @@ -27,7 +27,7 @@ export class AccountMnemonicHandler extends AccountBaseHandler { /* Create seed */ public async mnemonicCreateV2 ({ length = SEED_DEFAULT_LENGTH, mnemonic: _seed, type = 'general' }: RequestMnemonicCreateV2): Promise { - const types: KeypairType[] = type === 'general' ? ['sr25519', 'ethereum', 'ton', 'cardano'] : ['ton-native']; + const types: KeypairType[] = type === 'general' ? ['sr25519', 'ethereum', 'ton', 'cardano', 'bitcoin-44', 'bitcoin-84', 'bitcoin-86', 'bittest-44', 'bittest-84', 'bittest-86'] : ['ton-native']; const seed = _seed || type === 'general' ? mnemonicGenerate(length) @@ -57,7 +57,7 @@ export class AccountMnemonicHandler extends AccountBaseHandler { assert(mnemonicValidate(phrase), t('Invalid seed phrase. Please try again.')); mnemonicTypes = 'general'; - pairTypes = ['sr25519', 'ethereum', 'ton']; + pairTypes = ['sr25519', 'ethereum', 'ton', 'bitcoin-44', 'bitcoin-84', 'bitcoin-86', 'bittest-44', 'bittest-84', 'bittest-86']; } catch (e) { assert(tonMnemonicValidate(phrase), t('Invalid seed phrase. Please try again.')); mnemonicTypes = 'ton'; diff --git a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx index 9f717ee011d..ade66889377 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx @@ -116,7 +116,7 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { className={'address-item'} isShowInfoButton={isPolkadotUnifiedChain} item={item} - key={item.slug} + key={`${item.slug}_${item.address}`} onClick={onShowQr(item)} onClickCopyButton={onCopyAddress(item)} onClickInfoButton={onClickInfoButton(item)} diff --git a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx index dc198da4b2b..1d9859550ed 100644 --- a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx +++ b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx @@ -4,6 +4,7 @@ import type { KeypairType } from '@subwallet/keyring/types'; import { _ChainInfo } from '@subwallet/chain-list/types'; +import { _BITCOIN_CHAIN_SLUG, _BITCOIN_TESTNET_CHAIN_SLUG } from '@subwallet/extension-base/services/chain-service/constants'; import { AccountProxy } from '@subwallet/extension-base/types'; import { useReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { AccountChainAddress } from '@subwallet/extension-koni-ui/types'; @@ -54,7 +55,7 @@ const useGetAccountChainAddresses = (accountProxy: AccountProxy): AccountChainAd const reformatedAddress = getReformatAddress(a, chainInfo); if (reformatedAddress) { - const isBitcoin = chain.includes('bitcoin'); + const isBitcoin = [_BITCOIN_CHAIN_SLUG, _BITCOIN_TESTNET_CHAIN_SLUG].includes(chain); const chainAddress = createChainAddress( a.type, chainInfo, diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index c21cb625012..ea8aec358b6 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -172,7 +172,7 @@ export function getBitcoinAccountDetails (type: KeypairType): BitcoinAccountInfo switch (type) { case 'bitcoin-44': result.logoKey = 'bitcoin'; - result.name = 'Bitcoin (BIP44)'; + result.name = 'Bitcoin (Legacy)'; result.order = 1; break; From 9924ae6d6a2b45dfa1ea7e71115a988a89df335f Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 3 Apr 2025 12:51:36 +0700 Subject: [PATCH 007/178] [Issue-4200] refactor: fix eslint --- .../helpers/subscribe/bitcoin.ts | 119 ++-- .../helpers/subscribe/index.ts | 6 +- .../bitcoin/strategy/BlockStream/index.ts | 2 +- .../bitcoin/strategy/BlockStream/types.ts | 585 +++++++++--------- .../handler/bitcoin/strategy/types.ts | 2 +- .../src/services/chain-service/index.ts | 9 +- .../src/services/chain-service/types.ts | 5 +- .../strategy/api-request-strategy/types.ts | 47 +- packages/extension-base/src/types/bitcoin.ts | 211 ++++--- packages/extension-base/src/types/fee/base.ts | 2 +- 10 files changed, 494 insertions(+), 494 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index 53f32f3f30a..bde5644456c 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -1,21 +1,19 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; -import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; -import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; +import { UtxoResponseItem } from '@subwallet/extension-base/types'; // import { filterAssetsByChainAndType, filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { try { - console.log("AAAAAAAAAAAAAAAAAA") + console.log('AAAAAAAAAAAAAAAAAA'); // const [utxos] = await Promise.all([ - // await bitcoinApi.api.getUtxos(address), - // await getRuneUtxos(bitcoinApi, address), - // await getInscriptionUtxos(bitcoinApi, address) + // await bitcoinApi.api.getUtxos(address), + // await getRuneUtxos(bitcoinApi, address), + // await getInscriptionUtxos(bitcoinApi, address) // ]); // const response = await fetch(`https://blockstream.info/api/address/${address}/utxo`); @@ -24,8 +22,8 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre // throw new Error(`HTTP error! Status: ${response.status}`); // } - console.log("BITCOIN API: ", await bitcoinApi.api); - const utxos = await bitcoinApi.api.getUtxos(address) + // console.log('BITCOIN API: ', await bitcoinApi.api); + const utxos = await bitcoinApi.api.getUtxos(address); // const utxos = await response.json(); // console.log('UTXOUTXOUTXO: ', utxos); // let filteredUtxos: UtxoResponseItem[]; @@ -91,66 +89,69 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) })); } -export function subscribeBitcoinBalance_Old (addresses: string[], chainInfo: _ChainInfo, assetMap: Record, bitcoinApi: _BitcoinApi, callback: (rs: BalanceItem[]) => void): () => void { - const nativeSlug = _getChainNativeTokenSlug(chainInfo); - - const getBalance = () => { - getBitcoinBalance(bitcoinApi, addresses) - .then((balances) => { - return balances.map(({ balance, bitcoinBalanceMetadata }, index): BalanceItem => { - return { - address: addresses[index], - tokenSlug: nativeSlug, - state: APIItemState.READY, - free: balance, - locked: '0', - metadata: bitcoinBalanceMetadata - }; - }); - }) - .catch((e) => { - console.error(`Error on get Bitcoin balance with token ${nativeSlug}`, e); - - return addresses.map((address): BalanceItem => { - return { - address: address, - tokenSlug: nativeSlug, - state: APIItemState.READY, - free: '0', - locked: '0' - }; - }); - }) - .then((items) => { - callback(items); - }) - .catch(console.error); - }; - - console.log('btc balance: ', getBalance()); - - return () => { - console.log('unsub'); - }; -}; - +// export function subscribeBitcoinBalance_Old (addresses: string[], chainInfo: _ChainInfo, assetMap: Record, bitcoinApi: _BitcoinApi, callback: (rs: BalanceItem[]) => void): () => void { +// const nativeSlug = _getChainNativeTokenSlug(chainInfo); +// +// const getBalance = () => { +// getBitcoinBalance(bitcoinApi, addresses) +// .then((balances) => { +// return balances.map(({ balance, bitcoinBalanceMetadata }, index): BalanceItem => { +// return { +// address: addresses[index], +// tokenSlug: nativeSlug, +// state: APIItemState.READY, +// free: balance, +// locked: '0', +// metadata: bitcoinBalanceMetadata +// }; +// }); +// }) +// .catch((e) => { +// console.error(`Error on get Bitcoin balance with token ${nativeSlug}`, e); +// +// return addresses.map((address): BalanceItem => { +// return { +// address: address, +// tokenSlug: nativeSlug, +// state: APIItemState.READY, +// free: '0', +// locked: '0' +// }; +// }); +// }) +// .then((items) => { +// callback(items); +// }) +// .catch(console.error); +// }; +// +// console.log('btc balance: ', getBalance()); +// +// return () => { +// console.log('unsub'); +// }; +// } export const subscribeBitcoinBalance = async (addresses: string[]) => { - const bitcoinApi = {} as _BitcoinApi; + const getBalance = async () => { try { const balances = await getBitcoinBalance(bitcoinApi, addresses); + return balances[0].balance; } catch (e) { - console.error(`Error on get Bitcoin balance with token`, e); + console.error('Error on get Bitcoin balance with token', e); + return '0'; - }; - } - const balanceBTC = await getBalance(); + } + }; + + const balanceBTC = await getBalance(); + console.log('btc balance: ', balanceBTC); return () => { console.log('unsub'); }; -}; \ No newline at end of file +}; diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index efae2f2e445..d7a1e6ea8ac 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -3,9 +3,10 @@ import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; import { APIItemState, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { subscribeBitcoinBalance } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/bitcoin'; import { subscribeCardanoBalance } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano'; -import { _BitcoinApi, _CardanoApi, _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain, _isPureBitcoinChain } from '@subwallet/extension-base/services/chain-service/utils'; +import { _CardanoApi, _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; +import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountJson, BalanceItem } from '@subwallet/extension-base/types'; import { filterAssetsByChainAndType, getAddressesByChainTypeMap, pairToAccount } from '@subwallet/extension-base/utils'; import keyring from '@subwallet/ui-keyring'; @@ -13,7 +14,6 @@ import keyring from '@subwallet/ui-keyring'; import { subscribeTonBalance } from './ton/ton'; import { subscribeEVMBalance } from './evm'; import { subscribeSubstrateBalance } from './substrate'; -import {subscribeBitcoinBalance} from "@subwallet/extension-base/services/balance-service/helpers/subscribe/bitcoin"; /** * @function getAccountJsonByAddress 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 cd67f514e78..4d911397c85 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 @@ -3,7 +3,7 @@ import { SWError } from '@subwallet/extension-base/background/errors/SWError'; import { _BEAR_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; -import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, Brc20BalanceItem, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData, RuneTxs, RuneTxsResponse, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, RecommendedFeeEstimates, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { BitcoinApiStrategy, BitcoinTransactionEventMap } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { OBResponse } from '@subwallet/extension-base/services/chain-service/types'; // import { HiroService } from '@subwallet/extension-base/services/hiro-service'; diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts index 006b02d7100..28b497c3cab 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts @@ -2,302 +2,301 @@ // SPDX-License-Identifier: Apache-2.0 export interface BlockStreamBlock { - id: string; - height: number; - version: number; - timestamp: number; - tx_count: number; - size: number; - weight: number; - merkle_root: string; - previousblockhash: string; - mediantime: number; - nonce: number; - bits: number; - difficulty: number; - } - - export interface BitcoinAddressSummaryInfo { - address: string, - chain_stats: { - funded_txo_count: number, - funded_txo_sum: number, - spent_txo_count: number, - spent_txo_sum: number, - tx_count: number - }, - mempool_stats: { - funded_txo_count: number, - funded_txo_sum: number, - spent_txo_count: number, - spent_txo_sum: number, - tx_count: number - }, - balance: number, - total_inscription: number - } - - // todo: combine RunesByAddressResponse & RunesCollectionInfoResponse - - export interface RunesInfoByAddressResponse { - statusCode: number, - data: RunesInfoByAddressFetchedData - } - - export interface RunesInfoByAddressFetchedData { - limit: number, - offset: number, - total: number, - runes: RunesInfoByAddress[] - } - - // todo: check is_hot and turbo and cenotaph attributes meaning in RuneInfoByAddress - - export interface RunesInfoByAddress { - amount: string, - address: string, - rune_id: string, - rune: { - rune: string, - rune_name: string, - divisibility: number, - premine: string, - spacers: string, - symbol: string - } - } - - export interface RunesCollectionInfoResponse { - statusCode: number, - data: RunesCollectionInfoFetchedData - } - - interface RunesCollectionInfoFetchedData { - limit: number, - offset: number, - total: number, - runes: RunesCollectionInfo[] - } - - export interface RunesCollectionInfo { - rune_id: string, + id: string; + height: number; + version: number; + timestamp: number; + tx_count: number; + size: number; + weight: number; + merkle_root: string; + previousblockhash: string; + mediantime: number; + nonce: number; + bits: number; + difficulty: number; +} + +export interface BitcoinAddressSummaryInfo { + address: string, + chain_stats: { + funded_txo_count: number, + funded_txo_sum: number, + spent_txo_count: number, + spent_txo_sum: number, + tx_count: number + }, + mempool_stats: { + funded_txo_count: number, + funded_txo_sum: number, + spent_txo_count: number, + spent_txo_sum: number, + tx_count: number + }, + balance: number, + total_inscription: number +} + +// todo: combine RunesByAddressResponse & RunesCollectionInfoResponse + +export interface RunesInfoByAddressResponse { + statusCode: number, + data: RunesInfoByAddressFetchedData +} + +export interface RunesInfoByAddressFetchedData { + limit: number, + offset: number, + total: number, + runes: RunesInfoByAddress[] +} + +// todo: check is_hot and turbo and cenotaph attributes meaning in RuneInfoByAddress + +export interface RunesInfoByAddress { + amount: string, + address: string, + rune_id: string, + rune: { rune: string, rune_name: string, - divisibility: string, - spacers: string - } - - export interface RuneTxsResponse { - statusCode: number, - data: RuneTxsFetchedData - } - - interface RuneTxsFetchedData { - limit: number, - offset: number, - total: number, - transactions: RuneTxs[] - } - - export interface RuneTxs { - txid: string, - vout: RuneTxsUtxosVout[] - } - - interface RuneTxsUtxosVout { - n: number, - value: number, - runeInject: any - } - - export interface Brc20MetadataFetchedData { - token: Brc20Metadata - } - - export interface Brc20Metadata { - ticker: string, - decimals: number - } - - export interface Brc20BalanceFetchedData { - limit: number, - offset: number, - total: number, - results: Brc20Balance[] - } - - export interface Brc20Balance { - ticker: string, - available_balance: string, - transferrable_balance: string, - overall_balance: string - } - - export interface Brc20BalanceItem { - free: string, - locked: string - } - - export interface InscriptionFetchedData { - limit: number, - offset: number, - total: number, - results: Inscription[] - } - - export interface Inscription { - id: string; - number: number; - address: string; - genesis_block_height: number; - genesis_block_hash: string; - genesis_timestamp: number; - tx_id: string; - location: string; - output: string; - value: string; - offset: string; - fee: number; - sat_ordinal: string; - sat_rarity: string; - content_type: string; - content_length: number; - // content: any - } - - export interface UpdateOpenBitUtxo { - totalUtxo: number, - utxoItems: BlockStreamUtxo[] - } - - export interface BlockStreamUtxo { - txid: string; - vout: number; - status: { - confirmed: boolean; - block_height?: number; - block_hash: string; - block_time?: number; - }, - value: number; - } - - export interface BlockStreamTransactionStatus { - confirmed: boolean; - block_height: number; - block_hash: string; - block_time: number; - } - - export interface BlockStreamFeeEstimates { - 1: number; - 2: number; - 3: number; - 4: number; - 5: number; - 6: number; - 7: number; - 8: number; - } - - export interface RecommendedFeeEstimates { - fastestFee: number, - halfHourFee: number, - hourFee: number, - economyFee: number, - minimumFee: number - } - - export interface BlockStreamTransactionVectorOutput { - scriptpubkey: string; - scriptpubkey_asm: string; - scriptpubkey_type: string; - scriptpubkey_address: string; - value: number; - } - - export interface BlockStreamTransactionVectorInput { - is_coinbase: boolean; - prevout: BlockStreamTransactionVectorOutput; - scriptsig: string; - scriptsig_asm: string; - sequence: number; - txid: string; - vout: number; - witness: string[]; - } - - export interface BlockStreamTransactionDetail { - txid: string; - version: number; - locktime: number; - totalVin: number; - totalVout: number; - size: number; - weight: number; - fee: number; - status: { - confirmed: boolean; - block_height?: number; - block_hash?: string; - block_time?: number; - } - vin: BlockStreamTransactionVectorInput[]; - vout: BlockStreamTransactionVectorOutput[]; - } - - export interface RuneUtxoResponse { - start: number, - total: number, - utxo: RuneUtxo[] - } - - export interface RuneUtxo { - height: number, - confirmations: number, - address: string, - satoshi: number, - scriptPk: string, - txid: string, - vout: number, - runes: RuneInject[] - } - - interface RuneInject { - rune: string, - runeid: string, - spacedRune: string, - amount: string, - symbol: string, - divisibility: number - } - - export interface RuneMetadata { - id: string, - mintable: boolean, - parent: string, - entry: RuneInfo - } - - interface RuneInfo { - block: number, - burned: string, divisibility: number, - etching: string, - mints: string, - number: number, premine: string, - spaced_rune: string, - symbol: string, - terms: RuneTerms - timestamp: string, - turbo: boolean + spacers: string, + symbol: string } - - interface RuneTerms { - amount: string, - cap: string, - height: string[], - offset: string[] +} + +export interface RunesCollectionInfoResponse { + statusCode: number, + data: RunesCollectionInfoFetchedData +} + +interface RunesCollectionInfoFetchedData { + limit: number, + offset: number, + total: number, + runes: RunesCollectionInfo[] +} + +export interface RunesCollectionInfo { + rune_id: string, + rune: string, + rune_name: string, + divisibility: string, + spacers: string +} + +export interface RuneTxsResponse { + statusCode: number, + data: RuneTxsFetchedData +} + +interface RuneTxsFetchedData { + limit: number, + offset: number, + total: number, + transactions: RuneTxs[] +} + +export interface RuneTxs { + txid: string, + vout: RuneTxsUtxosVout[] +} + +interface RuneTxsUtxosVout { + n: number, + value: number, + runeInject: any +} + +export interface Brc20MetadataFetchedData { + token: Brc20Metadata +} + +export interface Brc20Metadata { + ticker: string, + decimals: number +} + +export interface Brc20BalanceFetchedData { + limit: number, + offset: number, + total: number, + results: Brc20Balance[] +} + +export interface Brc20Balance { + ticker: string, + available_balance: string, + transferrable_balance: string, + overall_balance: string +} + +export interface Brc20BalanceItem { + free: string, + locked: string +} + +export interface InscriptionFetchedData { + limit: number, + offset: number, + total: number, + results: Inscription[] +} + +export interface Inscription { + id: string; + number: number; + address: string; + genesis_block_height: number; + genesis_block_hash: string; + genesis_timestamp: number; + tx_id: string; + location: string; + output: string; + value: string; + offset: string; + fee: number; + sat_ordinal: string; + sat_rarity: string; + content_type: string; + content_length: number; + // content: any +} + +export interface UpdateOpenBitUtxo { + totalUtxo: number, + utxoItems: BlockStreamUtxo[] +} + +export interface BlockStreamUtxo { + txid: string; + vout: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash: string; + block_time?: number; + }, + value: number; +} + +export interface BlockStreamTransactionStatus { + confirmed: boolean; + block_height: number; + block_hash: string; + block_time: number; +} + +export interface BlockStreamFeeEstimates { + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; +} + +export interface RecommendedFeeEstimates { + fastestFee: number, + halfHourFee: number, + hourFee: number, + economyFee: number, + minimumFee: number +} + +export interface BlockStreamTransactionVectorOutput { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; +} + +export interface BlockStreamTransactionVectorInput { + is_coinbase: boolean; + prevout: BlockStreamTransactionVectorOutput; + scriptsig: string; + scriptsig_asm: string; + sequence: number; + txid: string; + vout: number; + witness: string[]; +} + +export interface BlockStreamTransactionDetail { + txid: string; + version: number; + locktime: number; + totalVin: number; + totalVout: number; + size: number; + weight: number; + fee: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; } - \ No newline at end of file + vin: BlockStreamTransactionVectorInput[]; + vout: BlockStreamTransactionVectorOutput[]; +} + +export interface RuneUtxoResponse { + start: number, + total: number, + utxo: RuneUtxo[] +} + +export interface RuneUtxo { + height: number, + confirmations: number, + address: string, + satoshi: number, + scriptPk: string, + txid: string, + vout: number, + runes: RuneInject[] +} + +interface RuneInject { + rune: string, + runeid: string, + spacedRune: string, + amount: string, + symbol: string, + divisibility: number +} + +export interface RuneMetadata { + id: string, + mintable: boolean, + parent: string, + entry: RuneInfo +} + +interface RuneInfo { + block: number, + burned: string, + divisibility: number, + etching: string, + mints: string, + number: number, + premine: string, + spaced_rune: string, + symbol: string, + terms: RuneTerms + timestamp: string, + turbo: boolean +} + +interface RuneTerms { + amount: string, + cap: string, + height: string[], + offset: string[] +} 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 daa89953556..13332ed6cac 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 @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { BitcoinAddressSummaryInfo, Brc20BalanceItem, Inscription, RunesInfoByAddress, RuneTxs, RuneUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { BitcoinAddressSummaryInfo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { ApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy/types'; import { BitcoinFeeInfo, BitcoinTransactionStatus, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; import EventEmitter from 'eventemitter3'; diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 4d2a6d112eb..2a3890f120d 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { AssetLogoMap, AssetRefMap, ChainAssetMap, ChainInfoMap, ChainLogoMap, MultiChainAssetMap } from '@subwallet/chain-list'; -import { _AssetRef, _AssetRefPath, _AssetType, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo, _BitcoinInfo } from '@subwallet/chain-list/types'; +import { _AssetRef, _AssetRefPath, _AssetType, _BitcoinInfo, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo } from '@subwallet/chain-list/types'; import { AssetSetting, MetadataItem, TokenPriorityDetails, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes'; import { _DEFAULT_ACTIVE_CHAINS, _ZK_ASSET_PREFIX, LATEST_CHAIN_DATA_FETCHING_INTERVAL } from '@subwallet/extension-base/services/chain-service/constants'; +import { BitcoinChainHandler } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/BitcoinChainHandler'; import { CardanoChainHandler } from '@subwallet/extension-base/services/chain-service/handler/CardanoChainHandler'; import { EvmChainHandler } from '@subwallet/extension-base/services/chain-service/handler/EvmChainHandler'; import { MantaPrivateHandler } from '@subwallet/extension-base/services/chain-service/handler/manta/MantaPrivateHandler'; @@ -21,7 +22,6 @@ import AssetSettingStore from '@subwallet/extension-base/stores/AssetSetting'; import { addLazy, calculateMetadataHash, fetchStaticData, filterAssetsByChainAndType, getShortMetadata, MODULE_SUPPORT } from '@subwallet/extension-base/utils'; import { BehaviorSubject, Subject } from 'rxjs'; import Web3 from 'web3'; -import { BitcoinChainHandler } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/BitcoinChainHandler'; import { logger as createLogger } from '@polkadot/util/logger'; import { HexString, Logger } from '@polkadot/util/types'; @@ -126,7 +126,7 @@ export class ChainService { this.tonChainHandler = new TonChainHandler(this); this.cardanoChainHandler = new CardanoChainHandler(this); this.bitcoinChainHandler = new BitcoinChainHandler(this); - + this.logger = createLogger('chain-service'); } @@ -218,6 +218,7 @@ export class ChainService { public getBitcoinApiMap () { return this.bitcoinChainHandler.getApiMap(); } + public getTonApi (slug: string) { return this.tonChainHandler.getTonApiByChain(slug); } @@ -1545,7 +1546,7 @@ export class ChainService { let evmInfo: _EvmInfo | null = null; const tonInfo: _TonInfo | null = null; const cardanoInfo: _CardanoInfo | null = null; - let bitcoinInfo: _BitcoinInfo | null = null; + const bitcoinInfo: _BitcoinInfo | null = null; if (params.chainSpec.genesisHash !== '') { substrateInfo = { diff --git a/packages/extension-base/src/services/chain-service/types.ts b/packages/extension-base/src/services/chain-service/types.ts index efc22822cf0..8ab8db81221 100644 --- a/packages/extension-base/src/services/chain-service/types.ts +++ b/packages/extension-base/src/services/chain-service/types.ts @@ -4,10 +4,11 @@ /* eslint @typescript-eslint/no-empty-interface: "off" */ import type { ApiInterfaceRx } from '@polkadot/api/types'; -import { BitcoinApiStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; + import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _CrowdloanFund } from '@subwallet/chain-list/types'; import { CardanoBalanceItem } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/types'; import { AccountState, TxByMsgResponse } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/types'; +import { 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 { TonWalletContract } from '@subwallet/keyring/types'; import { Cell } from '@ton/core'; @@ -255,4 +256,4 @@ export interface OBResponse { status_code: number, message: string, result: T, -} \ No newline at end of file +} diff --git a/packages/extension-base/src/strategy/api-request-strategy/types.ts b/packages/extension-base/src/strategy/api-request-strategy/types.ts index 90f49d7098c..e53f59fbadb 100644 --- a/packages/extension-base/src/strategy/api-request-strategy/types.ts +++ b/packages/extension-base/src/strategy/api-request-strategy/types.ts @@ -2,27 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 export interface ApiRequestContext { - callRate: number; // limit per interval check - limitRate: number; // max rate per interval check - intervalCheck: number; // interval check in ms - maxRetry: number; // interval check in ms - reduceLimitRate: () => void; - } - - export interface ApiRequestStrategy { - addRequest: (run: ApiRequest['run'], ordinal: number) => Promise; - setContext: (context: ApiRequestContext) => void; - stop: () => void; - } - - export interface ApiRequest { - id: number; - retry: number; // retry < 1 not start, retry === 0 start, retry > 0 number of retry - /** Serve smaller first */ - ordinal: number; - status: 'pending' | 'running'; - run: () => Promise; - resolve: (value: any) => T; - reject: (error?: any) => void; - } - \ No newline at end of file + callRate: number; // limit per interval check + limitRate: number; // max rate per interval check + intervalCheck: number; // interval check in ms + maxRetry: number; // interval check in ms + reduceLimitRate: () => void; +} + +export interface ApiRequestStrategy { + addRequest: (run: ApiRequest['run'], ordinal: number) => Promise; + setContext: (context: ApiRequestContext) => void; + stop: () => void; +} + +export interface ApiRequest { + id: number; + retry: number; // retry < 1 not start, retry === 0 start, retry > 0 number of retry + /** Serve smaller first */ + ordinal: number; + status: 'pending' | 'running'; + run: () => Promise; + resolve: (value: any) => T; + reject: (error?: any) => void; +} diff --git a/packages/extension-base/src/types/bitcoin.ts b/packages/extension-base/src/types/bitcoin.ts index f02255673f5..8873948b2b3 100644 --- a/packages/extension-base/src/types/bitcoin.ts +++ b/packages/extension-base/src/types/bitcoin.ts @@ -3,111 +3,110 @@ // https://github.com/leather-wallet/extension/blob/dev/src/app/query/bitcoin/bitcoin-client.ts export interface UtxoResponseItem { - txid: string; - vout: number; - status: { - confirmed: boolean; - block_height?: number; - block_hash?: string; - block_time?: number; - }; - value: number; - } - - // https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts - export interface DetermineUtxosForSpendArgs { - sender: string; - amount: number; - feeRate: number; - recipient: string; - utxos: UtxoResponseItem[]; - } - - interface DetermineUtxosOutput { - value: number; - address?: string; - } - - export interface DetermineUtxosForSpendResult { - filteredUtxos: UtxoResponseItem[]; - inputs: UtxoResponseItem[]; - outputs: DetermineUtxosOutput[], - size: number; - fee: number; - } - - // https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts - export class InsufficientFundsError extends Error { - constructor () { - super('Insufficient funds'); - } - } - // Source: https://github.com/Blockstream/esplora/blob/master/API.md#transaction-format - // --------------- - interface BitcoinTransactionIssuance { - asset_id: string; - is_reissuance: boolean; - asset_blinding_nonce: number; - asset_entropy: number; - contract_hash: string; - assetamount?: number; - assetamountcommitment?: number; - tokenamount?: number; - tokenamountcommitment?: number; - } - - interface BitcoinTransactionPegOut { - genesis_hash: string; - scriptpubkey: string; - scriptpubkey_asm: string; - scriptpubkey_address: string; - } - - export interface BitcoinTransactionStatus { + txid: string; + vout: number; + status: { confirmed: boolean; - block_height?: number | null; - block_hash?: string | null; - block_time?: number | null; - } - - export interface BitcoinTransactionVectorOutput { - scriptpubkey: string; - scriptpubkey_asm: string; - scriptpubkey_type: string; - scriptpubkey_address: string; - value: number; - valuecommitment?: number; - asset?: string; - assetcommitment?: number; - pegout?: BitcoinTransactionPegOut | null; - } - - export interface BitcoinTransactionVectorInput { - inner_redeemscript_asm?: string; - inner_witnessscript_asm?: string; - is_coinbase: boolean; - is_pegin?: boolean; - issuance?: BitcoinTransactionIssuance | null; - prevout: BitcoinTransactionVectorOutput; - scriptsig: string; - scriptsig_asm: string; - sequence: number; - txid: string; - vout: number; - witness: string[]; - } - - export interface BitcoinTx { - fee: number; - locktime: number; - size: number; - status: BitcoinTransactionStatus; - tx_type?: string; - txid: string; - version: number; - vin: BitcoinTransactionVectorInput[]; - vout: BitcoinTransactionVectorOutput[]; - weight: number; + block_height?: number; + block_hash?: string; + block_time?: number; + }; + value: number; +} + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts +export interface DetermineUtxosForSpendArgs { + sender: string; + amount: number; + feeRate: number; + recipient: string; + utxos: UtxoResponseItem[]; +} + +interface DetermineUtxosOutput { + value: number; + address?: string; +} + +export interface DetermineUtxosForSpendResult { + filteredUtxos: UtxoResponseItem[]; + inputs: UtxoResponseItem[]; + outputs: DetermineUtxosOutput[], + size: number; + fee: number; +} + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts +export class InsufficientFundsError extends Error { + constructor () { + super('Insufficient funds'); } - // --------------- - \ No newline at end of file +} +// Source: https://github.com/Blockstream/esplora/blob/master/API.md#transaction-format +// --------------- +interface BitcoinTransactionIssuance { + asset_id: string; + is_reissuance: boolean; + asset_blinding_nonce: number; + asset_entropy: number; + contract_hash: string; + assetamount?: number; + assetamountcommitment?: number; + tokenamount?: number; + tokenamountcommitment?: number; +} + +interface BitcoinTransactionPegOut { + genesis_hash: string; + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_address: string; +} + +export interface BitcoinTransactionStatus { + confirmed: boolean; + block_height?: number | null; + block_hash?: string | null; + block_time?: number | null; +} + +export interface BitcoinTransactionVectorOutput { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; + valuecommitment?: number; + asset?: string; + assetcommitment?: number; + pegout?: BitcoinTransactionPegOut | null; +} + +export interface BitcoinTransactionVectorInput { + inner_redeemscript_asm?: string; + inner_witnessscript_asm?: string; + is_coinbase: boolean; + is_pegin?: boolean; + issuance?: BitcoinTransactionIssuance | null; + prevout: BitcoinTransactionVectorOutput; + scriptsig: string; + scriptsig_asm: string; + sequence: number; + txid: string; + vout: number; + witness: string[]; +} + +export interface BitcoinTx { + fee: number; + locktime: number; + size: number; + status: BitcoinTransactionStatus; + tx_type?: string; + txid: string; + version: number; + vin: BitcoinTransactionVectorInput[]; + vout: BitcoinTransactionVectorOutput[]; + weight: number; +} +// --------------- diff --git a/packages/extension-base/src/types/fee/base.ts b/packages/extension-base/src/types/fee/base.ts index 477b254924a..572fd02ef91 100644 --- a/packages/extension-base/src/types/fee/base.ts +++ b/packages/extension-base/src/types/fee/base.ts @@ -14,4 +14,4 @@ export interface BaseFeeDetail { export interface BaseFeeTime { time: number; // in milliseconds -} \ No newline at end of file +} From fc8af8c2c3cc0e04163f704aa4154c5f5430c52c Mon Sep 17 00:00:00 2001 From: Phong Le Nhat Date: Fri, 4 Apr 2025 11:22:53 +0700 Subject: [PATCH 008/178] Add get Balance BTC through proxy --- packages/extension-base/package.json | 2 +- .../helpers/subscribe/bitcoin.ts | 84 ++-------- .../helpers/subscribe/index.ts | 14 +- .../src/services/balance-service/index.ts | 6 +- .../bitcoin/strategy/BlockStream/index.ts | 151 +----------------- .../src/services/chain-service/index.ts | 10 +- .../src/services/chain-service/utils/index.ts | 10 +- .../api-request-strategy/utils/index.ts | 1 - .../src/utils/bitcoin/utxo-management.ts | 50 ------ yarn.lock | 15 +- 10 files changed, 54 insertions(+), 289 deletions(-) diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json index cf7ef280eb8..447d8e37ec8 100644 --- a/packages/extension-base/package.json +++ b/packages/extension-base/package.json @@ -77,7 +77,7 @@ "bowser": "^2.11.0", "browser-passworder": "^2.0.3", "buffer": "^6.0.3", - "cross-fetch": "^3.1.5", + "cross-fetch": "^4.1.0", "dexie": "^3.2.2", "dexie-export-import": "^4.0.7", "eth-simple-keyring": "^4.2.0", diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index 53f32f3f30a..cc4b62587a9 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -5,29 +5,17 @@ import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; -import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; -// import { filterAssetsByChainAndType, filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; +// import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; +// import { filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { try { - console.log("AAAAAAAAAAAAAAAAAA") - // const [utxos] = await Promise.all([ - // await bitcoinApi.api.getUtxos(address), - // await getRuneUtxos(bitcoinApi, address), - // await getInscriptionUtxos(bitcoinApi, address) - // ]); - - // const response = await fetch(`https://blockstream.info/api/address/${address}/utxo`); - - // if (!response.ok) { - // throw new Error(`HTTP error! Status: ${response.status}`); - // } - - console.log("BITCOIN API: ", await bitcoinApi.api); - const utxos = await bitcoinApi.api.getUtxos(address) - // const utxos = await response.json(); - // console.log('UTXOUTXOUTXO: ', utxos); + // const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ + const [utxos] = await Promise.all([ + await bitcoinApi.api.getUtxos(address), + ]); + // let filteredUtxos: UtxoResponseItem[]; if (!utxos || !utxos.length) { @@ -51,7 +39,7 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre } }; -async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) { +async function getBitcoinBalance(bitcoinApi: _BitcoinApi, addresses: string[]) { return await Promise.all(addresses.map(async (address) => { try { const [filteredUtxos, addressSummaryInfo] = await Promise.all([ @@ -59,18 +47,13 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) bitcoinApi.api.getAddressSummaryInfo(address) ]); - // const filteredUtxos: UtxoResponseItem[] = await getTransferableBitcoinUtxos(bitcoinApi, address); - // const resGetAddrSummaryInfo = await fetch(`https://blockstream.info/api/address/${address}`); - - // const addressSummaryInfo = await resGetAddrSummaryInfo.json(); - const bitcoinBalanceMetadata = { inscriptionCount: addressSummaryInfo.total_inscription } as BitcoinBalanceMetadata; let balanceValue = new BigN(0); - filteredUtxos.forEach((utxo: UtxoResponseItem) => { + filteredUtxos.forEach((utxo) => { balanceValue = balanceValue.plus(utxo.value); }); @@ -91,53 +74,8 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) })); } -export function subscribeBitcoinBalance_Old (addresses: string[], chainInfo: _ChainInfo, assetMap: Record, bitcoinApi: _BitcoinApi, callback: (rs: BalanceItem[]) => void): () => void { - const nativeSlug = _getChainNativeTokenSlug(chainInfo); - - const getBalance = () => { - getBitcoinBalance(bitcoinApi, addresses) - .then((balances) => { - return balances.map(({ balance, bitcoinBalanceMetadata }, index): BalanceItem => { - return { - address: addresses[index], - tokenSlug: nativeSlug, - state: APIItemState.READY, - free: balance, - locked: '0', - metadata: bitcoinBalanceMetadata - }; - }); - }) - .catch((e) => { - console.error(`Error on get Bitcoin balance with token ${nativeSlug}`, e); - - return addresses.map((address): BalanceItem => { - return { - address: address, - tokenSlug: nativeSlug, - state: APIItemState.READY, - free: '0', - locked: '0' - }; - }); - }) - .then((items) => { - callback(items); - }) - .catch(console.error); - }; - - console.log('btc balance: ', getBalance()); - - return () => { - console.log('unsub'); - }; -}; - - -export const subscribeBitcoinBalance = async (addresses: string[]) => { +export const subscribeBitcoinBalance = async (addresses: string[], bitcoinApi: _BitcoinApi) => { - const bitcoinApi = {} as _BitcoinApi; const getBalance = async () => { try { const balances = await getBitcoinBalance(bitcoinApi, addresses); @@ -147,7 +85,7 @@ export const subscribeBitcoinBalance = async (addresses: string[]) => { return '0'; }; } - const balanceBTC = await getBalance(); + const balanceBTC = await getBalance(); console.log('btc balance: ', balanceBTC); return () => { diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index efae2f2e445..d7518a2049d 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -130,6 +130,7 @@ export function subscribeBalance ( evmApiMap: Record, tonApiMap: Record, cardanoApiMap: Record, + bitcoinApiMap: Record, callback: (rs: BalanceItem[]) => void, extrinsicType?: ExtrinsicType ) { @@ -177,7 +178,7 @@ export function subscribeBalance ( } const cardanoApi = cardanoApiMap[chainSlug]; - + if (_isPureCardanoChain(chainInfo)) { return subscribeCardanoBalance({ addresses: useAddresses, @@ -188,6 +189,15 @@ export function subscribeBalance ( }); } + const bitcoinApi = bitcoinApiMap[chainSlug]; + + if (_isPureBitcoinChain(chainInfo)) { + return subscribeBitcoinBalance( + ['bc1q224l0fvnfuf8mdh95hvu6e2gzx6ergugvghht2'], + bitcoinApi, + ); + } + // If the chain is not ready, return pending state if (!substrateApiMap[chainSlug].isApiReady) { handleUnsupportedOrPendingAddresses( @@ -204,8 +214,6 @@ export function subscribeBalance ( return subscribeSubstrateBalance(useAddresses, chainInfo, chainAssetMap, substrateApi, evmApi, callback, extrinsicType); }); - unsubList.push(subscribeBitcoinBalance(['bc1pw4gt62ne4csu74528qjkmv554vwf62dy6erm227qzjjlc2tlfd7qcta9w2'])); - return () => { unsubList.forEach((subProm) => { subProm.then((unsub) => { diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 2e00807589d..bdef2a48f15 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -221,10 +221,11 @@ export class BalanceService implements StoppableServiceInterface { const substrateApiMap = this.state.chainService.getSubstrateApiMap(); const tonApiMap = this.state.chainService.getTonApiMap(); const cardanoApiMap = this.state.chainService.getCardanoApiMap(); + const bitcoinApiMap = this.state.chainService.getBitcoinApiMap(); let unsub = noop; - unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, (result) => { + unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, bitcoinApiMap, (result) => { const rs = result[0]; let value: string; @@ -417,6 +418,7 @@ export class BalanceService implements StoppableServiceInterface { const substrateApiMap = this.state.chainService.getSubstrateApiMap(); const tonApiMap = this.state.chainService.getTonApiMap(); const cardanoApiMap = this.state.chainService.getCardanoApiMap(); + const bitcoinApiMap = this.state.chainService.getBitcoinApiMap(); const activeChainSlugs = Object.keys(this.state.getActiveChainInfoMap()); const assetState = this.state.chainService.subscribeAssetSettings().value; const assets: string[] = Object.values(assetMap) @@ -425,7 +427,7 @@ export class BalanceService implements StoppableServiceInterface { }) .map((asset) => asset.slug); - const unsub = subscribeBalance(addresses, activeChainSlugs, assets, assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, (result) => { + const unsub = subscribeBalance(addresses, activeChainSlugs, assets, assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, bitcoinApiMap, (result) => { !cancel && this.setBalanceItem(result); }, ExtrinsicType.TRANSFER_BALANCE); 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 cd67f514e78..dd792ccef20 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 @@ -6,8 +6,6 @@ import { _BEAR_TOKEN } from '@subwallet/extension-base/services/chain-service/co import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, Brc20BalanceItem, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData, RuneTxs, RuneTxsResponse, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { BitcoinApiStrategy, BitcoinTransactionEventMap } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { OBResponse } from '@subwallet/extension-base/services/chain-service/types'; -// import { HiroService } from '@subwallet/extension-base/services/hiro-service'; -// import { RunesService } from '@subwallet/extension-base/services/rune-service'; import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; import { getRequest, postRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; @@ -25,9 +23,8 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement super(context); - this.baseUrl = 'https://btc-api.koni.studio/'; + this.baseUrl = 'https://btc-api.koni.studio'; this.isTestnet = url.includes('testnet'); - console.log('BlockStreamRequestStrategy.getBlockTime'); this.getBlockTime() .then((rs) => { @@ -40,7 +37,7 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement private headers = { 'Content-Type': 'application/json', - Authorization: `Bearer ${_BEAR_TOKEN}` + 'Authorization': `Bearer ${_BEAR_TOKEN}` }; isRateLimited (): boolean { @@ -245,150 +242,6 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement }, 0); } - // async getRunes (address: string) { - // const runesFullList: RunesInfoByAddress[] = []; - // const pageSize = 60; - // let offset = 0; - - // const runeService = RunesService.getInstance(this.isTestnet); - - // try { - // while (true) { - // const response = await runeService.getAddressRunesInfo(address, { - // limit: String(pageSize), - // offset: String(offset) - // }) as unknown as RunesInfoByAddressFetchedData; - - // const runes = response.runes; - - // if (runes.length !== 0) { - // runesFullList.push(...runes); - // offset += pageSize; - // } else { - // break; - // } - // } - - // return runesFullList; - // } catch (error) { - // console.error(`Failed to get ${address} balances`, error); - // throw error; - // } - // } - - // * Deprecated - // async getRuneTxsUtxos (address: string) { - // const txsFullList: RuneTxs[] = []; - // const pageSize = 10; - // let offset = 0; - - // const runeService = RunesService.getInstance(this.isTestnet); - - // try { - // while (true) { - // const response = await runeService.getAddressRuneTxs(address, { - // limit: String(pageSize), - // offset: String(offset) - // }) as unknown as RuneTxsResponse; - - // let runesTxs: RuneTxs[] = []; - - // if (response.statusCode === 200) { - // runesTxs = response.data.transactions; - // } else { - // console.log(`Error on request rune transactions for address ${address}`); - // break; - // } - - // if (runesTxs.length !== 0) { - // txsFullList.push(...runesTxs); - // offset += pageSize; - // } else { - // break; - // } - // } - - // return txsFullList; - // } catch (error) { - // console.error(`Failed to get ${address} transactions`, error); - // throw error; - // } - // } - - // async getRuneUtxos (address: string) { - // const runeService = RunesService.getInstance(this.isTestnet); - - // try { - // const responseRuneUtxos = await runeService.getAddressRuneUtxos(address); - - // return responseRuneUtxos.utxo; - // } catch (error) { - // console.error(`Failed to get ${address} rune utxos`, error); - // throw error; - // } - // } - - // async getAddressBRC20FreeLockedBalance (address: string, ticker: string): Promise { - // const hiroService = HiroService.getInstance(this.isTestnet); - - // try { - // const response = await hiroService.getAddressBRC20BalanceInfo(address, { - // ticker: String(ticker) - // }); - - // const balanceInfo = response?.results[0]; - - // if (balanceInfo) { - // const rawFree = balanceInfo.transferrable_balance; - // const rawLocked = balanceInfo.available_balance; - - // return { - // free: rawFree.replace('.', ''), - // locked: rawLocked.replace('.', '') - // } as Brc20BalanceItem; - // } - // } catch (error) { - // console.error(`Failed to get ${address} BRC20 balance for ticker ${ticker}`, error); - // } - - // return { - // free: '0', - // locked: '0' - // } as Brc20BalanceItem; - // } - - // async getAddressInscriptions (address: string) { - // const inscriptionsFullList: Inscription[] = []; - // const pageSize = 60; - // let offset = 0; - - // const hiroService = HiroService.getInstance(this.isTestnet); - - // try { - // while (true) { - // const response = await hiroService.getAddressInscriptionsInfo({ - // limit: String(pageSize), - // offset: String(offset), - // address: String(address) - // }) as unknown as InscriptionFetchedData; - - // const inscriptions = response.results; - - // if (inscriptions.length !== 0) { - // inscriptionsFullList.push(...inscriptions); - // offset += pageSize; - // } else { - // break; - // } - // } - - // return inscriptionsFullList; - // } catch (error) { - // console.error(`Failed to get ${address} inscriptions`, error); - // throw error; - // } - // } - getTxHex (txHash: string): Promise { return this.addRequest(async (): Promise => { const _rs = await getRequest(this.getUrl(`tx/${txHash}/hex`), undefined, this.headers); diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 4d2a6d112eb..42c5a4e8aa7 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -12,7 +12,7 @@ import { SubstrateChainHandler } from '@subwallet/extension-base/services/chain- import { TonChainHandler } from '@subwallet/extension-base/services/chain-service/handler/TonChainHandler'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _CUSTOM_PREFIX, _DataMap, _EvmApi, _NetworkUpsertParams, _NFT_CONTRACT_STANDARDS, _SMART_CONTRACT_STANDARDS, _SmartContractTokenInfo, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isPureEvmChain, _isPureSubstrateChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isPureEvmChain, _isPureSubstrateChain, _isPureBitcoinChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { EventService } from '@subwallet/extension-base/services/event-service'; import { MYTHOS_MIGRATION_KEY } from '@subwallet/extension-base/services/migration-service/scripts'; import { IChain, IMetadataItem, IMetadataV15Item } from '@subwallet/extension-base/services/storage-service/databases'; @@ -790,12 +790,18 @@ export class ChainService { const assetSettings = this.assetSettingSubject.value; const chainStateMap = this.getChainStateMap(); - + const chainInfoMap = this.getChainInfoMap(); + for (const asset of autoEnableTokens) { const { originChain, slug: assetSlug } = asset; const assetState = assetSettings[assetSlug]; const chainState = chainStateMap[originChain]; + const chainInfo = chainInfoMap[originChain]; + // todo: will add more condition if there are more networks to support + if (!(chainInfo && (_isPureEvmChain(chainInfo) || _isPureBitcoinChain(chainInfo)))) { + continue; + } if (!assetState) { // If this asset not has asset setting, this token is not enabled before (not turned off before) if (!chainState || !chainState.manualTurnOff) { await this.updateAssetSetting(assetSlug, { visible: true }); diff --git a/packages/extension-base/src/services/chain-service/utils/index.ts b/packages/extension-base/src/services/chain-service/utils/index.ts index 7eb622fd624..679cede69e2 100644 --- a/packages/extension-base/src/services/chain-service/utils/index.ts +++ b/packages/extension-base/src/services/chain-service/utils/index.ts @@ -62,23 +62,23 @@ export function _isEqualSmartContractAsset (asset1: _ChainAsset, asset2: _ChainA } export function _isPureEvmChain (chainInfo: _ChainInfo) { - return (!!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo); + return (!!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureSubstrateChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !!chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo); + return (!chainInfo.evmInfo && !!chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureTonChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.tonInfo && !chainInfo.cardanoInfo); + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.tonInfo && !chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureCardanoChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !!chainInfo.cardanoInfo); + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !!chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureBitcoinChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.bitcoinInfo); + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo && !!chainInfo.bitcoinInfo); } export function _getOriginChainOfAsset (assetSlug: string) { diff --git a/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts b/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts index 0323ce8a451..6d43ba9ec5a 100644 --- a/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts +++ b/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts @@ -14,7 +14,6 @@ export const postRequest = (url: string, body: any, headers?: Record, headers?: Record) => { - console.log('getRequest url: ', url); const queryString = params ? Object.keys(params) .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index 01626c9c3af..0d5c28128dd 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -187,53 +187,3 @@ export function filteredOutTxsUtxos (allTxsUtxos: UtxoResponseItem[], filteredOu return allTxsUtxos.filter((element) => !listFilterOut.includes(`${element.txid}:${element.vout}`)); } - -// export async function getRuneUtxos (bitcoinApi: _BitcoinApi, address: string) { -// const responseRuneUtxos = await bitcoinApi.api.getRuneUtxos(address); -// const runeUtxos: UtxoResponseItem[] = []; - -// responseRuneUtxos.forEach((responseRuneUtxo) => { -// const txid = responseRuneUtxo.txid; -// const vout = responseRuneUtxo.vout; -// const utxoValue = responseRuneUtxo.satoshi; - -// if (txid && vout && utxoValue) { -// const item = { -// txid, -// vout, -// status: { -// confirmed: true // not use in filter out rune utxos -// }, -// value: utxoValue -// } as UtxoResponseItem; - -// runeUtxos.push(item); -// } -// }); - -// return runeUtxos; -// } - -// export async function getInscriptionUtxos (bitcoinApi: _BitcoinApi, address: string) { -// try { -// const inscriptions = await bitcoinApi.api.getAddressInscriptions(address); - -// return inscriptions.map((inscription) => { -// const [txid, vout] = inscription.output.split(':'); - -// return { -// txid, -// vout: parseInt(vout), -// status: { -// confirmed: true, // not use in filter out inscription utxos -// block_height: inscription.genesis_block_height, -// block_hash: inscription.genesis_block_hash, -// block_time: inscription.genesis_timestamp -// }, -// value: parseInt(inscription.value) -// } as UtxoResponseItem; -// }); -// } catch (e) { -// return []; -// } -// } diff --git a/yarn.lock b/yarn.lock index 2cf44f16f0b..15acdb6fe51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6872,7 +6872,7 @@ __metadata: bowser: ^2.11.0 browser-passworder: ^2.0.3 buffer: ^6.0.3 - cross-fetch: ^3.1.5 + cross-fetch: ^4.1.0 dexie: ^3.2.2 dexie-export-import: ^4.0.7 eth-simple-keyring: ^4.2.0 @@ -12999,7 +12999,7 @@ __metadata: languageName: node linkType: hard -"cross-fetch@npm:^3.1.4, cross-fetch@npm:^3.1.5": +"cross-fetch@npm:^3.1.4": version: 3.1.5 resolution: "cross-fetch@npm:3.1.5" dependencies: @@ -13017,6 +13017,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^4.1.0": + version: 4.1.0 + resolution: "cross-fetch@npm:4.1.0" + dependencies: + node-fetch: ^2.7.0 + checksum: c02fa85d59f83e50dbd769ee472c9cc984060c403ee5ec8654659f61a525c1a655eef1c7a35e365c1a107b4e72d76e786718b673d1cb3c97f61d4644cb0a9f9d + languageName: node + linkType: hard + "cross-spawn@npm:^6.0.5": version: 6.0.5 resolution: "cross-spawn@npm:6.0.5" @@ -22330,7 +22339,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.12": +"node-fetch@npm:^2.6.12, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: From 623832b7481f5173290ea8403051a5dc62011985 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 7 Apr 2025 10:31:56 +0700 Subject: [PATCH 009/178] [Issue-4200] refactor: fix eslint issues after merging branch 4162 --- .../helpers/subscribe/bitcoin.ts | 21 ++++++++++--------- .../bitcoin/strategy/BlockStream/index.ts | 2 +- .../src/services/chain-service/index.ts | 5 +++-- packages/extension-base/src/utils/index.ts | 2 +- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index cc4b62587a9..1eb1c79d663 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -1,10 +1,8 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; -import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; // import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; // import { filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; @@ -13,7 +11,7 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre try { // const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ const [utxos] = await Promise.all([ - await bitcoinApi.api.getUtxos(address), + await bitcoinApi.api.getUtxos(address) ]); // let filteredUtxos: UtxoResponseItem[]; @@ -39,7 +37,7 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre } }; -async function getBitcoinBalance(bitcoinApi: _BitcoinApi, addresses: string[]) { +async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) { return await Promise.all(addresses.map(async (address) => { try { const [filteredUtxos, addressSummaryInfo] = await Promise.all([ @@ -75,20 +73,23 @@ async function getBitcoinBalance(bitcoinApi: _BitcoinApi, addresses: string[]) { } export const subscribeBitcoinBalance = async (addresses: string[], bitcoinApi: _BitcoinApi) => { - const getBalance = async () => { try { const balances = await getBitcoinBalance(bitcoinApi, addresses); + return balances[0].balance; } catch (e) { - console.error(`Error on get Bitcoin balance with token`, e); + console.error('Error on get Bitcoin balance with token', e); + return '0'; - }; - } + } + }; + const balanceBTC = await getBalance(); + console.log('btc balance: ', balanceBTC); return () => { console.log('unsub'); }; -}; \ No newline at end of file +}; 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 83abe968bd5..f74bb2ecde1 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 @@ -37,7 +37,7 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement private headers = { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${_BEAR_TOKEN}` + Authorization: `Bearer ${_BEAR_TOKEN}` }; isRateLimited (): boolean { diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 814fca762ff..dc607df04ca 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -13,7 +13,7 @@ import { SubstrateChainHandler } from '@subwallet/extension-base/services/chain- import { TonChainHandler } from '@subwallet/extension-base/services/chain-service/handler/TonChainHandler'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _CUSTOM_PREFIX, _DataMap, _EvmApi, _NetworkUpsertParams, _NFT_CONTRACT_STANDARDS, _SMART_CONTRACT_STANDARDS, _SmartContractTokenInfo, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isPureEvmChain, _isPureSubstrateChain, _isPureBitcoinChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isPureBitcoinChain, _isPureEvmChain, _isPureSubstrateChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { EventService } from '@subwallet/extension-base/services/event-service'; import { MYTHOS_MIGRATION_KEY } from '@subwallet/extension-base/services/migration-service/scripts'; import { IChain, IMetadataItem, IMetadataV15Item } from '@subwallet/extension-base/services/storage-service/databases'; @@ -792,7 +792,7 @@ export class ChainService { const assetSettings = this.assetSettingSubject.value; const chainStateMap = this.getChainStateMap(); const chainInfoMap = this.getChainInfoMap(); - + for (const asset of autoEnableTokens) { const { originChain, slug: assetSlug } = asset; const assetState = assetSettings[assetSlug]; @@ -803,6 +803,7 @@ export class ChainService { if (!(chainInfo && (_isPureEvmChain(chainInfo) || _isPureBitcoinChain(chainInfo)))) { continue; } + if (!assetState) { // If this asset not has asset setting, this token is not enabled before (not turned off before) if (!chainState || !chainState.manualTurnOff) { await this.updateAssetSetting(assetSlug, { visible: true }); diff --git a/packages/extension-base/src/utils/index.ts b/packages/extension-base/src/utils/index.ts index a26bd20f238..98a73eb9192 100644 --- a/packages/extension-base/src/utils/index.ts +++ b/packages/extension-base/src/utils/index.ts @@ -409,4 +409,4 @@ export * from './promise'; export * from './registry'; export * from './swap'; export * from './translate'; -export * from './bitcoin'; \ No newline at end of file +export * from './bitcoin'; From 4a47fad8652aadb75d63b6f2f11ef37b01c401e3 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 7 Apr 2025 17:05:27 +0700 Subject: [PATCH 010/178] [Issue-4200] refactor: move isBitcoin condition to handler hook --- .../hooks/account/useGetAccountChainAddresses.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx index 1d9859550ed..011a7ac091d 100644 --- a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx +++ b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx @@ -15,12 +15,13 @@ import { useMemo } from 'react'; // - order the result // Helper function to create an AccountChainAddress object -const createChainAddress = ( +const createChainAddressItem = ( accountType: KeypairType, chainInfo: _ChainInfo, - address: string, - isBitcoin: boolean + address: string ): AccountChainAddress => { + const isBitcoin = [_BITCOIN_CHAIN_SLUG, _BITCOIN_TESTNET_CHAIN_SLUG].includes(chainInfo.slug); + if (isBitcoin) { const bitcoinInfo = getBitcoinAccountDetails(accountType); @@ -55,15 +56,13 @@ const useGetAccountChainAddresses = (accountProxy: AccountProxy): AccountChainAd const reformatedAddress = getReformatAddress(a, chainInfo); if (reformatedAddress) { - const isBitcoin = [_BITCOIN_CHAIN_SLUG, _BITCOIN_TESTNET_CHAIN_SLUG].includes(chain); - const chainAddress = createChainAddress( + const chainAddressItem = createChainAddressItem( a.type, chainInfo, - reformatedAddress, - isBitcoin + reformatedAddress ); - result.push(chainAddress); + result.push(chainAddressItem); } } }); From 049dbe5b2e5fcd4b1cba25b7825169d4c2ae9b78 Mon Sep 17 00:00:00 2001 From: Phong Le Nhat Date: Fri, 4 Apr 2025 11:22:53 +0700 Subject: [PATCH 011/178] Add get Balance BTC through proxy --- packages/extension-base/package.json | 2 +- .../helpers/subscribe/bitcoin.ts | 84 ++-------- .../helpers/subscribe/index.ts | 16 +- .../src/services/balance-service/index.ts | 6 +- .../bitcoin/strategy/BlockStream/index.ts | 151 +----------------- .../src/services/chain-service/index.ts | 10 +- .../src/services/chain-service/utils/index.ts | 10 +- .../api-request-strategy/utils/index.ts | 1 - .../src/utils/bitcoin/utxo-management.ts | 50 ------ yarn.lock | 15 +- 10 files changed, 55 insertions(+), 290 deletions(-) diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json index cf7ef280eb8..447d8e37ec8 100644 --- a/packages/extension-base/package.json +++ b/packages/extension-base/package.json @@ -77,7 +77,7 @@ "bowser": "^2.11.0", "browser-passworder": "^2.0.3", "buffer": "^6.0.3", - "cross-fetch": "^3.1.5", + "cross-fetch": "^4.1.0", "dexie": "^3.2.2", "dexie-export-import": "^4.0.7", "eth-simple-keyring": "^4.2.0", diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index 53f32f3f30a..cc4b62587a9 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -5,29 +5,17 @@ import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; -import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; -// import { filterAssetsByChainAndType, filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; +// import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; +// import { filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { try { - console.log("AAAAAAAAAAAAAAAAAA") - // const [utxos] = await Promise.all([ - // await bitcoinApi.api.getUtxos(address), - // await getRuneUtxos(bitcoinApi, address), - // await getInscriptionUtxos(bitcoinApi, address) - // ]); - - // const response = await fetch(`https://blockstream.info/api/address/${address}/utxo`); - - // if (!response.ok) { - // throw new Error(`HTTP error! Status: ${response.status}`); - // } - - console.log("BITCOIN API: ", await bitcoinApi.api); - const utxos = await bitcoinApi.api.getUtxos(address) - // const utxos = await response.json(); - // console.log('UTXOUTXOUTXO: ', utxos); + // const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ + const [utxos] = await Promise.all([ + await bitcoinApi.api.getUtxos(address), + ]); + // let filteredUtxos: UtxoResponseItem[]; if (!utxos || !utxos.length) { @@ -51,7 +39,7 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre } }; -async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) { +async function getBitcoinBalance(bitcoinApi: _BitcoinApi, addresses: string[]) { return await Promise.all(addresses.map(async (address) => { try { const [filteredUtxos, addressSummaryInfo] = await Promise.all([ @@ -59,18 +47,13 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) bitcoinApi.api.getAddressSummaryInfo(address) ]); - // const filteredUtxos: UtxoResponseItem[] = await getTransferableBitcoinUtxos(bitcoinApi, address); - // const resGetAddrSummaryInfo = await fetch(`https://blockstream.info/api/address/${address}`); - - // const addressSummaryInfo = await resGetAddrSummaryInfo.json(); - const bitcoinBalanceMetadata = { inscriptionCount: addressSummaryInfo.total_inscription } as BitcoinBalanceMetadata; let balanceValue = new BigN(0); - filteredUtxos.forEach((utxo: UtxoResponseItem) => { + filteredUtxos.forEach((utxo) => { balanceValue = balanceValue.plus(utxo.value); }); @@ -91,53 +74,8 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) })); } -export function subscribeBitcoinBalance_Old (addresses: string[], chainInfo: _ChainInfo, assetMap: Record, bitcoinApi: _BitcoinApi, callback: (rs: BalanceItem[]) => void): () => void { - const nativeSlug = _getChainNativeTokenSlug(chainInfo); - - const getBalance = () => { - getBitcoinBalance(bitcoinApi, addresses) - .then((balances) => { - return balances.map(({ balance, bitcoinBalanceMetadata }, index): BalanceItem => { - return { - address: addresses[index], - tokenSlug: nativeSlug, - state: APIItemState.READY, - free: balance, - locked: '0', - metadata: bitcoinBalanceMetadata - }; - }); - }) - .catch((e) => { - console.error(`Error on get Bitcoin balance with token ${nativeSlug}`, e); - - return addresses.map((address): BalanceItem => { - return { - address: address, - tokenSlug: nativeSlug, - state: APIItemState.READY, - free: '0', - locked: '0' - }; - }); - }) - .then((items) => { - callback(items); - }) - .catch(console.error); - }; - - console.log('btc balance: ', getBalance()); - - return () => { - console.log('unsub'); - }; -}; - - -export const subscribeBitcoinBalance = async (addresses: string[]) => { +export const subscribeBitcoinBalance = async (addresses: string[], bitcoinApi: _BitcoinApi) => { - const bitcoinApi = {} as _BitcoinApi; const getBalance = async () => { try { const balances = await getBitcoinBalance(bitcoinApi, addresses); @@ -147,7 +85,7 @@ export const subscribeBitcoinBalance = async (addresses: string[]) => { return '0'; }; } - const balanceBTC = await getBalance(); + const balanceBTC = await getBalance(); console.log('btc balance: ', balanceBTC); return () => { diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index efae2f2e445..aa324dc6d1b 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -3,9 +3,10 @@ import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; import { APIItemState, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { subscribeBitcoinBalance } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/bitcoin'; import { subscribeCardanoBalance } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano'; import { _BitcoinApi, _CardanoApi, _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain, _isPureBitcoinChain } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureBitcoinChain, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountJson, BalanceItem } from '@subwallet/extension-base/types'; import { filterAssetsByChainAndType, getAddressesByChainTypeMap, pairToAccount } from '@subwallet/extension-base/utils'; import keyring from '@subwallet/ui-keyring'; @@ -13,7 +14,6 @@ import keyring from '@subwallet/ui-keyring'; import { subscribeTonBalance } from './ton/ton'; import { subscribeEVMBalance } from './evm'; import { subscribeSubstrateBalance } from './substrate'; -import {subscribeBitcoinBalance} from "@subwallet/extension-base/services/balance-service/helpers/subscribe/bitcoin"; /** * @function getAccountJsonByAddress @@ -130,6 +130,7 @@ export function subscribeBalance ( evmApiMap: Record, tonApiMap: Record, cardanoApiMap: Record, + bitcoinApiMap: Record, callback: (rs: BalanceItem[]) => void, extrinsicType?: ExtrinsicType ) { @@ -188,6 +189,15 @@ export function subscribeBalance ( }); } + const bitcoinApi = bitcoinApiMap[chainSlug]; + + if (_isPureBitcoinChain(chainInfo)) { + return subscribeBitcoinBalance( + ['bc1p2v22jvkpr4r5shne4t7dczepsnf4tzeq7q743htlkjql9pj4q4hsmw3xte'], + bitcoinApi + ); + } + // If the chain is not ready, return pending state if (!substrateApiMap[chainSlug].isApiReady) { handleUnsupportedOrPendingAddresses( @@ -204,8 +214,6 @@ export function subscribeBalance ( return subscribeSubstrateBalance(useAddresses, chainInfo, chainAssetMap, substrateApi, evmApi, callback, extrinsicType); }); - unsubList.push(subscribeBitcoinBalance(['bc1pw4gt62ne4csu74528qjkmv554vwf62dy6erm227qzjjlc2tlfd7qcta9w2'])); - return () => { unsubList.forEach((subProm) => { subProm.then((unsub) => { diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 2e00807589d..bdef2a48f15 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -221,10 +221,11 @@ export class BalanceService implements StoppableServiceInterface { const substrateApiMap = this.state.chainService.getSubstrateApiMap(); const tonApiMap = this.state.chainService.getTonApiMap(); const cardanoApiMap = this.state.chainService.getCardanoApiMap(); + const bitcoinApiMap = this.state.chainService.getBitcoinApiMap(); let unsub = noop; - unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, (result) => { + unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, bitcoinApiMap, (result) => { const rs = result[0]; let value: string; @@ -417,6 +418,7 @@ export class BalanceService implements StoppableServiceInterface { const substrateApiMap = this.state.chainService.getSubstrateApiMap(); const tonApiMap = this.state.chainService.getTonApiMap(); const cardanoApiMap = this.state.chainService.getCardanoApiMap(); + const bitcoinApiMap = this.state.chainService.getBitcoinApiMap(); const activeChainSlugs = Object.keys(this.state.getActiveChainInfoMap()); const assetState = this.state.chainService.subscribeAssetSettings().value; const assets: string[] = Object.values(assetMap) @@ -425,7 +427,7 @@ export class BalanceService implements StoppableServiceInterface { }) .map((asset) => asset.slug); - const unsub = subscribeBalance(addresses, activeChainSlugs, assets, assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, (result) => { + const unsub = subscribeBalance(addresses, activeChainSlugs, assets, assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, bitcoinApiMap, (result) => { !cancel && this.setBalanceItem(result); }, ExtrinsicType.TRANSFER_BALANCE); 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 cd67f514e78..dd792ccef20 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 @@ -6,8 +6,6 @@ import { _BEAR_TOKEN } from '@subwallet/extension-base/services/chain-service/co import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, Brc20BalanceItem, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData, RuneTxs, RuneTxsResponse, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { BitcoinApiStrategy, BitcoinTransactionEventMap } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { OBResponse } from '@subwallet/extension-base/services/chain-service/types'; -// import { HiroService } from '@subwallet/extension-base/services/hiro-service'; -// import { RunesService } from '@subwallet/extension-base/services/rune-service'; import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; import { getRequest, postRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; @@ -25,9 +23,8 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement super(context); - this.baseUrl = 'https://btc-api.koni.studio/'; + this.baseUrl = 'https://btc-api.koni.studio'; this.isTestnet = url.includes('testnet'); - console.log('BlockStreamRequestStrategy.getBlockTime'); this.getBlockTime() .then((rs) => { @@ -40,7 +37,7 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement private headers = { 'Content-Type': 'application/json', - Authorization: `Bearer ${_BEAR_TOKEN}` + 'Authorization': `Bearer ${_BEAR_TOKEN}` }; isRateLimited (): boolean { @@ -245,150 +242,6 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement }, 0); } - // async getRunes (address: string) { - // const runesFullList: RunesInfoByAddress[] = []; - // const pageSize = 60; - // let offset = 0; - - // const runeService = RunesService.getInstance(this.isTestnet); - - // try { - // while (true) { - // const response = await runeService.getAddressRunesInfo(address, { - // limit: String(pageSize), - // offset: String(offset) - // }) as unknown as RunesInfoByAddressFetchedData; - - // const runes = response.runes; - - // if (runes.length !== 0) { - // runesFullList.push(...runes); - // offset += pageSize; - // } else { - // break; - // } - // } - - // return runesFullList; - // } catch (error) { - // console.error(`Failed to get ${address} balances`, error); - // throw error; - // } - // } - - // * Deprecated - // async getRuneTxsUtxos (address: string) { - // const txsFullList: RuneTxs[] = []; - // const pageSize = 10; - // let offset = 0; - - // const runeService = RunesService.getInstance(this.isTestnet); - - // try { - // while (true) { - // const response = await runeService.getAddressRuneTxs(address, { - // limit: String(pageSize), - // offset: String(offset) - // }) as unknown as RuneTxsResponse; - - // let runesTxs: RuneTxs[] = []; - - // if (response.statusCode === 200) { - // runesTxs = response.data.transactions; - // } else { - // console.log(`Error on request rune transactions for address ${address}`); - // break; - // } - - // if (runesTxs.length !== 0) { - // txsFullList.push(...runesTxs); - // offset += pageSize; - // } else { - // break; - // } - // } - - // return txsFullList; - // } catch (error) { - // console.error(`Failed to get ${address} transactions`, error); - // throw error; - // } - // } - - // async getRuneUtxos (address: string) { - // const runeService = RunesService.getInstance(this.isTestnet); - - // try { - // const responseRuneUtxos = await runeService.getAddressRuneUtxos(address); - - // return responseRuneUtxos.utxo; - // } catch (error) { - // console.error(`Failed to get ${address} rune utxos`, error); - // throw error; - // } - // } - - // async getAddressBRC20FreeLockedBalance (address: string, ticker: string): Promise { - // const hiroService = HiroService.getInstance(this.isTestnet); - - // try { - // const response = await hiroService.getAddressBRC20BalanceInfo(address, { - // ticker: String(ticker) - // }); - - // const balanceInfo = response?.results[0]; - - // if (balanceInfo) { - // const rawFree = balanceInfo.transferrable_balance; - // const rawLocked = balanceInfo.available_balance; - - // return { - // free: rawFree.replace('.', ''), - // locked: rawLocked.replace('.', '') - // } as Brc20BalanceItem; - // } - // } catch (error) { - // console.error(`Failed to get ${address} BRC20 balance for ticker ${ticker}`, error); - // } - - // return { - // free: '0', - // locked: '0' - // } as Brc20BalanceItem; - // } - - // async getAddressInscriptions (address: string) { - // const inscriptionsFullList: Inscription[] = []; - // const pageSize = 60; - // let offset = 0; - - // const hiroService = HiroService.getInstance(this.isTestnet); - - // try { - // while (true) { - // const response = await hiroService.getAddressInscriptionsInfo({ - // limit: String(pageSize), - // offset: String(offset), - // address: String(address) - // }) as unknown as InscriptionFetchedData; - - // const inscriptions = response.results; - - // if (inscriptions.length !== 0) { - // inscriptionsFullList.push(...inscriptions); - // offset += pageSize; - // } else { - // break; - // } - // } - - // return inscriptionsFullList; - // } catch (error) { - // console.error(`Failed to get ${address} inscriptions`, error); - // throw error; - // } - // } - getTxHex (txHash: string): Promise { return this.addRequest(async (): Promise => { const _rs = await getRequest(this.getUrl(`tx/${txHash}/hex`), undefined, this.headers); diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 4d2a6d112eb..42c5a4e8aa7 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -12,7 +12,7 @@ import { SubstrateChainHandler } from '@subwallet/extension-base/services/chain- import { TonChainHandler } from '@subwallet/extension-base/services/chain-service/handler/TonChainHandler'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _CUSTOM_PREFIX, _DataMap, _EvmApi, _NetworkUpsertParams, _NFT_CONTRACT_STANDARDS, _SMART_CONTRACT_STANDARDS, _SmartContractTokenInfo, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isPureEvmChain, _isPureSubstrateChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isPureEvmChain, _isPureSubstrateChain, _isPureBitcoinChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { EventService } from '@subwallet/extension-base/services/event-service'; import { MYTHOS_MIGRATION_KEY } from '@subwallet/extension-base/services/migration-service/scripts'; import { IChain, IMetadataItem, IMetadataV15Item } from '@subwallet/extension-base/services/storage-service/databases'; @@ -790,12 +790,18 @@ export class ChainService { const assetSettings = this.assetSettingSubject.value; const chainStateMap = this.getChainStateMap(); - + const chainInfoMap = this.getChainInfoMap(); + for (const asset of autoEnableTokens) { const { originChain, slug: assetSlug } = asset; const assetState = assetSettings[assetSlug]; const chainState = chainStateMap[originChain]; + const chainInfo = chainInfoMap[originChain]; + // todo: will add more condition if there are more networks to support + if (!(chainInfo && (_isPureEvmChain(chainInfo) || _isPureBitcoinChain(chainInfo)))) { + continue; + } if (!assetState) { // If this asset not has asset setting, this token is not enabled before (not turned off before) if (!chainState || !chainState.manualTurnOff) { await this.updateAssetSetting(assetSlug, { visible: true }); diff --git a/packages/extension-base/src/services/chain-service/utils/index.ts b/packages/extension-base/src/services/chain-service/utils/index.ts index 7eb622fd624..679cede69e2 100644 --- a/packages/extension-base/src/services/chain-service/utils/index.ts +++ b/packages/extension-base/src/services/chain-service/utils/index.ts @@ -62,23 +62,23 @@ export function _isEqualSmartContractAsset (asset1: _ChainAsset, asset2: _ChainA } export function _isPureEvmChain (chainInfo: _ChainInfo) { - return (!!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo); + return (!!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureSubstrateChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !!chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo); + return (!chainInfo.evmInfo && !!chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureTonChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.tonInfo && !chainInfo.cardanoInfo); + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.tonInfo && !chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureCardanoChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !!chainInfo.cardanoInfo); + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !!chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureBitcoinChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.bitcoinInfo); + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo && !!chainInfo.bitcoinInfo); } export function _getOriginChainOfAsset (assetSlug: string) { diff --git a/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts b/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts index 0323ce8a451..6d43ba9ec5a 100644 --- a/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts +++ b/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts @@ -14,7 +14,6 @@ export const postRequest = (url: string, body: any, headers?: Record, headers?: Record) => { - console.log('getRequest url: ', url); const queryString = params ? Object.keys(params) .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index 01626c9c3af..0d5c28128dd 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -187,53 +187,3 @@ export function filteredOutTxsUtxos (allTxsUtxos: UtxoResponseItem[], filteredOu return allTxsUtxos.filter((element) => !listFilterOut.includes(`${element.txid}:${element.vout}`)); } - -// export async function getRuneUtxos (bitcoinApi: _BitcoinApi, address: string) { -// const responseRuneUtxos = await bitcoinApi.api.getRuneUtxos(address); -// const runeUtxos: UtxoResponseItem[] = []; - -// responseRuneUtxos.forEach((responseRuneUtxo) => { -// const txid = responseRuneUtxo.txid; -// const vout = responseRuneUtxo.vout; -// const utxoValue = responseRuneUtxo.satoshi; - -// if (txid && vout && utxoValue) { -// const item = { -// txid, -// vout, -// status: { -// confirmed: true // not use in filter out rune utxos -// }, -// value: utxoValue -// } as UtxoResponseItem; - -// runeUtxos.push(item); -// } -// }); - -// return runeUtxos; -// } - -// export async function getInscriptionUtxos (bitcoinApi: _BitcoinApi, address: string) { -// try { -// const inscriptions = await bitcoinApi.api.getAddressInscriptions(address); - -// return inscriptions.map((inscription) => { -// const [txid, vout] = inscription.output.split(':'); - -// return { -// txid, -// vout: parseInt(vout), -// status: { -// confirmed: true, // not use in filter out inscription utxos -// block_height: inscription.genesis_block_height, -// block_hash: inscription.genesis_block_hash, -// block_time: inscription.genesis_timestamp -// }, -// value: parseInt(inscription.value) -// } as UtxoResponseItem; -// }); -// } catch (e) { -// return []; -// } -// } diff --git a/yarn.lock b/yarn.lock index 2cf44f16f0b..15acdb6fe51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6872,7 +6872,7 @@ __metadata: bowser: ^2.11.0 browser-passworder: ^2.0.3 buffer: ^6.0.3 - cross-fetch: ^3.1.5 + cross-fetch: ^4.1.0 dexie: ^3.2.2 dexie-export-import: ^4.0.7 eth-simple-keyring: ^4.2.0 @@ -12999,7 +12999,7 @@ __metadata: languageName: node linkType: hard -"cross-fetch@npm:^3.1.4, cross-fetch@npm:^3.1.5": +"cross-fetch@npm:^3.1.4": version: 3.1.5 resolution: "cross-fetch@npm:3.1.5" dependencies: @@ -13017,6 +13017,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^4.1.0": + version: 4.1.0 + resolution: "cross-fetch@npm:4.1.0" + dependencies: + node-fetch: ^2.7.0 + checksum: c02fa85d59f83e50dbd769ee472c9cc984060c403ee5ec8654659f61a525c1a655eef1c7a35e365c1a107b4e72d76e786718b673d1cb3c97f61d4644cb0a9f9d + languageName: node + linkType: hard + "cross-spawn@npm:^6.0.5": version: 6.0.5 resolution: "cross-spawn@npm:6.0.5" @@ -22330,7 +22339,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.12": +"node-fetch@npm:^2.6.12, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: From 4e3753796586a7d8c6dbcf7674b12b3927837313 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 7 Apr 2025 17:50:02 +0700 Subject: [PATCH 012/178] [Issue-4200] refactor: fix eslint issues after merging branch 4162 (2) --- .../helpers/subscribe/bitcoin.ts | 19 ++++++++++--------- .../bitcoin/strategy/BlockStream/index.ts | 2 +- .../src/services/chain-service/index.ts | 10 ++++++---- .../src/services/fee-service/service.ts | 3 ++- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index bc30677cf02..1eb1c79d663 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -1,10 +1,8 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; -import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; // import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; // import { filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; @@ -13,7 +11,7 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre try { // const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ const [utxos] = await Promise.all([ - await bitcoinApi.api.getUtxos(address), + await bitcoinApi.api.getUtxos(address) ]); // let filteredUtxos: UtxoResponseItem[]; @@ -39,7 +37,7 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre } }; -async function getBitcoinBalance(bitcoinApi: _BitcoinApi, addresses: string[]) { +async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) { return await Promise.all(addresses.map(async (address) => { try { const [filteredUtxos, addressSummaryInfo] = await Promise.all([ @@ -75,17 +73,20 @@ async function getBitcoinBalance(bitcoinApi: _BitcoinApi, addresses: string[]) { } export const subscribeBitcoinBalance = async (addresses: string[], bitcoinApi: _BitcoinApi) => { - const getBalance = async () => { try { const balances = await getBitcoinBalance(bitcoinApi, addresses); + return balances[0].balance; } catch (e) { - console.error(`Error on get Bitcoin balance with token`, e); + console.error('Error on get Bitcoin balance with token', e); + return '0'; - }; - } + } + }; + const balanceBTC = await getBalance(); + console.log('btc balance: ', balanceBTC); return () => { 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 83abe968bd5..f74bb2ecde1 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 @@ -37,7 +37,7 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement private headers = { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${_BEAR_TOKEN}` + Authorization: `Bearer ${_BEAR_TOKEN}` }; isRateLimited (): boolean { diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index ea1b3013f91..dc607df04ca 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { AssetLogoMap, AssetRefMap, ChainAssetMap, ChainInfoMap, ChainLogoMap, MultiChainAssetMap } from '@subwallet/chain-list'; -import { _AssetRef, _AssetRefPath, _AssetType, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo, _BitcoinInfo } from '@subwallet/chain-list/types'; +import { _AssetRef, _AssetRefPath, _AssetType, _BitcoinInfo, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo } from '@subwallet/chain-list/types'; import { AssetSetting, MetadataItem, TokenPriorityDetails, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes'; import { _DEFAULT_ACTIVE_CHAINS, _ZK_ASSET_PREFIX, LATEST_CHAIN_DATA_FETCHING_INTERVAL } from '@subwallet/extension-base/services/chain-service/constants'; +import { BitcoinChainHandler } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/BitcoinChainHandler'; import { CardanoChainHandler } from '@subwallet/extension-base/services/chain-service/handler/CardanoChainHandler'; import { EvmChainHandler } from '@subwallet/extension-base/services/chain-service/handler/EvmChainHandler'; import { MantaPrivateHandler } from '@subwallet/extension-base/services/chain-service/handler/manta/MantaPrivateHandler'; @@ -12,7 +13,7 @@ import { SubstrateChainHandler } from '@subwallet/extension-base/services/chain- import { TonChainHandler } from '@subwallet/extension-base/services/chain-service/handler/TonChainHandler'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _CUSTOM_PREFIX, _DataMap, _EvmApi, _NetworkUpsertParams, _NFT_CONTRACT_STANDARDS, _SMART_CONTRACT_STANDARDS, _SmartContractTokenInfo, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isPureEvmChain, _isPureSubstrateChain, _isPureBitcoinChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isPureBitcoinChain, _isPureEvmChain, _isPureSubstrateChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { EventService } from '@subwallet/extension-base/services/event-service'; import { MYTHOS_MIGRATION_KEY } from '@subwallet/extension-base/services/migration-service/scripts'; import { IChain, IMetadataItem, IMetadataV15Item } from '@subwallet/extension-base/services/storage-service/databases'; @@ -21,7 +22,6 @@ import AssetSettingStore from '@subwallet/extension-base/stores/AssetSetting'; import { addLazy, calculateMetadataHash, fetchStaticData, filterAssetsByChainAndType, getShortMetadata, MODULE_SUPPORT } from '@subwallet/extension-base/utils'; import { BehaviorSubject, Subject } from 'rxjs'; import Web3 from 'web3'; -import { BitcoinChainHandler } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/BitcoinChainHandler'; import { logger as createLogger } from '@polkadot/util/logger'; import { HexString, Logger } from '@polkadot/util/types'; @@ -218,6 +218,7 @@ export class ChainService { public getBitcoinApiMap () { return this.bitcoinChainHandler.getApiMap(); } + public getTonApi (slug: string) { return this.tonChainHandler.getTonApiByChain(slug); } @@ -802,6 +803,7 @@ export class ChainService { if (!(chainInfo && (_isPureEvmChain(chainInfo) || _isPureBitcoinChain(chainInfo)))) { continue; } + if (!assetState) { // If this asset not has asset setting, this token is not enabled before (not turned off before) if (!chainState || !chainState.manualTurnOff) { await this.updateAssetSetting(assetSlug, { visible: true }); @@ -1551,7 +1553,7 @@ export class ChainService { let evmInfo: _EvmInfo | null = null; const tonInfo: _TonInfo | null = null; const cardanoInfo: _CardanoInfo | null = null; - let bitcoinInfo: _BitcoinInfo | null = null; + const bitcoinInfo: _BitcoinInfo | null = null; if (params.chainSpec.genesisHash !== '') { substrateInfo = { diff --git a/packages/extension-base/src/services/fee-service/service.ts b/packages/extension-base/src/services/fee-service/service.ts index f2336f12665..70ee3ab3782 100644 --- a/packages/extension-base/src/services/fee-service/service.ts +++ b/packages/extension-base/src/services/fee-service/service.ts @@ -17,7 +17,8 @@ export default class FeeService { evm: {}, substrate: {}, ton: {}, - cardano: {} + cardano: {}, + bitcoin: {} }; constructor (state: KoniState) { From 04d9513c5013e5ff2a2ddfb21425fa6f364ad11b Mon Sep 17 00:00:00 2001 From: nampc Date: Thu, 27 Mar 2025 10:31:57 +0700 Subject: [PATCH 013/178] [Issue 4162] feat: init --- .../balance-service/helpers/subscribe/bitcoin.ts | 10 ++++++++++ .../balance-service/helpers/subscribe/index.ts | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts new file mode 100644 index 00000000000..7b4ce4b1865 --- /dev/null +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -0,0 +1,10 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export const subscribeBitcoinBalance = async (address: string[]) => { + console.log('btc balance'); + + return () => { + console.log('unsub'); + }; +}; diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index 71eb63b1953..2811a182b81 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -13,6 +13,7 @@ import keyring from '@subwallet/ui-keyring'; import { subscribeTonBalance } from './ton/ton'; import { subscribeEVMBalance } from './evm'; import { subscribeSubstrateBalance } from './substrate'; +import {subscribeBitcoinBalance} from "@subwallet/extension-base/services/balance-service/helpers/subscribe/bitcoin"; /** * @function getAccountJsonByAddress @@ -203,6 +204,8 @@ export function subscribeBalance ( return subscribeSubstrateBalance(useAddresses, chainInfo, chainAssetMap, substrateApi, evmApi, callback, extrinsicType); }); + unsubList.push(subscribeBitcoinBalance(['bc1p567vvhxrpe28ppdazajpjsgng22sunxlrk0dn3rfuechf2mx828qns8zks'])); + return () => { unsubList.forEach((subProm) => { subProm.then((unsub) => { From 7c33e125ca66d2a8079e80c32a31c144c968e489 Mon Sep 17 00:00:00 2001 From: Phong Le Nhat Date: Wed, 2 Apr 2025 11:28:44 +0700 Subject: [PATCH 014/178] Add config Bitcoin API --- .../src/background/KoniTypes.ts | 3 + .../extension-base/src/constants/bitcoin.ts | 15 + .../extension-base/src/constants/index.ts | 1 + .../helpers/subscribe/bitcoin.ts | 150 ++++++- .../helpers/subscribe/index.ts | 6 +- .../src/services/balance-service/index.ts | 1 - .../src/services/chain-service/constants.ts | 1 + .../handler/bitcoin/BitcoinApi.ts | 120 ++++++ .../handler/bitcoin/BitcoinChainHandler.ts | 90 ++++ .../bitcoin/strategy/BlockStream/index.ts | 404 ++++++++++++++++++ .../bitcoin/strategy/BlockStream/types.ts | 303 +++++++++++++ .../handler/bitcoin/strategy/types.ts | 32 ++ .../src/services/chain-service/index.ts | 30 +- .../src/services/chain-service/types.ts | 19 +- .../src/services/chain-service/utils/index.ts | 4 + .../api-request-strategy/context/base.ts | 30 ++ .../strategy/api-request-strategy/index.ts | 108 +++++ .../strategy/api-request-strategy/types.ts | 28 ++ .../api-request-strategy/utils/index.ts | 32 ++ packages/extension-base/src/types/bitcoin.ts | 113 +++++ packages/extension-base/src/types/fee/base.ts | 6 +- .../extension-base/src/types/fee/bitcoin.ts | 25 ++ .../extension-base/src/types/fee/index.ts | 1 + packages/extension-base/src/types/index.ts | 1 + .../src/utils/bitcoin/common.ts | 65 +++ .../extension-base/src/utils/bitcoin/fee.ts | 14 + .../extension-base/src/utils/bitcoin/index.ts | 6 + .../src/utils/bitcoin/utxo-management.ts | 239 +++++++++++ packages/extension-base/src/utils/index.ts | 1 + 29 files changed, 1836 insertions(+), 12 deletions(-) create mode 100644 packages/extension-base/src/constants/bitcoin.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/index.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/types.ts create mode 100644 packages/extension-base/src/strategy/api-request-strategy/context/base.ts create mode 100644 packages/extension-base/src/strategy/api-request-strategy/index.ts create mode 100644 packages/extension-base/src/strategy/api-request-strategy/types.ts create mode 100644 packages/extension-base/src/strategy/api-request-strategy/utils/index.ts create mode 100644 packages/extension-base/src/types/bitcoin.ts create mode 100644 packages/extension-base/src/types/fee/bitcoin.ts create mode 100644 packages/extension-base/src/utils/bitcoin/common.ts create mode 100644 packages/extension-base/src/utils/bitcoin/fee.ts create mode 100644 packages/extension-base/src/utils/bitcoin/index.ts create mode 100644 packages/extension-base/src/utils/bitcoin/utxo-management.ts diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index f554c1c4c4b..6888498e568 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -2006,6 +2006,9 @@ export interface RequestPingSession { /* Core types */ export type _Address = string; export type _BalanceMetadata = unknown; +export type BitcoinBalanceMetadata = { + inscriptionCount: number +} // Use stringify to communicate, pure boolean value will error with case 'false' value export interface KoniRequestSignatures { diff --git a/packages/extension-base/src/constants/bitcoin.ts b/packages/extension-base/src/constants/bitcoin.ts new file mode 100644 index 00000000000..39771e5a610 --- /dev/null +++ b/packages/extension-base/src/constants/bitcoin.ts @@ -0,0 +1,15 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +// https://bitcoin.stackexchange.com/a/41082/139277 +import { BitcoinAddressType } from '@subwallet/keyring/types'; + +export const BTC_DUST_AMOUNT: Record = { + [BitcoinAddressType.p2pkh]: 546, + [BitcoinAddressType.p2sh]: 540, + [BitcoinAddressType.p2tr]: 330, + [BitcoinAddressType.p2wpkh]: 294, + [BitcoinAddressType.p2wsh]: 330 +}; + +export const BITCOIN_DECIMAL = 8; diff --git a/packages/extension-base/src/constants/index.ts b/packages/extension-base/src/constants/index.ts index d38bc3a48f2..3df08116ba3 100644 --- a/packages/extension-base/src/constants/index.ts +++ b/packages/extension-base/src/constants/index.ts @@ -73,3 +73,4 @@ export * from './signing'; export * from './staking'; export * from './storage'; export * from './remind-notification-time'; +export * from './bitcoin'; diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index 7b4ce4b1865..53f32f3f30a 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -1,10 +1,156 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -export const subscribeBitcoinBalance = async (address: string[]) => { - console.log('btc balance'); +import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; +import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; +import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; +import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; +import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; +// import { filterAssetsByChainAndType, filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; +import BigN from 'bignumber.js'; + +export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { + try { + console.log("AAAAAAAAAAAAAAAAAA") + // const [utxos] = await Promise.all([ + // await bitcoinApi.api.getUtxos(address), + // await getRuneUtxos(bitcoinApi, address), + // await getInscriptionUtxos(bitcoinApi, address) + // ]); + + // const response = await fetch(`https://blockstream.info/api/address/${address}/utxo`); + + // if (!response.ok) { + // throw new Error(`HTTP error! Status: ${response.status}`); + // } + + console.log("BITCOIN API: ", await bitcoinApi.api); + const utxos = await bitcoinApi.api.getUtxos(address) + // const utxos = await response.json(); + // console.log('UTXOUTXOUTXO: ', utxos); + // let filteredUtxos: UtxoResponseItem[]; + + if (!utxos || !utxos.length) { + return []; + } + + // filter out pending utxos + // filteredUtxos = filterOutPendingTxsUtxos(utxos); + + // filter out rune utxos + // filteredUtxos = filteredOutTxsUtxos(utxos, runeTxsUtxos); + + // filter out inscription utxos + // filteredUtxos = filteredOutTxsUtxos(filteredUtxos, inscriptionUtxos); + + return utxos; + } catch (error) { + console.log('Error while fetching Bitcoin balances', error); + + return []; + } +}; + +async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) { + return await Promise.all(addresses.map(async (address) => { + try { + const [filteredUtxos, addressSummaryInfo] = await Promise.all([ + getTransferableBitcoinUtxos(bitcoinApi, address), + bitcoinApi.api.getAddressSummaryInfo(address) + ]); + + // const filteredUtxos: UtxoResponseItem[] = await getTransferableBitcoinUtxos(bitcoinApi, address); + // const resGetAddrSummaryInfo = await fetch(`https://blockstream.info/api/address/${address}`); + + // const addressSummaryInfo = await resGetAddrSummaryInfo.json(); + + const bitcoinBalanceMetadata = { + inscriptionCount: addressSummaryInfo.total_inscription + } as BitcoinBalanceMetadata; + + let balanceValue = new BigN(0); + + filteredUtxos.forEach((utxo: UtxoResponseItem) => { + balanceValue = balanceValue.plus(utxo.value); + }); + + return { + balance: balanceValue.toString(), + bitcoinBalanceMetadata: bitcoinBalanceMetadata + }; + } catch (error) { + console.log('Error while fetching Bitcoin balances', error); + + return { + balance: '0', + bitcoinBalanceMetadata: { + inscriptionCount: 0 + } + }; + } + })); +} + +export function subscribeBitcoinBalance_Old (addresses: string[], chainInfo: _ChainInfo, assetMap: Record, bitcoinApi: _BitcoinApi, callback: (rs: BalanceItem[]) => void): () => void { + const nativeSlug = _getChainNativeTokenSlug(chainInfo); + + const getBalance = () => { + getBitcoinBalance(bitcoinApi, addresses) + .then((balances) => { + return balances.map(({ balance, bitcoinBalanceMetadata }, index): BalanceItem => { + return { + address: addresses[index], + tokenSlug: nativeSlug, + state: APIItemState.READY, + free: balance, + locked: '0', + metadata: bitcoinBalanceMetadata + }; + }); + }) + .catch((e) => { + console.error(`Error on get Bitcoin balance with token ${nativeSlug}`, e); + + return addresses.map((address): BalanceItem => { + return { + address: address, + tokenSlug: nativeSlug, + state: APIItemState.READY, + free: '0', + locked: '0' + }; + }); + }) + .then((items) => { + callback(items); + }) + .catch(console.error); + }; + + console.log('btc balance: ', getBalance()); return () => { console.log('unsub'); }; }; + + +export const subscribeBitcoinBalance = async (addresses: string[]) => { + + const bitcoinApi = {} as _BitcoinApi; + const getBalance = async () => { + try { + const balances = await getBitcoinBalance(bitcoinApi, addresses); + return balances[0].balance; + } catch (e) { + console.error(`Error on get Bitcoin balance with token`, e); + return '0'; + }; + } + const balanceBTC = await getBalance(); + console.log('btc balance: ', balanceBTC); + + return () => { + console.log('unsub'); + }; +}; \ No newline at end of file diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index 2811a182b81..efae2f2e445 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -4,8 +4,8 @@ import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; import { APIItemState, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { subscribeCardanoBalance } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano'; -import { _CardanoApi, _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain } from '@subwallet/extension-base/services/chain-service/utils'; +import { _BitcoinApi, _CardanoApi, _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; +import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain, _isPureBitcoinChain } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountJson, BalanceItem } from '@subwallet/extension-base/types'; import { filterAssetsByChainAndType, getAddressesByChainTypeMap, pairToAccount } from '@subwallet/extension-base/utils'; import keyring from '@subwallet/ui-keyring'; @@ -204,7 +204,7 @@ export function subscribeBalance ( return subscribeSubstrateBalance(useAddresses, chainInfo, chainAssetMap, substrateApi, evmApi, callback, extrinsicType); }); - unsubList.push(subscribeBitcoinBalance(['bc1p567vvhxrpe28ppdazajpjsgng22sunxlrk0dn3rfuechf2mx828qns8zks'])); + unsubList.push(subscribeBitcoinBalance(['bc1pw4gt62ne4csu74528qjkmv554vwf62dy6erm227qzjjlc2tlfd7qcta9w2'])); return () => { unsubList.forEach((subProm) => { diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 6bfff019342..1a5d55f7424 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -417,7 +417,6 @@ export class BalanceService implements StoppableServiceInterface { const substrateApiMap = this.state.chainService.getSubstrateApiMap(); const tonApiMap = this.state.chainService.getTonApiMap(); const cardanoApiMap = this.state.chainService.getCardanoApiMap(); - const activeChainSlugs = Object.keys(this.state.getActiveChainInfoMap()); const assetState = this.state.chainService.subscribeAssetSettings().value; const assets: string[] = Object.values(assetMap) diff --git a/packages/extension-base/src/services/chain-service/constants.ts b/packages/extension-base/src/services/chain-service/constants.ts index d42cda152cd..9af4aba288d 100644 --- a/packages/extension-base/src/services/chain-service/constants.ts +++ b/packages/extension-base/src/services/chain-service/constants.ts @@ -303,3 +303,4 @@ export const _ASSET_REF_SRC = `https://raw.githubusercontent.com/Koniverse/SubWa export const _MULTI_CHAIN_ASSET_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/MultiChainAsset.json`; export const _CHAIN_LOGO_MAP_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/ChainLogoMap.json`; export const _ASSET_LOGO_MAP_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/AssetLogoMap.json`; +export const _BEAR_TOKEN = "aHR0cHM6Ly9xdWFuZ3RydW5nLXNvZnR3YXJlLnZuL2FwaS9tYXN0ZXIvYXBpLXB1YmxpYw=="; \ No newline at end of file diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts new file mode 100644 index 00000000000..d51625f8d8b --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts @@ -0,0 +1,120 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import '@polkadot/types-augment'; + +import { BlockStreamRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream'; +import { BitcoinApiStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; +import { createPromiseHandler, PromiseHandler } from '@subwallet/extension-base/utils/promise'; +import { BehaviorSubject } from 'rxjs'; + +import { _ApiOptions } from '../../handler/types'; +import { _BitcoinApi, _ChainConnectionStatus } from '../../types'; + +// const isBlockStreamProvider = (apiUrl: string): boolean => apiUrl === 'https://blockstream-testnet.openbit.app' || apiUrl === 'https://electrs.openbit.app'; + +export class BitcoinApi implements _BitcoinApi { + chainSlug: string; + apiUrl: string; + apiError?: string; + apiRetry = 0; + public readonly isApiConnectedSubject = new BehaviorSubject(false); + public readonly connectionStatusSubject = new BehaviorSubject(_ChainConnectionStatus.DISCONNECTED); + isApiReady = false; + isApiReadyOnce = false; + isReadyHandler: PromiseHandler<_BitcoinApi>; + + providerName: string; + api: BitcoinApiStrategy; + + constructor (chainSlug: string, apiUrl: string, { providerName }: _ApiOptions = {}) { + this.chainSlug = chainSlug; + this.apiUrl = apiUrl; + this.providerName = providerName || 'unknown'; + this.isReadyHandler = createPromiseHandler<_BitcoinApi>(); + this.api = new BlockStreamRequestStrategy(apiUrl); + + this.connect(); + } + + get isApiConnected (): boolean { + return this.isApiConnectedSubject.getValue(); + } + + get connectionStatus (): _ChainConnectionStatus { + return this.connectionStatusSubject.getValue(); + } + + private updateConnectionStatus (status: _ChainConnectionStatus): void { + const isConnected = status === _ChainConnectionStatus.CONNECTED; + + if (isConnected !== this.isApiConnectedSubject.value) { + this.isApiConnectedSubject.next(isConnected); + } + + if (status !== this.connectionStatusSubject.value) { + this.connectionStatusSubject.next(status); + } + } + + get isReady (): Promise<_BitcoinApi> { + return this.isReadyHandler.promise; + } + + async updateApiUrl (apiUrl: string) { + if (this.apiUrl === apiUrl) { + return; + } + + await this.disconnect(); + this.apiUrl = apiUrl; + this.api = new BlockStreamRequestStrategy(apiUrl); + this.connect(); + } + + async recoverConnect () { + await this.isReadyHandler.promise; + } + + connect (): void { + this.updateConnectionStatus(_ChainConnectionStatus.CONNECTING); + + this.onConnect(); + } + + async disconnect () { + this.api.stop(); + this.onDisconnect(); + + this.updateConnectionStatus(_ChainConnectionStatus.DISCONNECTED); + + return Promise.resolve(); + } + + destroy () { + return this.disconnect(); + } + + onConnect (): void { + if (!this.isApiConnected) { + console.log(`Connected to ${this.chainSlug} at ${this.apiUrl}`); + this.isApiReady = true; + + if (this.isApiReadyOnce) { + this.isReadyHandler.resolve(this); + } + } + + this.updateConnectionStatus(_ChainConnectionStatus.CONNECTED); + } + + onDisconnect (): void { + this.updateConnectionStatus(_ChainConnectionStatus.DISCONNECTED); + + if (this.isApiConnected) { + console.warn(`Disconnected from ${this.chainSlug} of ${this.apiUrl}`); + this.isApiReady = false; + this.isReadyHandler = createPromiseHandler<_BitcoinApi>(); + } + } +} diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts new file mode 100644 index 00000000000..31b4473e797 --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts @@ -0,0 +1,90 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ChainService } from '@subwallet/extension-base/services/chain-service/index'; + +import { AbstractChainHandler } from '../AbstractChainHandler'; +import { _ApiOptions } from '../types'; +import { BitcoinApi } from './BitcoinApi'; + +export class BitcoinChainHandler extends AbstractChainHandler { + private apiMap: Record = {}; + + // eslint-disable-next-line no-useless-constructor + constructor (parent?: ChainService) { + super(parent); + } + + public getApiMap () { + return this.apiMap; + } + + public getApiByChain (chain: string) { + return this.apiMap[chain]; + } + + public setApi (chainSlug: string, api: BitcoinApi) { + this.apiMap[chainSlug] = api; + } + + public async initApi (chainSlug: string, apiUrl: string, { onUpdateStatus, providerName }: Omit<_ApiOptions, 'metadata'> = {}) { + const existed = this.getApiByChain(chainSlug); + + if (existed) { + existed.connect(); + + if (apiUrl !== existed.apiUrl) { + existed.updateApiUrl(apiUrl).catch(console.error); + } + + return existed; + } + + const apiObject = new BitcoinApi(chainSlug, apiUrl, { providerName }); + + apiObject.connectionStatusSubject.subscribe(this.handleConnection.bind(this, chainSlug)); + apiObject.connectionStatusSubject.subscribe(onUpdateStatus); + + return Promise.resolve(apiObject); + } + + public async recoverApi (chainSlug: string): Promise { + const existed = this.getApiByChain(chainSlug); + + if (existed && !existed.isApiReadyOnce) { + console.log(`Reconnect ${existed.providerName || existed.chainSlug} at ${existed.apiUrl}`); + + return existed.recoverConnect(); + } + } + + destroyApi (chain: string) { + const api = this.getApiByChain(chain); + + api?.destroy().catch(console.error); + } + + async sleep () { + this.isSleeping = true; + this.cancelAllRecover(); + + await Promise.all(Object.values(this.getApiMap()).map((evmApi) => { + return evmApi.disconnect().catch(console.error); + })); + + return Promise.resolve(); + } + + wakeUp () { + this.isSleeping = false; + const activeChains = this.parent?.getActiveChains() || []; + + for (const chain of activeChains) { + const api = this.getApiByChain(chain); + + api?.connect(); + } + + return Promise.resolve(); + } +} 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 new file mode 100644 index 00000000000..cd67f514e78 --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/index.ts @@ -0,0 +1,404 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { SWError } from '@subwallet/extension-base/background/errors/SWError'; +import { _BEAR_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; +import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, Brc20BalanceItem, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData, RuneTxs, RuneTxsResponse, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { BitcoinApiStrategy, BitcoinTransactionEventMap } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; +import { OBResponse } from '@subwallet/extension-base/services/chain-service/types'; +// import { HiroService } from '@subwallet/extension-base/services/hiro-service'; +// import { RunesService } from '@subwallet/extension-base/services/rune-service'; +import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; +import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; +import { getRequest, postRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; +import { BitcoinFeeInfo, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; +import BigN from 'bignumber.js'; +import EventEmitter from 'eventemitter3'; + +export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implements BitcoinApiStrategy { + private readonly baseUrl: string; + private readonly isTestnet: boolean; + private timePerBlock = 0; // in milliseconds + + constructor (url: string) { + const context = new BaseApiRequestContext(); + + super(context); + + this.baseUrl = 'https://btc-api.koni.studio/'; + this.isTestnet = url.includes('testnet'); + console.log('BlockStreamRequestStrategy.getBlockTime'); + + this.getBlockTime() + .then((rs) => { + this.timePerBlock = rs; + }) + .catch(() => { + this.timePerBlock = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; + }); + } + + private headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${_BEAR_TOKEN}` + }; + + isRateLimited (): boolean { + return false; + } + + getUrl (path: string): string { + return `${this.baseUrl}/${path}`; + } + + getBlockTime (): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl('blocks'), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getBlockTime', rs.message); + } + + const blocks = rs.result; + const length = blocks.length; + const sortedBlocks = blocks.sort((a, b) => b.timestamp - a.timestamp); + const time = (sortedBlocks[0].timestamp - sortedBlocks[length - 1].timestamp) * 1000; + + return time / length; + }, 0); + } + + getAddressSummaryInfo (address: string): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`address/${address}`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getAddressSummaryInfo', rs.message); + } + + return rs.result; + }, 0); + } + + getAddressTransaction (address: string, limit = 100): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`address/${address}/txs`), { limit: `${limit}` }, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getAddressTransaction', rs.message); + } + + return rs.result; + }, 1); + } + + getTransactionStatus (txHash: string): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`tx/${txHash}/status`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getTransactionStatus', rs.message); + } + + return rs.result; + }, 1); + } + + getTransactionDetail (txHash: string): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`tx/${txHash}`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getTransactionDetail', rs.message); + } + + return rs.result; + }, 1); + } + + getFeeRate (): Promise { + return this.addRequest(async (): Promise => { + const _rs = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getFeeRate', rs.message); + } + + const result = rs.result; + + const low = 6; + const average = 3; + const fast = 1; + + const convertFee = (fee: number) => parseFloat(new BigN(fee).toFixed(2)); + + return { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: convertFee(result[low]), time: this.timePerBlock * low }, + average: { feeRate: convertFee(result[average]), time: this.timePerBlock * average }, + fast: { feeRate: convertFee(result[fast]), time: this.timePerBlock * fast }, + default: 'slow' + } + }; + }, 0); + } + + getRecommendedFeeRate (): Promise { + return this.addRequest(async (): Promise => { + const _rs = await getRequest(this.getUrl('fee-estimates/recommended'), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getRecommendedFeeRate', rs.message); + } + + const result = rs.result; + + const convertTimeMilisec = { + fastestFee: 10 * 60000, + halfHourFee: 30 * 60000, + hourFee: 60 * 60000 + }; + + const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed()); + + return { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: convertFee(result.hourFee), time: convertTimeMilisec.hourFee }, + average: { feeRate: convertFee(result.halfHourFee), time: convertTimeMilisec.halfHourFee }, + fast: { feeRate: convertFee(result.fastestFee), time: convertTimeMilisec.fastestFee }, + default: 'slow' + } + }; + }, 0); + } + + getUtxos (address: string): Promise { + return this.addRequest(async (): Promise => { + const _rs = await getRequest(this.getUrl(`address/${address}/utxo`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getUtxos', rs.message); + } + + return rs.result.utxoItems; + }, 0); + } + + sendRawTransaction (rawTransaction: string) { + const eventEmitter = new EventEmitter(); + + 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.sendRawTransaction', rs.message); + } + + return rs.result; + }, 0) + .then((extrinsicHash) => { + eventEmitter.emit('extrinsicHash', extrinsicHash); + + // Check transaction status + const interval = setInterval(() => { + this.getTransactionStatus(extrinsicHash) + .then((transactionStatus) => { + if (transactionStatus.confirmed && transactionStatus.block_time > 0) { + clearInterval(interval); + eventEmitter.emit('success', transactionStatus); + } + }) + .catch(console.error); + }, 30000); + }) + .catch((error: Error) => { + eventEmitter.emit('error', error.message); + }) + ; + + 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; + // let offset = 0; + + // const runeService = RunesService.getInstance(this.isTestnet); + + // try { + // while (true) { + // const response = await runeService.getAddressRunesInfo(address, { + // limit: String(pageSize), + // offset: String(offset) + // }) as unknown as RunesInfoByAddressFetchedData; + + // const runes = response.runes; + + // if (runes.length !== 0) { + // runesFullList.push(...runes); + // offset += pageSize; + // } else { + // break; + // } + // } + + // return runesFullList; + // } catch (error) { + // console.error(`Failed to get ${address} balances`, error); + // throw error; + // } + // } + + // * Deprecated + // async getRuneTxsUtxos (address: string) { + // const txsFullList: RuneTxs[] = []; + // const pageSize = 10; + // let offset = 0; + + // const runeService = RunesService.getInstance(this.isTestnet); + + // try { + // while (true) { + // const response = await runeService.getAddressRuneTxs(address, { + // limit: String(pageSize), + // offset: String(offset) + // }) as unknown as RuneTxsResponse; + + // let runesTxs: RuneTxs[] = []; + + // if (response.statusCode === 200) { + // runesTxs = response.data.transactions; + // } else { + // console.log(`Error on request rune transactions for address ${address}`); + // break; + // } + + // if (runesTxs.length !== 0) { + // txsFullList.push(...runesTxs); + // offset += pageSize; + // } else { + // break; + // } + // } + + // return txsFullList; + // } catch (error) { + // console.error(`Failed to get ${address} transactions`, error); + // throw error; + // } + // } + + // async getRuneUtxos (address: string) { + // const runeService = RunesService.getInstance(this.isTestnet); + + // try { + // const responseRuneUtxos = await runeService.getAddressRuneUtxos(address); + + // return responseRuneUtxos.utxo; + // } catch (error) { + // console.error(`Failed to get ${address} rune utxos`, error); + // throw error; + // } + // } + + // async getAddressBRC20FreeLockedBalance (address: string, ticker: string): Promise { + // const hiroService = HiroService.getInstance(this.isTestnet); + + // try { + // const response = await hiroService.getAddressBRC20BalanceInfo(address, { + // ticker: String(ticker) + // }); + + // const balanceInfo = response?.results[0]; + + // if (balanceInfo) { + // const rawFree = balanceInfo.transferrable_balance; + // const rawLocked = balanceInfo.available_balance; + + // return { + // free: rawFree.replace('.', ''), + // locked: rawLocked.replace('.', '') + // } as Brc20BalanceItem; + // } + // } catch (error) { + // console.error(`Failed to get ${address} BRC20 balance for ticker ${ticker}`, error); + // } + + // return { + // free: '0', + // locked: '0' + // } as Brc20BalanceItem; + // } + + // async getAddressInscriptions (address: string) { + // const inscriptionsFullList: Inscription[] = []; + // const pageSize = 60; + // let offset = 0; + + // const hiroService = HiroService.getInstance(this.isTestnet); + + // try { + // while (true) { + // const response = await hiroService.getAddressInscriptionsInfo({ + // limit: String(pageSize), + // offset: String(offset), + // address: String(address) + // }) as unknown as InscriptionFetchedData; + + // const inscriptions = response.results; + + // if (inscriptions.length !== 0) { + // inscriptionsFullList.push(...inscriptions); + // offset += pageSize; + // } else { + // break; + // } + // } + + // return inscriptionsFullList; + // } catch (error) { + // console.error(`Failed to get ${address} inscriptions`, error); + // throw error; + // } + // } + + getTxHex (txHash: string): Promise { + return this.addRequest(async (): Promise => { + const _rs = await getRequest(this.getUrl(`tx/${txHash}/hex`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('BlockStreamRequestStrategy.getTxHex', rs.message); + } + + return rs.result; + }, 0); + } +} diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts new file mode 100644 index 00000000000..006b02d7100 --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts @@ -0,0 +1,303 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export interface BlockStreamBlock { + id: string; + height: number; + version: number; + timestamp: number; + tx_count: number; + size: number; + weight: number; + merkle_root: string; + previousblockhash: string; + mediantime: number; + nonce: number; + bits: number; + difficulty: number; + } + + export interface BitcoinAddressSummaryInfo { + address: string, + chain_stats: { + funded_txo_count: number, + funded_txo_sum: number, + spent_txo_count: number, + spent_txo_sum: number, + tx_count: number + }, + mempool_stats: { + funded_txo_count: number, + funded_txo_sum: number, + spent_txo_count: number, + spent_txo_sum: number, + tx_count: number + }, + balance: number, + total_inscription: number + } + + // todo: combine RunesByAddressResponse & RunesCollectionInfoResponse + + export interface RunesInfoByAddressResponse { + statusCode: number, + data: RunesInfoByAddressFetchedData + } + + export interface RunesInfoByAddressFetchedData { + limit: number, + offset: number, + total: number, + runes: RunesInfoByAddress[] + } + + // todo: check is_hot and turbo and cenotaph attributes meaning in RuneInfoByAddress + + export interface RunesInfoByAddress { + amount: string, + address: string, + rune_id: string, + rune: { + rune: string, + rune_name: string, + divisibility: number, + premine: string, + spacers: string, + symbol: string + } + } + + export interface RunesCollectionInfoResponse { + statusCode: number, + data: RunesCollectionInfoFetchedData + } + + interface RunesCollectionInfoFetchedData { + limit: number, + offset: number, + total: number, + runes: RunesCollectionInfo[] + } + + export interface RunesCollectionInfo { + rune_id: string, + rune: string, + rune_name: string, + divisibility: string, + spacers: string + } + + export interface RuneTxsResponse { + statusCode: number, + data: RuneTxsFetchedData + } + + interface RuneTxsFetchedData { + limit: number, + offset: number, + total: number, + transactions: RuneTxs[] + } + + export interface RuneTxs { + txid: string, + vout: RuneTxsUtxosVout[] + } + + interface RuneTxsUtxosVout { + n: number, + value: number, + runeInject: any + } + + export interface Brc20MetadataFetchedData { + token: Brc20Metadata + } + + export interface Brc20Metadata { + ticker: string, + decimals: number + } + + export interface Brc20BalanceFetchedData { + limit: number, + offset: number, + total: number, + results: Brc20Balance[] + } + + export interface Brc20Balance { + ticker: string, + available_balance: string, + transferrable_balance: string, + overall_balance: string + } + + export interface Brc20BalanceItem { + free: string, + locked: string + } + + export interface InscriptionFetchedData { + limit: number, + offset: number, + total: number, + results: Inscription[] + } + + export interface Inscription { + id: string; + number: number; + address: string; + genesis_block_height: number; + genesis_block_hash: string; + genesis_timestamp: number; + tx_id: string; + location: string; + output: string; + value: string; + offset: string; + fee: number; + sat_ordinal: string; + sat_rarity: string; + content_type: string; + content_length: number; + // content: any + } + + export interface UpdateOpenBitUtxo { + totalUtxo: number, + utxoItems: BlockStreamUtxo[] + } + + export interface BlockStreamUtxo { + txid: string; + vout: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash: string; + block_time?: number; + }, + value: number; + } + + export interface BlockStreamTransactionStatus { + confirmed: boolean; + block_height: number; + block_hash: string; + block_time: number; + } + + export interface BlockStreamFeeEstimates { + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + } + + export interface RecommendedFeeEstimates { + fastestFee: number, + halfHourFee: number, + hourFee: number, + economyFee: number, + minimumFee: number + } + + export interface BlockStreamTransactionVectorOutput { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; + } + + export interface BlockStreamTransactionVectorInput { + is_coinbase: boolean; + prevout: BlockStreamTransactionVectorOutput; + scriptsig: string; + scriptsig_asm: string; + sequence: number; + txid: string; + vout: number; + witness: string[]; + } + + export interface BlockStreamTransactionDetail { + txid: string; + version: number; + locktime: number; + totalVin: number; + totalVout: number; + size: number; + weight: number; + fee: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; + } + vin: BlockStreamTransactionVectorInput[]; + vout: BlockStreamTransactionVectorOutput[]; + } + + export interface RuneUtxoResponse { + start: number, + total: number, + utxo: RuneUtxo[] + } + + export interface RuneUtxo { + height: number, + confirmations: number, + address: string, + satoshi: number, + scriptPk: string, + txid: string, + vout: number, + runes: RuneInject[] + } + + interface RuneInject { + rune: string, + runeid: string, + spacedRune: string, + amount: string, + symbol: string, + divisibility: number + } + + export interface RuneMetadata { + id: string, + mintable: boolean, + parent: string, + entry: RuneInfo + } + + interface RuneInfo { + block: number, + burned: string, + divisibility: number, + etching: string, + mints: string, + number: number, + premine: string, + spaced_rune: string, + symbol: string, + terms: RuneTerms + timestamp: string, + turbo: boolean + } + + interface RuneTerms { + amount: string, + cap: string, + height: string[], + offset: string[] + } + \ No newline at end of file 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 new file mode 100644 index 00000000000..daa89953556 --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/types.ts @@ -0,0 +1,32 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { BitcoinAddressSummaryInfo, Brc20BalanceItem, Inscription, RunesInfoByAddress, RuneTxs, RuneUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { ApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy/types'; +import { BitcoinFeeInfo, BitcoinTransactionStatus, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; +import EventEmitter from 'eventemitter3'; + +export interface BitcoinApiStrategy extends Omit { + getBlockTime (): Promise; + getAddressSummaryInfo (address: string): Promise; + // getRunes (address: string): Promise; + // getRuneTxsUtxos (address: string): Promise; // noted: all rune utxos come in account + // getRuneUtxos (address: string): Promise; + // getAddressBRC20FreeLockedBalance (address: string, ticker: string): Promise; + // getAddressInscriptions (address: string): Promise + getAddressTransaction (address: string, limit?: number): Promise; + getTransactionStatus (txHash: string): Promise; + getTransactionDetail (txHash: string): Promise; + getFeeRate (): Promise; + getRecommendedFeeRate (): Promise; + getUtxos (address: string): Promise; + getTxHex (txHash: string): Promise; + sendRawTransaction (rawTransaction: string): EventEmitter; + simpleSendRawTransaction (rawTransaction: string): Promise; +} + +export interface BitcoinTransactionEventMap { + extrinsicHash: (txHash: string) => void; + error: (error: string) => void; + success: (data: BitcoinTransactionStatus) => void; +} diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 3417955a01d..484b911c4d8 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AssetLogoMap, AssetRefMap, ChainAssetMap, ChainInfoMap, ChainLogoMap, MultiChainAssetMap } from '@subwallet/chain-list'; -import { _AssetRef, _AssetRefPath, _AssetType, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo } from '@subwallet/chain-list/types'; +import { _AssetRef, _AssetRefPath, _AssetType, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo, _BitcoinInfo } from '@subwallet/chain-list/types'; import { AssetSetting, MetadataItem, TokenPriorityDetails, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes'; import { _DEFAULT_ACTIVE_CHAINS, _ZK_ASSET_PREFIX, LATEST_CHAIN_DATA_FETCHING_INTERVAL } from '@subwallet/extension-base/services/chain-service/constants'; import { CardanoChainHandler } from '@subwallet/extension-base/services/chain-service/handler/CardanoChainHandler'; @@ -21,6 +21,7 @@ import AssetSettingStore from '@subwallet/extension-base/stores/AssetSetting'; import { addLazy, calculateMetadataHash, fetchStaticData, filterAssetsByChainAndType, getShortMetadata, MODULE_SUPPORT } from '@subwallet/extension-base/utils'; import { BehaviorSubject, Subject } from 'rxjs'; import Web3 from 'web3'; +import { BitcoinChainHandler } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/BitcoinChainHandler'; import { logger as createLogger } from '@polkadot/util/logger'; import { HexString, Logger } from '@polkadot/util/types'; @@ -29,9 +30,10 @@ import { ExtraInfo } from '@polkadot-api/merkleize-metadata'; const filterChainInfoMap = (data: Record, ignoredChains: string[]): Record => { return Object.fromEntries( Object.entries(data) - .filter(([slug, info]) => !info.bitcoinInfo && !ignoredChains.includes(slug)) + .filter(([slug, info]) => !ignoredChains.includes(slug)) ); }; +// .filter(([slug, info]) => !info.bitcoinInfo && !ignoredChains.includes(slug)) const ignoredList = [ 'bevm', @@ -74,6 +76,7 @@ export class ChainService { private substrateChainHandler: SubstrateChainHandler; private evmChainHandler: EvmChainHandler; + private bitcoinChainHandler: BitcoinChainHandler; private tonChainHandler: TonChainHandler; private cardanoChainHandler: CardanoChainHandler; private mantaChainHandler: MantaPrivateHandler | undefined; @@ -122,7 +125,8 @@ export class ChainService { this.evmChainHandler = new EvmChainHandler(this); this.tonChainHandler = new TonChainHandler(this); this.cardanoChainHandler = new CardanoChainHandler(this); - + this.bitcoinChainHandler = new BitcoinChainHandler(this); + this.logger = createLogger('chain-service'); } @@ -207,6 +211,13 @@ export class ChainService { return this.substrateChainHandler.getSubstrateApiMap(); } + public getBitcoinApi (slug: string) { + return this.bitcoinChainHandler.getApiByChain(slug); + } + + public getBitcoinApiMap () { + return this.bitcoinChainHandler.getApiMap(); + } public getTonApi (slug: string) { return this.tonChainHandler.getTonApiByChain(slug); } @@ -951,6 +962,12 @@ export class ChainService { this.cardanoChainHandler.setCardanoApi(chainInfo.slug, chainApi); } + + if (chainInfo.bitcoinInfo !== null && chainInfo.bitcoinInfo !== undefined) { + const chainApi = await this.bitcoinChainHandler.initApi(chainInfo.slug, endpoint, { providerName, onUpdateStatus }); + + this.bitcoinChainHandler.setApi(chainInfo.slug, chainApi); + } } private destroyApiForChain (chainInfo: _ChainInfo) { @@ -969,6 +986,10 @@ export class ChainService { if (chainInfo.cardanoInfo !== null) { this.cardanoChainHandler.destroyCardanoApi(chainInfo.slug); } + + if (chainInfo.bitcoinInfo !== null && chainInfo.bitcoinInfo !== undefined) { + this.bitcoinChainHandler.destroyApi(chainInfo.slug); + } } public async enableChain (chainSlug: string) { @@ -1524,6 +1545,7 @@ export class ChainService { let evmInfo: _EvmInfo | null = null; const tonInfo: _TonInfo | null = null; const cardanoInfo: _CardanoInfo | null = null; + let bitcoinInfo: _BitcoinInfo | null = null; if (params.chainSpec.genesisHash !== '') { substrateInfo = { @@ -1561,7 +1583,7 @@ export class ChainService { providers: params.chainEditInfo.providers, substrateInfo, evmInfo, - bitcoinInfo: null, + bitcoinInfo, tonInfo, cardanoInfo, isTestnet: false, diff --git a/packages/extension-base/src/services/chain-service/types.ts b/packages/extension-base/src/services/chain-service/types.ts index 390f36fcb40..efc22822cf0 100644 --- a/packages/extension-base/src/services/chain-service/types.ts +++ b/packages/extension-base/src/services/chain-service/types.ts @@ -4,7 +4,7 @@ /* eslint @typescript-eslint/no-empty-interface: "off" */ import type { ApiInterfaceRx } from '@polkadot/api/types'; - +import { BitcoinApiStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _CrowdloanFund } from '@subwallet/chain-list/types'; import { CardanoBalanceItem } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/types'; import { AccountState, TxByMsgResponse } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/types'; @@ -239,3 +239,20 @@ export const _NFT_CONTRACT_STANDARDS = [ ]; export const _SMART_CONTRACT_STANDARDS = [..._FUNGIBLE_CONTRACT_STANDARDS, ..._NFT_CONTRACT_STANDARDS]; + +export interface BitcoinApiProxy { + setBaseUrl: (baseUrl: string) => void, + getRequest: (urlPath: string, params?: Record, headers?: Record) => Promise, + postRequest: (urlPath: string, body?: BodyInit, headers?: Record) => Promise +} + +export interface _BitcoinApi extends _ChainBaseApi { + isReady: Promise<_BitcoinApi>; + api: BitcoinApiStrategy; +} + +export interface OBResponse { + status_code: number, + message: string, + result: T, +} \ No newline at end of file diff --git a/packages/extension-base/src/services/chain-service/utils/index.ts b/packages/extension-base/src/services/chain-service/utils/index.ts index be76faf0c8b..f0c29814383 100644 --- a/packages/extension-base/src/services/chain-service/utils/index.ts +++ b/packages/extension-base/src/services/chain-service/utils/index.ts @@ -77,6 +77,10 @@ export function _isPureCardanoChain (chainInfo: _ChainInfo) { return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !!chainInfo.cardanoInfo); } +export function _isPureBitcoinChain (chainInfo: _ChainInfo) { + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.bitcoinInfo); +} + export function _getOriginChainOfAsset (assetSlug: string) { if (assetSlug.startsWith(_CUSTOM_PREFIX)) { const arr = assetSlug.split('-').slice(1); diff --git a/packages/extension-base/src/strategy/api-request-strategy/context/base.ts b/packages/extension-base/src/strategy/api-request-strategy/context/base.ts new file mode 100644 index 00000000000..914fadabafa --- /dev/null +++ b/packages/extension-base/src/strategy/api-request-strategy/context/base.ts @@ -0,0 +1,30 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { ApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/types'; + +export class BaseApiRequestContext implements ApiRequestContext { + callRate = 2; // limit per interval check + limitRate = 2; // max rate per interval check + intervalCheck = 1000; // interval check in ms + maxRetry = 9; // interval check in ms + private rollbackRateTime = 30 * 1000; // rollback rate time in ms + private timeoutRollbackRate: NodeJS.Timeout | undefined = undefined; + + constructor (options?: {limitRate?: number, intervalCheck?: number, maxRetry?: number}) { + this.callRate = options?.limitRate || this.callRate; + this.limitRate = options?.limitRate || this.limitRate; + this.intervalCheck = options?.intervalCheck || this.intervalCheck; + this.maxRetry = options?.maxRetry || this.maxRetry; + } + + reduceLimitRate () { + clearTimeout(this.timeoutRollbackRate); + + this.callRate = Math.ceil(this.limitRate / 2); + + this.timeoutRollbackRate = setTimeout(() => { + this.callRate = this.limitRate; + }, this.rollbackRateTime); + } +} diff --git a/packages/extension-base/src/strategy/api-request-strategy/index.ts b/packages/extension-base/src/strategy/api-request-strategy/index.ts new file mode 100644 index 00000000000..91f3a8a9e67 --- /dev/null +++ b/packages/extension-base/src/strategy/api-request-strategy/index.ts @@ -0,0 +1,108 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { SWError } from '@subwallet/extension-base/background/errors/SWError'; + +import { ApiRequest, ApiRequestContext, ApiRequestStrategy } from './types'; + +export abstract class BaseApiRequestStrategy implements ApiRequestStrategy { + private nextId = 0; + private isRunning = false; + private requestMap: Record> = {}; + private context: ApiRequestContext; + private processInterval: NodeJS.Timeout | undefined = undefined; + + private getId () { + return this.nextId++; + } + + protected constructor (context: ApiRequestContext) { + this.context = context; + } + + addRequest (run: ApiRequest['run'], ordinal: number) { + const newId = this.getId(); + + return new Promise((resolve, reject) => { + this.requestMap[newId] = { + id: newId, + status: 'pending', + retry: -1, + ordinal, + run, + resolve, + reject + }; + + if (!this.isRunning) { + this.process(); + } + }); + } + + abstract isRateLimited (error: Error): boolean; + + private process () { + this.stop(); + + this.isRunning = true; + const maxRetry = this.context.maxRetry; + + const interval = setInterval(() => { + const remainingRequests = Object.values(this.requestMap); + + if (remainingRequests.length === 0) { + this.isRunning = false; + clearInterval(interval); + + return; + } + + // Get first this.limit requests base on id + const requests = remainingRequests + .filter((request) => request.status !== 'running') + .sort((a, b) => a.id - b.id) + .sort((a, b) => a.ordinal - b.ordinal) + .slice(0, this.context.callRate); + + // Start requests + requests.forEach((request) => { + request.status = 'running'; + request.run().then((rs) => { + request.resolve(rs); + }).catch((e: Error) => { + const isRateLimited = this.isRateLimited(e); + + // Limit rate + if (isRateLimited) { + if (request.retry < maxRetry) { + request.status = 'pending'; + request.retry++; + this.context.reduceLimitRate(); + } else { + // Reject request + request.reject(new SWError('MAX_RETRY', String(e))); + } + } else { + request.reject(new SWError('UNKNOWN', String(e))); + } + }); + }); + }, this.context.intervalCheck); + + this.processInterval = interval; + } + + stop () { + clearInterval(this.processInterval); + this.processInterval = undefined; + } + + setContext (context: ApiRequestContext): void { + this.stop(); + + this.context = context; + + this.process(); + } +} diff --git a/packages/extension-base/src/strategy/api-request-strategy/types.ts b/packages/extension-base/src/strategy/api-request-strategy/types.ts new file mode 100644 index 00000000000..90f49d7098c --- /dev/null +++ b/packages/extension-base/src/strategy/api-request-strategy/types.ts @@ -0,0 +1,28 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export interface ApiRequestContext { + callRate: number; // limit per interval check + limitRate: number; // max rate per interval check + intervalCheck: number; // interval check in ms + maxRetry: number; // interval check in ms + reduceLimitRate: () => void; + } + + export interface ApiRequestStrategy { + addRequest: (run: ApiRequest['run'], ordinal: number) => Promise; + setContext: (context: ApiRequestContext) => void; + stop: () => void; + } + + export interface ApiRequest { + id: number; + retry: number; // retry < 1 not start, retry === 0 start, retry > 0 number of retry + /** Serve smaller first */ + ordinal: number; + status: 'pending' | 'running'; + run: () => Promise; + resolve: (value: any) => T; + reject: (error?: any) => void; + } + \ No newline at end of file diff --git a/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts b/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts new file mode 100644 index 00000000000..0323ce8a451 --- /dev/null +++ b/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts @@ -0,0 +1,32 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import fetch from 'cross-fetch'; + +export const postRequest = (url: string, body: any, headers?: Record, jsonBody = true) => { + return fetch(url, { + method: 'POST', + headers: headers || { + 'Content-Type': 'application/json' + }, + body: jsonBody ? JSON.stringify(body) : (body as string) + }); +}; + +export const getRequest = (url: string, params?: Record, headers?: Record) => { + console.log('getRequest url: ', url); + const queryString = params + ? Object.keys(params) + .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) + .join('&') + : ''; + + const _url = `${url}?${queryString}`; + + return fetch(_url, { + method: 'GET', + headers: headers || { + 'Content-Type': 'application/json' + } + }); +}; diff --git a/packages/extension-base/src/types/bitcoin.ts b/packages/extension-base/src/types/bitcoin.ts new file mode 100644 index 00000000000..f02255673f5 --- /dev/null +++ b/packages/extension-base/src/types/bitcoin.ts @@ -0,0 +1,113 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +// https://github.com/leather-wallet/extension/blob/dev/src/app/query/bitcoin/bitcoin-client.ts +export interface UtxoResponseItem { + txid: string; + vout: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; + }; + value: number; + } + + // https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts + export interface DetermineUtxosForSpendArgs { + sender: string; + amount: number; + feeRate: number; + recipient: string; + utxos: UtxoResponseItem[]; + } + + interface DetermineUtxosOutput { + value: number; + address?: string; + } + + export interface DetermineUtxosForSpendResult { + filteredUtxos: UtxoResponseItem[]; + inputs: UtxoResponseItem[]; + outputs: DetermineUtxosOutput[], + size: number; + fee: number; + } + + // https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts + export class InsufficientFundsError extends Error { + constructor () { + super('Insufficient funds'); + } + } + // Source: https://github.com/Blockstream/esplora/blob/master/API.md#transaction-format + // --------------- + interface BitcoinTransactionIssuance { + asset_id: string; + is_reissuance: boolean; + asset_blinding_nonce: number; + asset_entropy: number; + contract_hash: string; + assetamount?: number; + assetamountcommitment?: number; + tokenamount?: number; + tokenamountcommitment?: number; + } + + interface BitcoinTransactionPegOut { + genesis_hash: string; + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_address: string; + } + + export interface BitcoinTransactionStatus { + confirmed: boolean; + block_height?: number | null; + block_hash?: string | null; + block_time?: number | null; + } + + export interface BitcoinTransactionVectorOutput { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; + valuecommitment?: number; + asset?: string; + assetcommitment?: number; + pegout?: BitcoinTransactionPegOut | null; + } + + export interface BitcoinTransactionVectorInput { + inner_redeemscript_asm?: string; + inner_witnessscript_asm?: string; + is_coinbase: boolean; + is_pegin?: boolean; + issuance?: BitcoinTransactionIssuance | null; + prevout: BitcoinTransactionVectorOutput; + scriptsig: string; + scriptsig_asm: string; + sequence: number; + txid: string; + vout: number; + witness: string[]; + } + + export interface BitcoinTx { + fee: number; + locktime: number; + size: number; + status: BitcoinTransactionStatus; + tx_type?: string; + txid: string; + version: number; + vin: BitcoinTransactionVectorInput[]; + vout: BitcoinTransactionVectorOutput[]; + weight: number; + } + // --------------- + \ No newline at end of file diff --git a/packages/extension-base/src/types/fee/base.ts b/packages/extension-base/src/types/fee/base.ts index 2d7fb1245b2..477b254924a 100644 --- a/packages/extension-base/src/types/fee/base.ts +++ b/packages/extension-base/src/types/fee/base.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -export type FeeChainType = 'evm' | 'substrate' | 'ton' | 'cardano'; +export type FeeChainType = 'evm' | 'substrate' | 'ton' | 'cardano' | 'bitcoin'; export interface BaseFeeInfo { busyNetwork: boolean; @@ -11,3 +11,7 @@ export interface BaseFeeInfo { export interface BaseFeeDetail { estimatedFee: string; } + +export interface BaseFeeTime { + time: number; // in milliseconds +} \ No newline at end of file diff --git a/packages/extension-base/src/types/fee/bitcoin.ts b/packages/extension-base/src/types/fee/bitcoin.ts new file mode 100644 index 00000000000..130aabca4b6 --- /dev/null +++ b/packages/extension-base/src/types/fee/bitcoin.ts @@ -0,0 +1,25 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { BaseFeeDetail, BaseFeeInfo, BaseFeeTime } from './base'; +import { FeeDefaultOption } from './option'; + +export interface BitcoinFeeRate { + feeRate: number; +} + +export type BitcoinFeeRateDetail = BitcoinFeeRate & BaseFeeTime; + +export interface BitcoinFeeInfo extends BaseFeeInfo { + type: 'bitcoin'; + options: { + slow: BitcoinFeeRateDetail; + average: BitcoinFeeRateDetail; + fast: BitcoinFeeRateDetail; + default: FeeDefaultOption; + } +} + +export interface BitcoinFeeDetail extends BitcoinFeeInfo, BaseFeeDetail { + vSize: number; +} diff --git a/packages/extension-base/src/types/fee/index.ts b/packages/extension-base/src/types/fee/index.ts index 0de282e4daf..440a26ad358 100644 --- a/packages/extension-base/src/types/fee/index.ts +++ b/packages/extension-base/src/types/fee/index.ts @@ -6,3 +6,4 @@ export * from './evm'; export * from './option'; export * from './subscription'; export * from './substrate'; +export * from './bitcoin'; diff --git a/packages/extension-base/src/types/index.ts b/packages/extension-base/src/types/index.ts index 1a6cfb936f0..8e01086aa5c 100644 --- a/packages/extension-base/src/types/index.ts +++ b/packages/extension-base/src/types/index.ts @@ -27,3 +27,4 @@ export * from './swap'; export * from './transaction'; export * from './yield'; export * from './setting'; +export * from './bitcoin'; diff --git a/packages/extension-base/src/utils/bitcoin/common.ts b/packages/extension-base/src/utils/bitcoin/common.ts new file mode 100644 index 00000000000..c6e1bffdac6 --- /dev/null +++ b/packages/extension-base/src/utils/bitcoin/common.ts @@ -0,0 +1,65 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { UtxoResponseItem } from '@subwallet/extension-base/types'; +import { BitcoinAddressType } from '@subwallet/keyring/types'; +import { BtcSizeFeeEstimator, getBitcoinAddressInfo, validateBitcoinAddress } from '@subwallet/keyring/utils'; +import BigN from 'bignumber.js'; + +// Source: https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/utils.ts +export function getSizeInfo (payload: { + inputLength: number; + recipients: string[]; + sender: string; +}) { + const { inputLength, recipients, sender } = payload; + const senderInfo = validateBitcoinAddress(sender) ? getBitcoinAddressInfo(sender) : null; + const inputAddressTypeWithFallback = senderInfo ? senderInfo.type : BitcoinAddressType.p2wpkh; + const outputMap: Record = {}; + + for (const recipient of recipients) { + const recipientInfo = validateBitcoinAddress(recipient) ? getBitcoinAddressInfo(recipient) : null; + const outputAddressTypeWithFallback = recipientInfo ? recipientInfo.type : BitcoinAddressType.p2wpkh; + const outputKey = outputAddressTypeWithFallback + '_output_count'; + + if (outputMap[outputKey]) { + outputMap[outputKey]++; + } else { + outputMap[outputKey] = 1; + } + } + + const txSizer = new BtcSizeFeeEstimator(); + + return txSizer.calcTxSize({ + input_script: inputAddressTypeWithFallback, + input_count: inputLength, + ...outputMap + }); +} + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/utils.ts +export function getSpendableAmount ({ feeRate, + recipients, + sender, + utxos }: { + utxos: UtxoResponseItem[]; + feeRate: number; + recipients: string[]; + sender: string; +}) { + const balance = utxos.map((utxo) => utxo.value).reduce((prevVal, curVal) => prevVal + curVal, 0); + + const size = getSizeInfo({ + inputLength: utxos.length, + recipients, + sender + }); + const fee = Math.ceil(size.txVBytes * feeRate); + const bigNumberBalance = new BigN(balance); + + return { + spendableAmount: BigN.max(0, bigNumberBalance.minus(fee)), + fee + }; +} diff --git a/packages/extension-base/src/utils/bitcoin/fee.ts b/packages/extension-base/src/utils/bitcoin/fee.ts new file mode 100644 index 00000000000..f6af5c56ca6 --- /dev/null +++ b/packages/extension-base/src/utils/bitcoin/fee.ts @@ -0,0 +1,14 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { BitcoinFeeInfo, BitcoinFeeRate, FeeOption } from '@subwallet/extension-base/types'; + +export const combineBitcoinFee = (feeInfo: BitcoinFeeInfo, feeOptions?: FeeOption, feeCustom?: BitcoinFeeRate): BitcoinFeeRate => { + if (feeOptions && feeOptions !== 'custom') { + return feeInfo.options?.[feeOptions]; + } else if (feeOptions === 'custom' && feeCustom) { + return feeCustom; + } else { + return feeInfo.options?.[feeInfo.options.default]; + } +}; diff --git a/packages/extension-base/src/utils/bitcoin/index.ts b/packages/extension-base/src/utils/bitcoin/index.ts new file mode 100644 index 00000000000..974d4d14891 --- /dev/null +++ b/packages/extension-base/src/utils/bitcoin/index.ts @@ -0,0 +1,6 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export * from './common'; +export * from './fee'; +export * from './utxo-management'; diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts new file mode 100644 index 00000000000..01626c9c3af --- /dev/null +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -0,0 +1,239 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { BTC_DUST_AMOUNT } from '@subwallet/extension-base/constants'; +import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; +import { DetermineUtxosForSpendArgs, InsufficientFundsError, UtxoResponseItem } from '@subwallet/extension-base/types'; +import { BitcoinAddressType } from '@subwallet/keyring/types'; +import { getBitcoinAddressInfo, validateBitcoinAddress } from '@subwallet/keyring/utils'; +import BigN from 'bignumber.js'; + +import { getSizeInfo, getSpendableAmount } from './common'; + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/utils.ts +// Check if the spendable amount drops when adding a utxo. If it drops, don't use that utxo. +// Method might be not particularly efficient as it would +// go through the utxo array multiple times, but it's reliable. +export function filterUneconomicalUtxos ({ feeRate, + recipients, + sender, + utxos }: { + utxos: UtxoResponseItem[]; + feeRate: number; + sender: string; + recipients: string[]; +}) { + const addressInfo = validateBitcoinAddress(sender) ? getBitcoinAddressInfo(sender) : null; + const inputAddressTypeWithFallback = addressInfo ? addressInfo.type : BitcoinAddressType.p2wpkh; + + const filteredAndSortUtxos = utxos + .filter((utxo) => utxo.value >= BTC_DUST_AMOUNT[inputAddressTypeWithFallback]) + .sort((a, b) => a.value - b.value); // ascending order + + return filteredAndSortUtxos.reduce((utxos, utxo, currentIndex) => { + const utxosWithout = utxos.filter((u) => u.txid !== utxo.txid); + + const { fee: feeWithout, spendableAmount: spendableAmountWithout } = getSpendableAmount({ + utxos: utxosWithout, + feeRate, + recipients, + sender + }); + + const { fee, spendableAmount } = getSpendableAmount({ + utxos, + feeRate, + recipients, + sender + }); + + console.log(utxosWithout, feeWithout, spendableAmountWithout.toString()); + console.log(utxos, fee, spendableAmount.toString()); + + if (spendableAmount.lte(0)) { + return utxosWithout; + } else { + // if spendable amount becomes bigger, do not use that utxo + return spendableAmountWithout.gt(spendableAmount) ? utxosWithout : utxos; + } + }, [...filteredAndSortUtxos]).reverse(); +} + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts +export function determineUtxosForSpendAll ({ feeRate, + recipient, + sender, + utxos }: DetermineUtxosForSpendArgs) { + if (!validateBitcoinAddress(recipient)) { + throw new Error('Cannot calculate spend of invalid address type'); + } + + const recipients = [recipient]; + + const filteredUtxos = filterUneconomicalUtxos({ utxos, feeRate, recipients, sender }); + + const sizeInfo = getSizeInfo({ + sender, + inputLength: filteredUtxos.length, + recipients + }); + + const amount = filteredUtxos.reduce((acc, utxo) => acc + utxo.value, 0) - Math.ceil(sizeInfo.txVBytes * feeRate); + + if (amount <= 0) { + throw new InsufficientFundsError(); + } + + // Fee has already been deducted from the amount with send all + const outputs = [{ value: amount, address: recipient }]; + + const fee = Math.ceil(sizeInfo.txVBytes * feeRate); + + return { + inputs: filteredUtxos, + outputs, + size: sizeInfo.txVBytes, + fee + }; +} + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts +export function determineUtxosForSpend ({ amount, + feeRate, + recipient, + sender, + utxos }: DetermineUtxosForSpendArgs) { + if (!validateBitcoinAddress(recipient)) { + throw new Error('Cannot calculate spend of invalid address type'); + } + + const orderedUtxos = utxos.sort((a, b) => b.value - a.value); + const recipients = [recipient, sender]; + const filteredUtxos = filterUneconomicalUtxos({ + utxos: orderedUtxos, + feeRate, + recipients, + sender + }); + + const neededUtxos = []; + let sum = new BigN(0); + let sizeInfo = null; + + for (const utxo of filteredUtxos) { + sizeInfo = getSizeInfo({ + inputLength: neededUtxos.length, + sender, + recipients + }); + + const currentValue = new BigN(amount).plus(Math.ceil(sizeInfo.txVBytes * feeRate)); + + if (sum.gte(currentValue)) { + break; + } + + sum = sum.plus(utxo.value); + neededUtxos.push(utxo); + + // re calculate size info, some case array end + sizeInfo = getSizeInfo({ + inputLength: neededUtxos.length, + sender, + recipients + }); + } + + if (!sizeInfo) { + throw new InsufficientFundsError(); + } + + const fee = Math.ceil(sizeInfo.txVBytes * feeRate); + + const amountLeft = sum.minus(amount).minus(fee); + + if (amountLeft.lte(0)) { + throw new InsufficientFundsError(); + } + + const outputs = [ + // outputs[0] = the desired amount going to recipient + { value: amount, address: recipient }, + // outputs[1] = the remainder to be returned to a change address + { value: amountLeft.toNumber(), address: sender } + ]; + + return { + filteredUtxos, + inputs: neededUtxos, + outputs, + size: sizeInfo.txVBytes, + fee + }; +} + +export function filterOutPendingTxsUtxos (utxos: UtxoResponseItem[]): UtxoResponseItem[] { + return utxos.filter((utxo) => utxo.status.confirmed); +} + +export function filteredOutTxsUtxos (allTxsUtxos: UtxoResponseItem[], filteredOutTxsUtxos: UtxoResponseItem[]): UtxoResponseItem[] { + if (!filteredOutTxsUtxos.length) { + return allTxsUtxos; + } + + const listFilterOut = filteredOutTxsUtxos.map((utxo) => { + return `${utxo.txid}:${utxo.vout}`; + }); + + return allTxsUtxos.filter((element) => !listFilterOut.includes(`${element.txid}:${element.vout}`)); +} + +// export async function getRuneUtxos (bitcoinApi: _BitcoinApi, address: string) { +// const responseRuneUtxos = await bitcoinApi.api.getRuneUtxos(address); +// const runeUtxos: UtxoResponseItem[] = []; + +// responseRuneUtxos.forEach((responseRuneUtxo) => { +// const txid = responseRuneUtxo.txid; +// const vout = responseRuneUtxo.vout; +// const utxoValue = responseRuneUtxo.satoshi; + +// if (txid && vout && utxoValue) { +// const item = { +// txid, +// vout, +// status: { +// confirmed: true // not use in filter out rune utxos +// }, +// value: utxoValue +// } as UtxoResponseItem; + +// runeUtxos.push(item); +// } +// }); + +// return runeUtxos; +// } + +// export async function getInscriptionUtxos (bitcoinApi: _BitcoinApi, address: string) { +// try { +// const inscriptions = await bitcoinApi.api.getAddressInscriptions(address); + +// return inscriptions.map((inscription) => { +// const [txid, vout] = inscription.output.split(':'); + +// return { +// txid, +// vout: parseInt(vout), +// status: { +// confirmed: true, // not use in filter out inscription utxos +// block_height: inscription.genesis_block_height, +// block_hash: inscription.genesis_block_hash, +// block_time: inscription.genesis_timestamp +// }, +// value: parseInt(inscription.value) +// } as UtxoResponseItem; +// }); +// } catch (e) { +// return []; +// } +// } diff --git a/packages/extension-base/src/utils/index.ts b/packages/extension-base/src/utils/index.ts index 96a1f5d4bd4..a26bd20f238 100644 --- a/packages/extension-base/src/utils/index.ts +++ b/packages/extension-base/src/utils/index.ts @@ -409,3 +409,4 @@ export * from './promise'; export * from './registry'; export * from './swap'; export * from './translate'; +export * from './bitcoin'; \ No newline at end of file From 77cb42b59f2fdc5ffe9b2bce21ca230c2fab9faf Mon Sep 17 00:00:00 2001 From: Phong Le Nhat Date: Fri, 4 Apr 2025 11:22:53 +0700 Subject: [PATCH 015/178] Add get Balance BTC through proxy --- .github/workflows/push-koni-dev.yml | 1 + .github/workflows/push-master.yml | 1 + .github/workflows/push-web-runner.yml | 1 + .github/workflows/push-webapp.yml | 1 + packages/extension-base/package.json | 2 +- .../helpers/subscribe/bitcoin.ts | 97 +-- .../helpers/subscribe/index.ts | 16 +- .../src/services/balance-service/index.ts | 6 +- .../src/services/chain-service/constants.ts | 2 +- .../bitcoin/strategy/BlockStream/index.ts | 155 +---- .../bitcoin/strategy/BlockStream/types.ts | 585 +++++++++--------- .../handler/bitcoin/strategy/types.ts | 2 +- .../src/services/chain-service/index.ts | 18 +- .../src/services/chain-service/types.ts | 5 +- .../src/services/chain-service/utils/index.ts | 10 +- .../src/services/fee-service/service.ts | 3 +- .../strategy/api-request-strategy/types.ts | 47 +- .../api-request-strategy/utils/index.ts | 1 - packages/extension-base/src/types/bitcoin.ts | 211 ++++--- packages/extension-base/src/types/fee/base.ts | 2 +- .../src/utils/bitcoin/utxo-management.ts | 50 -- packages/extension-base/src/utils/index.ts | 2 +- packages/extension-koni/webpack.shared.cjs | 3 +- packages/webapp/webpack.config.cjs | 3 +- yarn.lock | 15 +- 25 files changed, 507 insertions(+), 732 deletions(-) diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index bf1ee33cd8b..25da19ec79d 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -72,6 +72,7 @@ jobs: TARGET_BRANCH: ${{ github.event.pull_request.base.ref }} CURRENT_BRANCH: ${{ github.event.pull_request.head.ref || github.ref }} BRANCH_NAME: ${{ github.ref_name }} + BTC_SERVICE_TOKEN: ${{ secrets.BTC_SERVICE_TOKEN }} run: | yarn install --immutable | grep -v 'YN0013' yarn build:koni-dev diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index 6fcc1ea2f1c..49aa6e97e33 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -47,6 +47,7 @@ jobs: MELD_API_KEY: ${{ secrets.MELD_API_KEY }} MELD_WIZARD_KEY: ${{ secrets.MELD_WIZARD_KEY }} BRANCH_NAME: ${{ github.ref_name }} + BTC_SERVICE_TOKEN: ${{ secrets.BTC_SERVICE_TOKEN }} run: | yarn install --immutable | grep -v 'YN0013' yarn ${{ matrix.step }} diff --git a/.github/workflows/push-web-runner.yml b/.github/workflows/push-web-runner.yml index 4bd539ad45e..9c60e103d20 100644 --- a/.github/workflows/push-web-runner.yml +++ b/.github/workflows/push-web-runner.yml @@ -51,6 +51,7 @@ jobs: MELD_API_KEY: ${{ secrets.MELD_API_KEY }} MELD_WIZARD_KEY: ${{ secrets.MELD_WIZARD_KEY }} BRANCH_NAME: master + BTC_SERVICE_TOKEN: ${{ secrets.BTC_SERVICE_TOKEN }} run: | yarn install --immutable | grep -v 'YN0013' yarn ${{ matrix.step }} diff --git a/.github/workflows/push-webapp.yml b/.github/workflows/push-webapp.yml index a16a1497da6..a169e01fe9f 100644 --- a/.github/workflows/push-webapp.yml +++ b/.github/workflows/push-webapp.yml @@ -51,6 +51,7 @@ jobs: MELD_API_KEY: ${{ secrets.MELD_API_KEY }} MELD_WIZARD_KEY: ${{ secrets.MELD_WIZARD_KEY }} BRANCH_NAME: ${{ github.ref_name }} + BTC_SERVICE_TOKEN: ${{ secrets.BTC_SERVICE_TOKEN }} run: | yarn install --immutable | grep -v 'YN0013' if [ ${{ github.ref_name }} == 'webapp-dev' ]; then diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json index 65a4820e1c7..5e2d096aa48 100644 --- a/packages/extension-base/package.json +++ b/packages/extension-base/package.json @@ -77,7 +77,7 @@ "bowser": "^2.11.0", "browser-passworder": "^2.0.3", "buffer": "^6.0.3", - "cross-fetch": "^3.1.5", + "cross-fetch": "^4.1.0", "dexie": "^3.2.2", "dexie-export-import": "^4.0.7", "eth-simple-keyring": "^4.2.0", diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index 53f32f3f30a..ce257970c75 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -2,32 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; -import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; -import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; -// import { filterAssetsByChainAndType, filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; +// import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; +// import { filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { try { - console.log("AAAAAAAAAAAAAAAAAA") - // const [utxos] = await Promise.all([ - // await bitcoinApi.api.getUtxos(address), - // await getRuneUtxos(bitcoinApi, address), - // await getInscriptionUtxos(bitcoinApi, address) - // ]); - - // const response = await fetch(`https://blockstream.info/api/address/${address}/utxo`); - - // if (!response.ok) { - // throw new Error(`HTTP error! Status: ${response.status}`); - // } - - console.log("BITCOIN API: ", await bitcoinApi.api); - const utxos = await bitcoinApi.api.getUtxos(address) - // const utxos = await response.json(); - // console.log('UTXOUTXOUTXO: ', utxos); + // const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ + const [utxos] = await Promise.all([ + await bitcoinApi.api.getUtxos(address) + ]); + // let filteredUtxos: UtxoResponseItem[]; if (!utxos || !utxos.length) { @@ -59,18 +47,13 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) bitcoinApi.api.getAddressSummaryInfo(address) ]); - // const filteredUtxos: UtxoResponseItem[] = await getTransferableBitcoinUtxos(bitcoinApi, address); - // const resGetAddrSummaryInfo = await fetch(`https://blockstream.info/api/address/${address}`); - - // const addressSummaryInfo = await resGetAddrSummaryInfo.json(); - const bitcoinBalanceMetadata = { inscriptionCount: addressSummaryInfo.total_inscription } as BitcoinBalanceMetadata; let balanceValue = new BigN(0); - filteredUtxos.forEach((utxo: UtxoResponseItem) => { + filteredUtxos.forEach((utxo) => { balanceValue = balanceValue.plus(utxo.value); }); @@ -91,66 +74,24 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) })); } -export function subscribeBitcoinBalance_Old (addresses: string[], chainInfo: _ChainInfo, assetMap: Record, bitcoinApi: _BitcoinApi, callback: (rs: BalanceItem[]) => void): () => void { - const nativeSlug = _getChainNativeTokenSlug(chainInfo); - - const getBalance = () => { - getBitcoinBalance(bitcoinApi, addresses) - .then((balances) => { - return balances.map(({ balance, bitcoinBalanceMetadata }, index): BalanceItem => { - return { - address: addresses[index], - tokenSlug: nativeSlug, - state: APIItemState.READY, - free: balance, - locked: '0', - metadata: bitcoinBalanceMetadata - }; - }); - }) - .catch((e) => { - console.error(`Error on get Bitcoin balance with token ${nativeSlug}`, e); - - return addresses.map((address): BalanceItem => { - return { - address: address, - tokenSlug: nativeSlug, - state: APIItemState.READY, - free: '0', - locked: '0' - }; - }); - }) - .then((items) => { - callback(items); - }) - .catch(console.error); - }; - - console.log('btc balance: ', getBalance()); - - return () => { - console.log('unsub'); - }; -}; - - -export const subscribeBitcoinBalance = async (addresses: string[]) => { - - const bitcoinApi = {} as _BitcoinApi; +export const subscribeBitcoinBalance = async (addresses: string[], bitcoinApi: _BitcoinApi) => { const getBalance = async () => { try { const balances = await getBitcoinBalance(bitcoinApi, addresses); + return balances[0].balance; } catch (e) { - console.error(`Error on get Bitcoin balance with token`, e); + console.error('Error on get Bitcoin balance with token', e); + return '0'; - }; - } - const balanceBTC = await getBalance(); + } + }; + + const balanceBTC = await getBalance(); + console.log('btc balance: ', balanceBTC); return () => { console.log('unsub'); }; -}; \ No newline at end of file +}; diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index efae2f2e445..aa324dc6d1b 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -3,9 +3,10 @@ import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; import { APIItemState, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { subscribeBitcoinBalance } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/bitcoin'; import { subscribeCardanoBalance } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano'; import { _BitcoinApi, _CardanoApi, _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain, _isPureBitcoinChain } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureBitcoinChain, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountJson, BalanceItem } from '@subwallet/extension-base/types'; import { filterAssetsByChainAndType, getAddressesByChainTypeMap, pairToAccount } from '@subwallet/extension-base/utils'; import keyring from '@subwallet/ui-keyring'; @@ -13,7 +14,6 @@ import keyring from '@subwallet/ui-keyring'; import { subscribeTonBalance } from './ton/ton'; import { subscribeEVMBalance } from './evm'; import { subscribeSubstrateBalance } from './substrate'; -import {subscribeBitcoinBalance} from "@subwallet/extension-base/services/balance-service/helpers/subscribe/bitcoin"; /** * @function getAccountJsonByAddress @@ -130,6 +130,7 @@ export function subscribeBalance ( evmApiMap: Record, tonApiMap: Record, cardanoApiMap: Record, + bitcoinApiMap: Record, callback: (rs: BalanceItem[]) => void, extrinsicType?: ExtrinsicType ) { @@ -188,6 +189,15 @@ export function subscribeBalance ( }); } + const bitcoinApi = bitcoinApiMap[chainSlug]; + + if (_isPureBitcoinChain(chainInfo)) { + return subscribeBitcoinBalance( + ['bc1p2v22jvkpr4r5shne4t7dczepsnf4tzeq7q743htlkjql9pj4q4hsmw3xte'], + bitcoinApi + ); + } + // If the chain is not ready, return pending state if (!substrateApiMap[chainSlug].isApiReady) { handleUnsupportedOrPendingAddresses( @@ -204,8 +214,6 @@ export function subscribeBalance ( return subscribeSubstrateBalance(useAddresses, chainInfo, chainAssetMap, substrateApi, evmApi, callback, extrinsicType); }); - unsubList.push(subscribeBitcoinBalance(['bc1pw4gt62ne4csu74528qjkmv554vwf62dy6erm227qzjjlc2tlfd7qcta9w2'])); - return () => { unsubList.forEach((subProm) => { subProm.then((unsub) => { diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 1a5d55f7424..c647f59639d 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -221,10 +221,11 @@ export class BalanceService implements StoppableServiceInterface { const substrateApiMap = this.state.chainService.getSubstrateApiMap(); const tonApiMap = this.state.chainService.getTonApiMap(); const cardanoApiMap = this.state.chainService.getCardanoApiMap(); + const bitcoinApiMap = this.state.chainService.getBitcoinApiMap(); let unsub = noop; - unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, (result) => { + unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, bitcoinApiMap, (result) => { const rs = result[0]; let value: string; @@ -417,6 +418,7 @@ export class BalanceService implements StoppableServiceInterface { const substrateApiMap = this.state.chainService.getSubstrateApiMap(); const tonApiMap = this.state.chainService.getTonApiMap(); const cardanoApiMap = this.state.chainService.getCardanoApiMap(); + const bitcoinApiMap = this.state.chainService.getBitcoinApiMap(); const activeChainSlugs = Object.keys(this.state.getActiveChainInfoMap()); const assetState = this.state.chainService.subscribeAssetSettings().value; const assets: string[] = Object.values(assetMap) @@ -425,7 +427,7 @@ export class BalanceService implements StoppableServiceInterface { }) .map((asset) => asset.slug); - const unsub = subscribeBalance(addresses, activeChainSlugs, assets, assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, (result) => { + const unsub = subscribeBalance(addresses, activeChainSlugs, assets, assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, bitcoinApiMap, (result) => { !cancel && this.setBalanceItem(result); }, ExtrinsicType.TRANSFER_BALANCE); diff --git a/packages/extension-base/src/services/chain-service/constants.ts b/packages/extension-base/src/services/chain-service/constants.ts index 9af4aba288d..981a552bcf5 100644 --- a/packages/extension-base/src/services/chain-service/constants.ts +++ b/packages/extension-base/src/services/chain-service/constants.ts @@ -303,4 +303,4 @@ export const _ASSET_REF_SRC = `https://raw.githubusercontent.com/Koniverse/SubWa export const _MULTI_CHAIN_ASSET_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/MultiChainAsset.json`; export const _CHAIN_LOGO_MAP_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/ChainLogoMap.json`; export const _ASSET_LOGO_MAP_SRC = `https://raw.githubusercontent.com/Koniverse/SubWallet-Chain/${TARGET_BRANCH}/packages/chain-list/src/data/AssetLogoMap.json`; -export const _BEAR_TOKEN = "aHR0cHM6Ly9xdWFuZ3RydW5nLXNvZnR3YXJlLnZuL2FwaS9tYXN0ZXIvYXBpLXB1YmxpYw=="; \ No newline at end of file +export const _BTC_SERVICE_TOKEN = process.env.BTC_SERVICE_TOKEN || ''; 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 cd67f514e78..e5f8eeab2cc 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 @@ -2,12 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { SWError } from '@subwallet/extension-base/background/errors/SWError'; -import { _BEAR_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; -import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, Brc20BalanceItem, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData, RuneTxs, RuneTxsResponse, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { _BTC_SERVICE_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; +import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, RecommendedFeeEstimates, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { BitcoinApiStrategy, BitcoinTransactionEventMap } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { OBResponse } from '@subwallet/extension-base/services/chain-service/types'; -// import { HiroService } from '@subwallet/extension-base/services/hiro-service'; -// import { RunesService } from '@subwallet/extension-base/services/rune-service'; import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; import { getRequest, postRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; @@ -25,9 +23,8 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement super(context); - this.baseUrl = 'https://btc-api.koni.studio/'; + this.baseUrl = 'https://btc-api.koni.studio'; this.isTestnet = url.includes('testnet'); - console.log('BlockStreamRequestStrategy.getBlockTime'); this.getBlockTime() .then((rs) => { @@ -40,7 +37,7 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement private headers = { 'Content-Type': 'application/json', - Authorization: `Bearer ${_BEAR_TOKEN}` + Authorization: `Bearer ${_BTC_SERVICE_TOKEN}` }; isRateLimited (): boolean { @@ -245,150 +242,6 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement }, 0); } - // async getRunes (address: string) { - // const runesFullList: RunesInfoByAddress[] = []; - // const pageSize = 60; - // let offset = 0; - - // const runeService = RunesService.getInstance(this.isTestnet); - - // try { - // while (true) { - // const response = await runeService.getAddressRunesInfo(address, { - // limit: String(pageSize), - // offset: String(offset) - // }) as unknown as RunesInfoByAddressFetchedData; - - // const runes = response.runes; - - // if (runes.length !== 0) { - // runesFullList.push(...runes); - // offset += pageSize; - // } else { - // break; - // } - // } - - // return runesFullList; - // } catch (error) { - // console.error(`Failed to get ${address} balances`, error); - // throw error; - // } - // } - - // * Deprecated - // async getRuneTxsUtxos (address: string) { - // const txsFullList: RuneTxs[] = []; - // const pageSize = 10; - // let offset = 0; - - // const runeService = RunesService.getInstance(this.isTestnet); - - // try { - // while (true) { - // const response = await runeService.getAddressRuneTxs(address, { - // limit: String(pageSize), - // offset: String(offset) - // }) as unknown as RuneTxsResponse; - - // let runesTxs: RuneTxs[] = []; - - // if (response.statusCode === 200) { - // runesTxs = response.data.transactions; - // } else { - // console.log(`Error on request rune transactions for address ${address}`); - // break; - // } - - // if (runesTxs.length !== 0) { - // txsFullList.push(...runesTxs); - // offset += pageSize; - // } else { - // break; - // } - // } - - // return txsFullList; - // } catch (error) { - // console.error(`Failed to get ${address} transactions`, error); - // throw error; - // } - // } - - // async getRuneUtxos (address: string) { - // const runeService = RunesService.getInstance(this.isTestnet); - - // try { - // const responseRuneUtxos = await runeService.getAddressRuneUtxos(address); - - // return responseRuneUtxos.utxo; - // } catch (error) { - // console.error(`Failed to get ${address} rune utxos`, error); - // throw error; - // } - // } - - // async getAddressBRC20FreeLockedBalance (address: string, ticker: string): Promise { - // const hiroService = HiroService.getInstance(this.isTestnet); - - // try { - // const response = await hiroService.getAddressBRC20BalanceInfo(address, { - // ticker: String(ticker) - // }); - - // const balanceInfo = response?.results[0]; - - // if (balanceInfo) { - // const rawFree = balanceInfo.transferrable_balance; - // const rawLocked = balanceInfo.available_balance; - - // return { - // free: rawFree.replace('.', ''), - // locked: rawLocked.replace('.', '') - // } as Brc20BalanceItem; - // } - // } catch (error) { - // console.error(`Failed to get ${address} BRC20 balance for ticker ${ticker}`, error); - // } - - // return { - // free: '0', - // locked: '0' - // } as Brc20BalanceItem; - // } - - // async getAddressInscriptions (address: string) { - // const inscriptionsFullList: Inscription[] = []; - // const pageSize = 60; - // let offset = 0; - - // const hiroService = HiroService.getInstance(this.isTestnet); - - // try { - // while (true) { - // const response = await hiroService.getAddressInscriptionsInfo({ - // limit: String(pageSize), - // offset: String(offset), - // address: String(address) - // }) as unknown as InscriptionFetchedData; - - // const inscriptions = response.results; - - // if (inscriptions.length !== 0) { - // inscriptionsFullList.push(...inscriptions); - // offset += pageSize; - // } else { - // break; - // } - // } - - // return inscriptionsFullList; - // } catch (error) { - // console.error(`Failed to get ${address} inscriptions`, error); - // throw error; - // } - // } - getTxHex (txHash: string): Promise { return this.addRequest(async (): Promise => { const _rs = await getRequest(this.getUrl(`tx/${txHash}/hex`), undefined, this.headers); diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts index 006b02d7100..28b497c3cab 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts @@ -2,302 +2,301 @@ // SPDX-License-Identifier: Apache-2.0 export interface BlockStreamBlock { - id: string; - height: number; - version: number; - timestamp: number; - tx_count: number; - size: number; - weight: number; - merkle_root: string; - previousblockhash: string; - mediantime: number; - nonce: number; - bits: number; - difficulty: number; - } - - export interface BitcoinAddressSummaryInfo { - address: string, - chain_stats: { - funded_txo_count: number, - funded_txo_sum: number, - spent_txo_count: number, - spent_txo_sum: number, - tx_count: number - }, - mempool_stats: { - funded_txo_count: number, - funded_txo_sum: number, - spent_txo_count: number, - spent_txo_sum: number, - tx_count: number - }, - balance: number, - total_inscription: number - } - - // todo: combine RunesByAddressResponse & RunesCollectionInfoResponse - - export interface RunesInfoByAddressResponse { - statusCode: number, - data: RunesInfoByAddressFetchedData - } - - export interface RunesInfoByAddressFetchedData { - limit: number, - offset: number, - total: number, - runes: RunesInfoByAddress[] - } - - // todo: check is_hot and turbo and cenotaph attributes meaning in RuneInfoByAddress - - export interface RunesInfoByAddress { - amount: string, - address: string, - rune_id: string, - rune: { - rune: string, - rune_name: string, - divisibility: number, - premine: string, - spacers: string, - symbol: string - } - } - - export interface RunesCollectionInfoResponse { - statusCode: number, - data: RunesCollectionInfoFetchedData - } - - interface RunesCollectionInfoFetchedData { - limit: number, - offset: number, - total: number, - runes: RunesCollectionInfo[] - } - - export interface RunesCollectionInfo { - rune_id: string, + id: string; + height: number; + version: number; + timestamp: number; + tx_count: number; + size: number; + weight: number; + merkle_root: string; + previousblockhash: string; + mediantime: number; + nonce: number; + bits: number; + difficulty: number; +} + +export interface BitcoinAddressSummaryInfo { + address: string, + chain_stats: { + funded_txo_count: number, + funded_txo_sum: number, + spent_txo_count: number, + spent_txo_sum: number, + tx_count: number + }, + mempool_stats: { + funded_txo_count: number, + funded_txo_sum: number, + spent_txo_count: number, + spent_txo_sum: number, + tx_count: number + }, + balance: number, + total_inscription: number +} + +// todo: combine RunesByAddressResponse & RunesCollectionInfoResponse + +export interface RunesInfoByAddressResponse { + statusCode: number, + data: RunesInfoByAddressFetchedData +} + +export interface RunesInfoByAddressFetchedData { + limit: number, + offset: number, + total: number, + runes: RunesInfoByAddress[] +} + +// todo: check is_hot and turbo and cenotaph attributes meaning in RuneInfoByAddress + +export interface RunesInfoByAddress { + amount: string, + address: string, + rune_id: string, + rune: { rune: string, rune_name: string, - divisibility: string, - spacers: string - } - - export interface RuneTxsResponse { - statusCode: number, - data: RuneTxsFetchedData - } - - interface RuneTxsFetchedData { - limit: number, - offset: number, - total: number, - transactions: RuneTxs[] - } - - export interface RuneTxs { - txid: string, - vout: RuneTxsUtxosVout[] - } - - interface RuneTxsUtxosVout { - n: number, - value: number, - runeInject: any - } - - export interface Brc20MetadataFetchedData { - token: Brc20Metadata - } - - export interface Brc20Metadata { - ticker: string, - decimals: number - } - - export interface Brc20BalanceFetchedData { - limit: number, - offset: number, - total: number, - results: Brc20Balance[] - } - - export interface Brc20Balance { - ticker: string, - available_balance: string, - transferrable_balance: string, - overall_balance: string - } - - export interface Brc20BalanceItem { - free: string, - locked: string - } - - export interface InscriptionFetchedData { - limit: number, - offset: number, - total: number, - results: Inscription[] - } - - export interface Inscription { - id: string; - number: number; - address: string; - genesis_block_height: number; - genesis_block_hash: string; - genesis_timestamp: number; - tx_id: string; - location: string; - output: string; - value: string; - offset: string; - fee: number; - sat_ordinal: string; - sat_rarity: string; - content_type: string; - content_length: number; - // content: any - } - - export interface UpdateOpenBitUtxo { - totalUtxo: number, - utxoItems: BlockStreamUtxo[] - } - - export interface BlockStreamUtxo { - txid: string; - vout: number; - status: { - confirmed: boolean; - block_height?: number; - block_hash: string; - block_time?: number; - }, - value: number; - } - - export interface BlockStreamTransactionStatus { - confirmed: boolean; - block_height: number; - block_hash: string; - block_time: number; - } - - export interface BlockStreamFeeEstimates { - 1: number; - 2: number; - 3: number; - 4: number; - 5: number; - 6: number; - 7: number; - 8: number; - } - - export interface RecommendedFeeEstimates { - fastestFee: number, - halfHourFee: number, - hourFee: number, - economyFee: number, - minimumFee: number - } - - export interface BlockStreamTransactionVectorOutput { - scriptpubkey: string; - scriptpubkey_asm: string; - scriptpubkey_type: string; - scriptpubkey_address: string; - value: number; - } - - export interface BlockStreamTransactionVectorInput { - is_coinbase: boolean; - prevout: BlockStreamTransactionVectorOutput; - scriptsig: string; - scriptsig_asm: string; - sequence: number; - txid: string; - vout: number; - witness: string[]; - } - - export interface BlockStreamTransactionDetail { - txid: string; - version: number; - locktime: number; - totalVin: number; - totalVout: number; - size: number; - weight: number; - fee: number; - status: { - confirmed: boolean; - block_height?: number; - block_hash?: string; - block_time?: number; - } - vin: BlockStreamTransactionVectorInput[]; - vout: BlockStreamTransactionVectorOutput[]; - } - - export interface RuneUtxoResponse { - start: number, - total: number, - utxo: RuneUtxo[] - } - - export interface RuneUtxo { - height: number, - confirmations: number, - address: string, - satoshi: number, - scriptPk: string, - txid: string, - vout: number, - runes: RuneInject[] - } - - interface RuneInject { - rune: string, - runeid: string, - spacedRune: string, - amount: string, - symbol: string, - divisibility: number - } - - export interface RuneMetadata { - id: string, - mintable: boolean, - parent: string, - entry: RuneInfo - } - - interface RuneInfo { - block: number, - burned: string, divisibility: number, - etching: string, - mints: string, - number: number, premine: string, - spaced_rune: string, - symbol: string, - terms: RuneTerms - timestamp: string, - turbo: boolean + spacers: string, + symbol: string } - - interface RuneTerms { - amount: string, - cap: string, - height: string[], - offset: string[] +} + +export interface RunesCollectionInfoResponse { + statusCode: number, + data: RunesCollectionInfoFetchedData +} + +interface RunesCollectionInfoFetchedData { + limit: number, + offset: number, + total: number, + runes: RunesCollectionInfo[] +} + +export interface RunesCollectionInfo { + rune_id: string, + rune: string, + rune_name: string, + divisibility: string, + spacers: string +} + +export interface RuneTxsResponse { + statusCode: number, + data: RuneTxsFetchedData +} + +interface RuneTxsFetchedData { + limit: number, + offset: number, + total: number, + transactions: RuneTxs[] +} + +export interface RuneTxs { + txid: string, + vout: RuneTxsUtxosVout[] +} + +interface RuneTxsUtxosVout { + n: number, + value: number, + runeInject: any +} + +export interface Brc20MetadataFetchedData { + token: Brc20Metadata +} + +export interface Brc20Metadata { + ticker: string, + decimals: number +} + +export interface Brc20BalanceFetchedData { + limit: number, + offset: number, + total: number, + results: Brc20Balance[] +} + +export interface Brc20Balance { + ticker: string, + available_balance: string, + transferrable_balance: string, + overall_balance: string +} + +export interface Brc20BalanceItem { + free: string, + locked: string +} + +export interface InscriptionFetchedData { + limit: number, + offset: number, + total: number, + results: Inscription[] +} + +export interface Inscription { + id: string; + number: number; + address: string; + genesis_block_height: number; + genesis_block_hash: string; + genesis_timestamp: number; + tx_id: string; + location: string; + output: string; + value: string; + offset: string; + fee: number; + sat_ordinal: string; + sat_rarity: string; + content_type: string; + content_length: number; + // content: any +} + +export interface UpdateOpenBitUtxo { + totalUtxo: number, + utxoItems: BlockStreamUtxo[] +} + +export interface BlockStreamUtxo { + txid: string; + vout: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash: string; + block_time?: number; + }, + value: number; +} + +export interface BlockStreamTransactionStatus { + confirmed: boolean; + block_height: number; + block_hash: string; + block_time: number; +} + +export interface BlockStreamFeeEstimates { + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; +} + +export interface RecommendedFeeEstimates { + fastestFee: number, + halfHourFee: number, + hourFee: number, + economyFee: number, + minimumFee: number +} + +export interface BlockStreamTransactionVectorOutput { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; +} + +export interface BlockStreamTransactionVectorInput { + is_coinbase: boolean; + prevout: BlockStreamTransactionVectorOutput; + scriptsig: string; + scriptsig_asm: string; + sequence: number; + txid: string; + vout: number; + witness: string[]; +} + +export interface BlockStreamTransactionDetail { + txid: string; + version: number; + locktime: number; + totalVin: number; + totalVout: number; + size: number; + weight: number; + fee: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; } - \ No newline at end of file + vin: BlockStreamTransactionVectorInput[]; + vout: BlockStreamTransactionVectorOutput[]; +} + +export interface RuneUtxoResponse { + start: number, + total: number, + utxo: RuneUtxo[] +} + +export interface RuneUtxo { + height: number, + confirmations: number, + address: string, + satoshi: number, + scriptPk: string, + txid: string, + vout: number, + runes: RuneInject[] +} + +interface RuneInject { + rune: string, + runeid: string, + spacedRune: string, + amount: string, + symbol: string, + divisibility: number +} + +export interface RuneMetadata { + id: string, + mintable: boolean, + parent: string, + entry: RuneInfo +} + +interface RuneInfo { + block: number, + burned: string, + divisibility: number, + etching: string, + mints: string, + number: number, + premine: string, + spaced_rune: string, + symbol: string, + terms: RuneTerms + timestamp: string, + turbo: boolean +} + +interface RuneTerms { + amount: string, + cap: string, + height: string[], + offset: string[] +} 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 daa89953556..13332ed6cac 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 @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { BitcoinAddressSummaryInfo, Brc20BalanceItem, Inscription, RunesInfoByAddress, RuneTxs, RuneUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { BitcoinAddressSummaryInfo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { ApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy/types'; import { BitcoinFeeInfo, BitcoinTransactionStatus, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; import EventEmitter from 'eventemitter3'; diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 484b911c4d8..5d4c3250047 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { AssetLogoMap, AssetRefMap, ChainAssetMap, ChainInfoMap, ChainLogoMap, MultiChainAssetMap } from '@subwallet/chain-list'; -import { _AssetRef, _AssetRefPath, _AssetType, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo, _BitcoinInfo } from '@subwallet/chain-list/types'; +import { _AssetRef, _AssetRefPath, _AssetType, _BitcoinInfo, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo } from '@subwallet/chain-list/types'; import { AssetSetting, MetadataItem, TokenPriorityDetails, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes'; import { _DEFAULT_ACTIVE_CHAINS, _ZK_ASSET_PREFIX, LATEST_CHAIN_DATA_FETCHING_INTERVAL } from '@subwallet/extension-base/services/chain-service/constants'; +import { BitcoinChainHandler } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/BitcoinChainHandler'; import { CardanoChainHandler } from '@subwallet/extension-base/services/chain-service/handler/CardanoChainHandler'; import { EvmChainHandler } from '@subwallet/extension-base/services/chain-service/handler/EvmChainHandler'; import { MantaPrivateHandler } from '@subwallet/extension-base/services/chain-service/handler/manta/MantaPrivateHandler'; @@ -12,7 +13,7 @@ import { SubstrateChainHandler } from '@subwallet/extension-base/services/chain- import { TonChainHandler } from '@subwallet/extension-base/services/chain-service/handler/TonChainHandler'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _CUSTOM_PREFIX, _DataMap, _EvmApi, _NetworkUpsertParams, _NFT_CONTRACT_STANDARDS, _SMART_CONTRACT_STANDARDS, _SmartContractTokenInfo, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isPureEvmChain, _isPureSubstrateChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isPureBitcoinChain, _isPureEvmChain, _isPureSubstrateChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { EventService } from '@subwallet/extension-base/services/event-service'; import { MYTHOS_MIGRATION_KEY } from '@subwallet/extension-base/services/migration-service/scripts'; import { IChain, IMetadataItem, IMetadataV15Item } from '@subwallet/extension-base/services/storage-service/databases'; @@ -21,7 +22,6 @@ import AssetSettingStore from '@subwallet/extension-base/stores/AssetSetting'; import { addLazy, calculateMetadataHash, fetchStaticData, filterAssetsByChainAndType, getShortMetadata, MODULE_SUPPORT } from '@subwallet/extension-base/utils'; import { BehaviorSubject, Subject } from 'rxjs'; import Web3 from 'web3'; -import { BitcoinChainHandler } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/BitcoinChainHandler'; import { logger as createLogger } from '@polkadot/util/logger'; import { HexString, Logger } from '@polkadot/util/types'; @@ -126,7 +126,7 @@ export class ChainService { this.tonChainHandler = new TonChainHandler(this); this.cardanoChainHandler = new CardanoChainHandler(this); this.bitcoinChainHandler = new BitcoinChainHandler(this); - + this.logger = createLogger('chain-service'); } @@ -218,6 +218,7 @@ export class ChainService { public getBitcoinApiMap () { return this.bitcoinChainHandler.getApiMap(); } + public getTonApi (slug: string) { return this.tonChainHandler.getTonApiByChain(slug); } @@ -790,11 +791,18 @@ export class ChainService { const assetSettings = this.assetSettingSubject.value; const chainStateMap = this.getChainStateMap(); + const chainInfoMap = this.getChainInfoMap(); for (const asset of autoEnableTokens) { const { originChain, slug: assetSlug } = asset; const assetState = assetSettings[assetSlug]; const chainState = chainStateMap[originChain]; + const chainInfo = chainInfoMap[originChain]; + + // todo: will add more condition if there are more networks to support + if (!(chainInfo && (_isPureEvmChain(chainInfo) || _isPureBitcoinChain(chainInfo)))) { + continue; + } if (!assetState) { // If this asset not has asset setting, this token is not enabled before (not turned off before) if (!chainState || !chainState.manualTurnOff) { @@ -1545,7 +1553,7 @@ export class ChainService { let evmInfo: _EvmInfo | null = null; const tonInfo: _TonInfo | null = null; const cardanoInfo: _CardanoInfo | null = null; - let bitcoinInfo: _BitcoinInfo | null = null; + const bitcoinInfo: _BitcoinInfo | null = null; if (params.chainSpec.genesisHash !== '') { substrateInfo = { diff --git a/packages/extension-base/src/services/chain-service/types.ts b/packages/extension-base/src/services/chain-service/types.ts index efc22822cf0..8ab8db81221 100644 --- a/packages/extension-base/src/services/chain-service/types.ts +++ b/packages/extension-base/src/services/chain-service/types.ts @@ -4,10 +4,11 @@ /* eslint @typescript-eslint/no-empty-interface: "off" */ import type { ApiInterfaceRx } from '@polkadot/api/types'; -import { BitcoinApiStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; + import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _CrowdloanFund } from '@subwallet/chain-list/types'; import { CardanoBalanceItem } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/types'; import { AccountState, TxByMsgResponse } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/types'; +import { 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 { TonWalletContract } from '@subwallet/keyring/types'; import { Cell } from '@ton/core'; @@ -255,4 +256,4 @@ export interface OBResponse { status_code: number, message: string, result: T, -} \ No newline at end of file +} diff --git a/packages/extension-base/src/services/chain-service/utils/index.ts b/packages/extension-base/src/services/chain-service/utils/index.ts index f0c29814383..e8f5eb41b56 100644 --- a/packages/extension-base/src/services/chain-service/utils/index.ts +++ b/packages/extension-base/src/services/chain-service/utils/index.ts @@ -62,23 +62,23 @@ export function _isEqualSmartContractAsset (asset1: _ChainAsset, asset2: _ChainA } export function _isPureEvmChain (chainInfo: _ChainInfo) { - return (!!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo); + return (!!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureSubstrateChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !!chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo); + return (!chainInfo.evmInfo && !!chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureTonChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.tonInfo && !chainInfo.cardanoInfo); + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.tonInfo && !chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureCardanoChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !!chainInfo.cardanoInfo); + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !!chainInfo.cardanoInfo && !chainInfo.bitcoinInfo); } export function _isPureBitcoinChain (chainInfo: _ChainInfo) { - return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !!chainInfo.bitcoinInfo); + return (!chainInfo.evmInfo && !chainInfo.substrateInfo && !chainInfo.tonInfo && !chainInfo.cardanoInfo && !!chainInfo.bitcoinInfo); } export function _getOriginChainOfAsset (assetSlug: string) { diff --git a/packages/extension-base/src/services/fee-service/service.ts b/packages/extension-base/src/services/fee-service/service.ts index f2336f12665..70ee3ab3782 100644 --- a/packages/extension-base/src/services/fee-service/service.ts +++ b/packages/extension-base/src/services/fee-service/service.ts @@ -17,7 +17,8 @@ export default class FeeService { evm: {}, substrate: {}, ton: {}, - cardano: {} + cardano: {}, + bitcoin: {} }; constructor (state: KoniState) { diff --git a/packages/extension-base/src/strategy/api-request-strategy/types.ts b/packages/extension-base/src/strategy/api-request-strategy/types.ts index 90f49d7098c..e53f59fbadb 100644 --- a/packages/extension-base/src/strategy/api-request-strategy/types.ts +++ b/packages/extension-base/src/strategy/api-request-strategy/types.ts @@ -2,27 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 export interface ApiRequestContext { - callRate: number; // limit per interval check - limitRate: number; // max rate per interval check - intervalCheck: number; // interval check in ms - maxRetry: number; // interval check in ms - reduceLimitRate: () => void; - } - - export interface ApiRequestStrategy { - addRequest: (run: ApiRequest['run'], ordinal: number) => Promise; - setContext: (context: ApiRequestContext) => void; - stop: () => void; - } - - export interface ApiRequest { - id: number; - retry: number; // retry < 1 not start, retry === 0 start, retry > 0 number of retry - /** Serve smaller first */ - ordinal: number; - status: 'pending' | 'running'; - run: () => Promise; - resolve: (value: any) => T; - reject: (error?: any) => void; - } - \ No newline at end of file + callRate: number; // limit per interval check + limitRate: number; // max rate per interval check + intervalCheck: number; // interval check in ms + maxRetry: number; // interval check in ms + reduceLimitRate: () => void; +} + +export interface ApiRequestStrategy { + addRequest: (run: ApiRequest['run'], ordinal: number) => Promise; + setContext: (context: ApiRequestContext) => void; + stop: () => void; +} + +export interface ApiRequest { + id: number; + retry: number; // retry < 1 not start, retry === 0 start, retry > 0 number of retry + /** Serve smaller first */ + ordinal: number; + status: 'pending' | 'running'; + run: () => Promise; + resolve: (value: any) => T; + reject: (error?: any) => void; +} diff --git a/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts b/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts index 0323ce8a451..6d43ba9ec5a 100644 --- a/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts +++ b/packages/extension-base/src/strategy/api-request-strategy/utils/index.ts @@ -14,7 +14,6 @@ export const postRequest = (url: string, body: any, headers?: Record, headers?: Record) => { - console.log('getRequest url: ', url); const queryString = params ? Object.keys(params) .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) diff --git a/packages/extension-base/src/types/bitcoin.ts b/packages/extension-base/src/types/bitcoin.ts index f02255673f5..8873948b2b3 100644 --- a/packages/extension-base/src/types/bitcoin.ts +++ b/packages/extension-base/src/types/bitcoin.ts @@ -3,111 +3,110 @@ // https://github.com/leather-wallet/extension/blob/dev/src/app/query/bitcoin/bitcoin-client.ts export interface UtxoResponseItem { - txid: string; - vout: number; - status: { - confirmed: boolean; - block_height?: number; - block_hash?: string; - block_time?: number; - }; - value: number; - } - - // https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts - export interface DetermineUtxosForSpendArgs { - sender: string; - amount: number; - feeRate: number; - recipient: string; - utxos: UtxoResponseItem[]; - } - - interface DetermineUtxosOutput { - value: number; - address?: string; - } - - export interface DetermineUtxosForSpendResult { - filteredUtxos: UtxoResponseItem[]; - inputs: UtxoResponseItem[]; - outputs: DetermineUtxosOutput[], - size: number; - fee: number; - } - - // https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts - export class InsufficientFundsError extends Error { - constructor () { - super('Insufficient funds'); - } - } - // Source: https://github.com/Blockstream/esplora/blob/master/API.md#transaction-format - // --------------- - interface BitcoinTransactionIssuance { - asset_id: string; - is_reissuance: boolean; - asset_blinding_nonce: number; - asset_entropy: number; - contract_hash: string; - assetamount?: number; - assetamountcommitment?: number; - tokenamount?: number; - tokenamountcommitment?: number; - } - - interface BitcoinTransactionPegOut { - genesis_hash: string; - scriptpubkey: string; - scriptpubkey_asm: string; - scriptpubkey_address: string; - } - - export interface BitcoinTransactionStatus { + txid: string; + vout: number; + status: { confirmed: boolean; - block_height?: number | null; - block_hash?: string | null; - block_time?: number | null; - } - - export interface BitcoinTransactionVectorOutput { - scriptpubkey: string; - scriptpubkey_asm: string; - scriptpubkey_type: string; - scriptpubkey_address: string; - value: number; - valuecommitment?: number; - asset?: string; - assetcommitment?: number; - pegout?: BitcoinTransactionPegOut | null; - } - - export interface BitcoinTransactionVectorInput { - inner_redeemscript_asm?: string; - inner_witnessscript_asm?: string; - is_coinbase: boolean; - is_pegin?: boolean; - issuance?: BitcoinTransactionIssuance | null; - prevout: BitcoinTransactionVectorOutput; - scriptsig: string; - scriptsig_asm: string; - sequence: number; - txid: string; - vout: number; - witness: string[]; - } - - export interface BitcoinTx { - fee: number; - locktime: number; - size: number; - status: BitcoinTransactionStatus; - tx_type?: string; - txid: string; - version: number; - vin: BitcoinTransactionVectorInput[]; - vout: BitcoinTransactionVectorOutput[]; - weight: number; + block_height?: number; + block_hash?: string; + block_time?: number; + }; + value: number; +} + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts +export interface DetermineUtxosForSpendArgs { + sender: string; + amount: number; + feeRate: number; + recipient: string; + utxos: UtxoResponseItem[]; +} + +interface DetermineUtxosOutput { + value: number; + address?: string; +} + +export interface DetermineUtxosForSpendResult { + filteredUtxos: UtxoResponseItem[]; + inputs: UtxoResponseItem[]; + outputs: DetermineUtxosOutput[], + size: number; + fee: number; +} + +// https://github.com/leather-wallet/extension/blob/dev/src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts +export class InsufficientFundsError extends Error { + constructor () { + super('Insufficient funds'); } - // --------------- - \ No newline at end of file +} +// Source: https://github.com/Blockstream/esplora/blob/master/API.md#transaction-format +// --------------- +interface BitcoinTransactionIssuance { + asset_id: string; + is_reissuance: boolean; + asset_blinding_nonce: number; + asset_entropy: number; + contract_hash: string; + assetamount?: number; + assetamountcommitment?: number; + tokenamount?: number; + tokenamountcommitment?: number; +} + +interface BitcoinTransactionPegOut { + genesis_hash: string; + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_address: string; +} + +export interface BitcoinTransactionStatus { + confirmed: boolean; + block_height?: number | null; + block_hash?: string | null; + block_time?: number | null; +} + +export interface BitcoinTransactionVectorOutput { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; + valuecommitment?: number; + asset?: string; + assetcommitment?: number; + pegout?: BitcoinTransactionPegOut | null; +} + +export interface BitcoinTransactionVectorInput { + inner_redeemscript_asm?: string; + inner_witnessscript_asm?: string; + is_coinbase: boolean; + is_pegin?: boolean; + issuance?: BitcoinTransactionIssuance | null; + prevout: BitcoinTransactionVectorOutput; + scriptsig: string; + scriptsig_asm: string; + sequence: number; + txid: string; + vout: number; + witness: string[]; +} + +export interface BitcoinTx { + fee: number; + locktime: number; + size: number; + status: BitcoinTransactionStatus; + tx_type?: string; + txid: string; + version: number; + vin: BitcoinTransactionVectorInput[]; + vout: BitcoinTransactionVectorOutput[]; + weight: number; +} +// --------------- diff --git a/packages/extension-base/src/types/fee/base.ts b/packages/extension-base/src/types/fee/base.ts index 477b254924a..572fd02ef91 100644 --- a/packages/extension-base/src/types/fee/base.ts +++ b/packages/extension-base/src/types/fee/base.ts @@ -14,4 +14,4 @@ export interface BaseFeeDetail { export interface BaseFeeTime { time: number; // in milliseconds -} \ No newline at end of file +} diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index 01626c9c3af..0d5c28128dd 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -187,53 +187,3 @@ export function filteredOutTxsUtxos (allTxsUtxos: UtxoResponseItem[], filteredOu return allTxsUtxos.filter((element) => !listFilterOut.includes(`${element.txid}:${element.vout}`)); } - -// export async function getRuneUtxos (bitcoinApi: _BitcoinApi, address: string) { -// const responseRuneUtxos = await bitcoinApi.api.getRuneUtxos(address); -// const runeUtxos: UtxoResponseItem[] = []; - -// responseRuneUtxos.forEach((responseRuneUtxo) => { -// const txid = responseRuneUtxo.txid; -// const vout = responseRuneUtxo.vout; -// const utxoValue = responseRuneUtxo.satoshi; - -// if (txid && vout && utxoValue) { -// const item = { -// txid, -// vout, -// status: { -// confirmed: true // not use in filter out rune utxos -// }, -// value: utxoValue -// } as UtxoResponseItem; - -// runeUtxos.push(item); -// } -// }); - -// return runeUtxos; -// } - -// export async function getInscriptionUtxos (bitcoinApi: _BitcoinApi, address: string) { -// try { -// const inscriptions = await bitcoinApi.api.getAddressInscriptions(address); - -// return inscriptions.map((inscription) => { -// const [txid, vout] = inscription.output.split(':'); - -// return { -// txid, -// vout: parseInt(vout), -// status: { -// confirmed: true, // not use in filter out inscription utxos -// block_height: inscription.genesis_block_height, -// block_hash: inscription.genesis_block_hash, -// block_time: inscription.genesis_timestamp -// }, -// value: parseInt(inscription.value) -// } as UtxoResponseItem; -// }); -// } catch (e) { -// return []; -// } -// } diff --git a/packages/extension-base/src/utils/index.ts b/packages/extension-base/src/utils/index.ts index a26bd20f238..98a73eb9192 100644 --- a/packages/extension-base/src/utils/index.ts +++ b/packages/extension-base/src/utils/index.ts @@ -409,4 +409,4 @@ export * from './promise'; export * from './registry'; export * from './swap'; export * from './translate'; -export * from './bitcoin'; \ No newline at end of file +export * from './bitcoin'; diff --git a/packages/extension-koni/webpack.shared.cjs b/packages/extension-koni/webpack.shared.cjs index fdd5053ffcc..f73c494cb60 100644 --- a/packages/extension-koni/webpack.shared.cjs +++ b/packages/extension-koni/webpack.shared.cjs @@ -65,7 +65,8 @@ const _additionalEnv = { BLOCKFROST_API_KEY_PREP: JSON.stringify(process.env.BLOCKFROST_API_KEY_PREP), MELD_API_KEY: JSON.stringify(process.env.MELD_API_KEY), MELD_WIZARD_KEY: JSON.stringify(process.env.MELD_WIZARD_KEY), - MELD_TEST_MODE: JSON.stringify(false) + MELD_TEST_MODE: JSON.stringify(false), + BTC_SERVICE_TOKEN: JSON.stringify(process.env.BTC_SERVICE_TOKEN) }; const additionalEnvDict = { diff --git a/packages/webapp/webpack.config.cjs b/packages/webapp/webpack.config.cjs index a2aa5da87aa..11a77d42e26 100644 --- a/packages/webapp/webpack.config.cjs +++ b/packages/webapp/webpack.config.cjs @@ -73,7 +73,8 @@ const _additionalEnv = { UNISWAP_API_KEY: JSON.stringify(process.env.UNISWAP_API_KEY), SUBWALLET_API: JSON.stringify(process.env.SUBWALLET_API), BLOCKFROST_API_KEY_MAIN: JSON.stringify(process.env.BLOCKFROST_API_KEY_MAIN), - BLOCKFROST_API_KEY_PREP: JSON.stringify(process.env.BLOCKFROST_API_KEY_PREP) + BLOCKFROST_API_KEY_PREP: JSON.stringify(process.env.BLOCKFROST_API_KEY_PREP), + BTC_SERVICE_TOKEN: JSON.stringify(process.env.BTC_SERVICE_TOKEN) }; const createConfig = (entry, alias = {}, useSplitChunk = false) => { diff --git a/yarn.lock b/yarn.lock index b484e5af6ee..95335aac2a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6872,7 +6872,7 @@ __metadata: bowser: ^2.11.0 browser-passworder: ^2.0.3 buffer: ^6.0.3 - cross-fetch: ^3.1.5 + cross-fetch: ^4.1.0 dexie: ^3.2.2 dexie-export-import: ^4.0.7 eth-simple-keyring: ^4.2.0 @@ -12999,7 +12999,7 @@ __metadata: languageName: node linkType: hard -"cross-fetch@npm:^3.1.4, cross-fetch@npm:^3.1.5": +"cross-fetch@npm:^3.1.4": version: 3.1.5 resolution: "cross-fetch@npm:3.1.5" dependencies: @@ -13017,6 +13017,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^4.1.0": + version: 4.1.0 + resolution: "cross-fetch@npm:4.1.0" + dependencies: + node-fetch: ^2.7.0 + checksum: c02fa85d59f83e50dbd769ee472c9cc984060c403ee5ec8654659f61a525c1a655eef1c7a35e365c1a107b4e72d76e786718b673d1cb3c97f61d4644cb0a9f9d + languageName: node + linkType: hard + "cross-spawn@npm:^6.0.5": version: 6.0.5 resolution: "cross-spawn@npm:6.0.5" @@ -22330,7 +22339,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.12": +"node-fetch@npm:^2.6.12, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: From bed64c9799c247d9e0962779bf91b0e35eef4ff5 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 8 Apr 2025 18:49:05 +0700 Subject: [PATCH 016/178] [Issue-4228] feat: Support watch-only account for Bitcoin --- .../extension-koni-ui/src/utils/account/account.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index ea8aec358b6..24cd93c8aa5 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -13,11 +13,11 @@ import { MODE_CAN_SIGN } from '@subwallet/extension-koni-ui/constants/signing'; import { AccountAddressType, AccountSignMode, AccountType, BitcoinAccountInfo } from '@subwallet/extension-koni-ui/types'; import { getNetworkKeyByGenesisHash } from '@subwallet/extension-koni-ui/utils/chain/getNetworkJsonByGenesisHash'; import { AccountInfoByNetwork } from '@subwallet/extension-koni-ui/utils/types'; -import { isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; +import { decodeAddress, encodeAddress, getKeypairTypeByAddress, isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; import { BitcoinTestnetKeypairTypes, KeypairType } from '@subwallet/keyring/types'; import { Web3LogoMap } from '@subwallet/react-ui/es/config-provider/context'; -import { decodeAddress, encodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; +import { isEthereumAddress } from '@polkadot/util-crypto'; import { isChainInfoAccordantAccountChainType } from '../chain'; import { getLogoByNetworkKey } from '../common'; @@ -126,7 +126,7 @@ export const funcSortByName = (a: AbstractAddressJson, b: AbstractAddressJson) = return ((a?.name || '').toLowerCase() > (b?.name || '').toLowerCase()) ? 1 : -1; }; -export const findContactByAddress = (contacts: AbstractAddressJson[], address?: string): AbstractAddressJson | null => { +export const findContactByAddress = (contacts: AbstractAddressJson[], address?: string, networkPrefix = 42): AbstractAddressJson | null => { try { const isAllAccount = address && isAccountAll(address); @@ -134,7 +134,10 @@ export const findContactByAddress = (contacts: AbstractAddressJson[], address?: return null; } - const originAddress = isAccountAll(address) ? address : isEthereumAddress(address) ? address : encodeAddress(decodeAddress(address)); + const type: KeypairType = getKeypairTypeByAddress(address); + + const originAddress = isAccountAll(address) ? address : isEthereumAddress(address) ? address : encodeAddress(decodeAddress(address), networkPrefix, type); + const result = contacts.find((contact) => contact.address.toLowerCase() === originAddress.toLowerCase()); return result || null; From 80e98cdd9cc336b8277c9fd09778078cf1c84ad6 Mon Sep 17 00:00:00 2001 From: Phong Le Nhat Date: Tue, 8 Apr 2025 19:14:24 +0700 Subject: [PATCH 017/178] get balance incriptions address through proxy --- .../helpers/subscribe/bitcoin.ts | 16 +-- .../helpers/subscribe/index.ts | 2 +- .../bitcoin/strategy/BlockStream/index.ts | 35 +++++- .../handler/bitcoin/strategy/types.ts | 4 +- .../src/services/hiro-service/index.ts | 115 ++++++++++++++++++ .../src/services/hiro-service/utils/index.ts | 87 +++++++++++++ .../src/utils/bitcoin/utxo-management.ts | 24 ++++ 7 files changed, 272 insertions(+), 11 deletions(-) create mode 100644 packages/extension-base/src/services/hiro-service/index.ts create mode 100644 packages/extension-base/src/services/hiro-service/utils/index.ts diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index ce257970c75..1bd084ec5c7 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -5,18 +5,20 @@ import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types import { BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; -// import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; -// import { filteredOutTxsUtxos } from '@subwallet/extension-base/utils'; +import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; +import { filteredOutTxsUtxos, getInscriptionUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { try { // const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ - const [utxos] = await Promise.all([ - await bitcoinApi.api.getUtxos(address) + const [utxos, inscriptionUtxos] = await Promise.all([ + // const [utxos] = await Promise.all([ + await bitcoinApi.api.getUtxos(address), + await getInscriptionUtxos(bitcoinApi, address) ]); - // let filteredUtxos: UtxoResponseItem[]; + let filteredUtxos: UtxoResponseItem[] = []; if (!utxos || !utxos.length) { return []; @@ -29,9 +31,9 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre // filteredUtxos = filteredOutTxsUtxos(utxos, runeTxsUtxos); // filter out inscription utxos - // filteredUtxos = filteredOutTxsUtxos(filteredUtxos, inscriptionUtxos); + filteredUtxos = filteredOutTxsUtxos(utxos, inscriptionUtxos); - return utxos; + return filteredUtxos; } catch (error) { console.log('Error while fetching Bitcoin balances', error); diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index aa324dc6d1b..158e81cff9a 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -193,7 +193,7 @@ export function subscribeBalance ( if (_isPureBitcoinChain(chainInfo)) { return subscribeBitcoinBalance( - ['bc1p2v22jvkpr4r5shne4t7dczepsnf4tzeq7q743htlkjql9pj4q4hsmw3xte'], + ['bc1p24z4682fqmnfarcwzmxuzpslyj2f6r8yu2xnm8j7yy5w085kzrcst2mskw'], bitcoinApi ); } 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 e5f8eeab2cc..1491fc37685 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 @@ -3,9 +3,10 @@ import { SWError } from '@subwallet/extension-base/background/errors/SWError'; import { _BTC_SERVICE_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; -import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, RecommendedFeeEstimates, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { BitcoinApiStrategy, BitcoinTransactionEventMap } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { OBResponse } from '@subwallet/extension-base/services/chain-service/types'; +import { HiroService } from '@subwallet/extension-base/services/hiro-service'; import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; import { getRequest, postRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; @@ -242,6 +243,38 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement }, 0); } + async getAddressInscriptions (address: string) { + const inscriptionsFullList: Inscription[] = []; + const pageSize = 60; + let offset = 0; + + const hiroService = HiroService.getInstance(this.isTestnet); + + try { + while (true) { + const response = await hiroService.getAddressInscriptionsInfo({ + limit: String(pageSize), + offset: String(offset), + address: String(address) + }) as unknown as InscriptionFetchedData; + + const inscriptions = response.results; + + if (inscriptions.length !== 0) { + inscriptionsFullList.push(...inscriptions); + offset += pageSize; + } else { + break; + } + } + + return inscriptionsFullList; + } catch (error) { + console.error(`Failed to get ${address} inscriptions`, error); + throw error; + } + } + getTxHex (txHash: string): Promise { return this.addRequest(async (): Promise => { const _rs = await getRequest(this.getUrl(`tx/${txHash}/hex`), undefined, this.headers); 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 13332ed6cac..2f5db7b0b5f 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 @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { BitcoinAddressSummaryInfo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { BitcoinAddressSummaryInfo, Inscription } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { ApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy/types'; import { BitcoinFeeInfo, BitcoinTransactionStatus, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; import EventEmitter from 'eventemitter3'; @@ -13,7 +13,7 @@ export interface BitcoinApiStrategy extends Omit; // noted: all rune utxos come in account // getRuneUtxos (address: string): Promise; // getAddressBRC20FreeLockedBalance (address: string, ticker: string): Promise; - // getAddressInscriptions (address: string): Promise + getAddressInscriptions (address: string): Promise getAddressTransaction (address: string, limit?: number): Promise; getTransactionStatus (txHash: string): Promise; getTransactionDetail (txHash: string): Promise; diff --git a/packages/extension-base/src/services/hiro-service/index.ts b/packages/extension-base/src/services/hiro-service/index.ts new file mode 100644 index 00000000000..590cb7506be --- /dev/null +++ b/packages/extension-base/src/services/hiro-service/index.ts @@ -0,0 +1,115 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { SWError } from '@subwallet/extension-base/background/errors/SWError'; +import { _BTC_SERVICE_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; +import { Brc20BalanceFetchedData, Brc20MetadataFetchedData, InscriptionFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { OBResponse } from '@subwallet/extension-base/services/chain-service/types'; +import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; +import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; +import { getRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; + +const OPENBIT_URL = 'https://api.openbit.app'; +const OPENBIT_URL_TEST = 'https://api-testnet.openbit.app'; + +export class HiroService extends BaseApiRequestStrategy { + baseUrl: string; + + private constructor (url: string) { + const context = new BaseApiRequestContext(); + + super(context); + + this.baseUrl = 'https://btc-api.koni.studio'; + } + + private headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${_BTC_SERVICE_TOKEN}` + }; + + isRateLimited (): boolean { + return false; + } + + getUrl (path: string): string { + return `${this.baseUrl}/${path}`; + } + + getBRC20Metadata (ticker: string): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`brc-20/tokens/${ticker}`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('HiroService.getBRC20Metadata', rs.message); + } + + return rs.result; + }, 3); + } + + getAddressBRC20BalanceInfo (address: string, params: Record): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`brc-20/balances/${address}`), params, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('HiroService.getAddressBRC20BalanceInfo', rs.message); + } + + return rs.result; + }, 3); + } + + getAddressInscriptionsInfo (params: Record): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl('inscriptions'), params, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('HiroService.getAddressInscriptionsInfo', rs.message); + } + + return rs.result; + }, 0); + } + + getInscriptionContent (inscriptionId: string): Promise> { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`inscriptions/${inscriptionId}/content`), undefined, this.headers); + const rs = await _rs.json() as OBResponse>; + + if (rs.status_code !== 200) { + throw new SWError('HiroService.getInscriptionContent', rs.message); + } + + return rs.result; + }, 0); + } + + // todo: handle token authen for url preview + getPreviewUrl (inscriptionId: string) { + return `${OPENBIT_URL}/inscriptions/${inscriptionId}/content`; + } + + // Singleton + private static mainnet: HiroService; + private static testnet: HiroService; + + public static getInstance (isTestnet = false) { + if (isTestnet) { + if (!HiroService.testnet) { + HiroService.testnet = new HiroService(OPENBIT_URL_TEST); + } + + return HiroService.testnet; + } else { + if (!HiroService.mainnet) { + HiroService.mainnet = new HiroService(OPENBIT_URL); + } + + return HiroService.mainnet; + } + } +} diff --git a/packages/extension-base/src/services/hiro-service/utils/index.ts b/packages/extension-base/src/services/hiro-service/utils/index.ts new file mode 100644 index 00000000000..ec24aebb45d --- /dev/null +++ b/packages/extension-base/src/services/hiro-service/utils/index.ts @@ -0,0 +1,87 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { Brc20Metadata, InscriptionFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { HiroService } from '@subwallet/extension-base/services/hiro-service'; + +// todo: handle inscription testnet +export async function getBrc20Metadata (isTestnet = false, ticker: string) { + const hiroService = HiroService.getInstance(isTestnet); + const defaultMetadata = { + ticker: '', + decimals: 0 + } as Brc20Metadata; + + try { + const response = await hiroService.getBRC20Metadata(ticker); + const rs = response?.token; + + if (rs) { + return { + ticker: rs.ticker, + decimals: rs.decimals + } as Brc20Metadata; + } + + return defaultMetadata; + } catch (error) { + console.log(`Error on request brc20 metadata with ticker ${ticker}`); + + return defaultMetadata; + } +} + +export async function getInscriptionContent (isTestnet: boolean, inscriptionId: string) { + const hiroService = HiroService.getInstance(isTestnet); + + try { + return await hiroService.getInscriptionContent(inscriptionId); + } catch (error) { + console.log(`Error on request inscription ${inscriptionId} content`); + + return {}; + } +} + +// todo: handle large inscriptions +export async function getAddressInscriptions (address: string, isTestnet: boolean, offset = 0, limit = 25) { + const hiroService = HiroService.getInstance(isTestnet); + + try { + const response = await hiroService.getAddressInscriptionsInfo({ + limit: String(limit), + offset: String(offset), + address: String(address) + }) as unknown as InscriptionFetchedData; + + return response.results; + } catch (error) { + console.error(`Failed to get ${address} inscriptions with offset ${offset} and limit ${limit}`, error); + throw error; + } +} + +export function getPreviewUrl (inscriptionId: string) { + const hiroService = HiroService.getInstance(); + + try { + return hiroService.getPreviewUrl(inscriptionId); + } catch (error) { + console.error(`Failed to get inscription ${inscriptionId} preview url`, error); + throw error; + } +} + +export function isValidBrc20Ticker (ticker: string) { + const bytesLength = getByteLength(ticker); + + return bytesLength === 4 || bytesLength === 5; +} + +function getByteLength (str: string): number { + const encoder = new TextEncoder(); + const encodedStr = encoder.encode(str); + + // Return the length of the encoded array, which represents the number of bytes + return encodedStr.length; +} diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index 0d5c28128dd..5b91d1e8014 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -187,3 +187,27 @@ export function filteredOutTxsUtxos (allTxsUtxos: UtxoResponseItem[], filteredOu return allTxsUtxos.filter((element) => !listFilterOut.includes(`${element.txid}:${element.vout}`)); } + +export async function getInscriptionUtxos (bitcoinApi: _BitcoinApi, address: string) { + try { + const inscriptions = await bitcoinApi.api.getAddressInscriptions(address); + + return inscriptions.map((inscription) => { + const [txid, vout] = inscription.output.split(':'); + + return { + txid, + vout: parseInt(vout), + status: { + confirmed: true, // not use in filter out inscription utxos + block_height: inscription.genesis_block_height, + block_hash: inscription.genesis_block_hash, + block_time: inscription.genesis_timestamp + }, + value: parseInt(inscription.value) + } as UtxoResponseItem; + }); + } catch (e) { + return []; + } +} From ff0f32d5a0609c50abcc2b3f32e629b986981e65 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 9 Apr 2025 11:13:16 +0700 Subject: [PATCH 018/178] [Issue-4200] feat: Add cardano validation when importing seed phrase --- .../src/services/keyring-service/context/handlers/Mnemonic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts b/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts index 9424abe8978..e2dc6e4beb7 100644 --- a/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts +++ b/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts @@ -57,7 +57,7 @@ export class AccountMnemonicHandler extends AccountBaseHandler { assert(mnemonicValidate(phrase), t('Invalid seed phrase. Please try again.')); mnemonicTypes = 'general'; - pairTypes = ['sr25519', 'ethereum', 'ton', 'bitcoin-44', 'bitcoin-84', 'bitcoin-86', 'bittest-44', 'bittest-84', 'bittest-86']; + pairTypes = ['sr25519', 'ethereum', 'ton', 'cardano', 'bitcoin-44', 'bitcoin-84', 'bitcoin-86', 'bittest-44', 'bittest-84', 'bittest-86']; } catch (e) { assert(tonMnemonicValidate(phrase), t('Invalid seed phrase. Please try again.')); mnemonicTypes = 'ton'; From 487725aac7bb35a8aa11b06946228fe05a4e7c3d Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 10 Apr 2025 19:17:14 +0700 Subject: [PATCH 019/178] [Issue-4201] feat: Migrate unifed account to support Bitcoin --- .../keyring-service/context/handlers/Migration.ts | 4 ++-- .../extension-base/src/types/account/info/keyring.ts | 2 +- .../extension-base/src/utils/account/transform.ts | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/extension-base/src/services/keyring-service/context/handlers/Migration.ts b/packages/extension-base/src/services/keyring-service/context/handlers/Migration.ts index 407d9d42d62..2e2c21baada 100644 --- a/packages/extension-base/src/services/keyring-service/context/handlers/Migration.ts +++ b/packages/extension-base/src/services/keyring-service/context/handlers/Migration.ts @@ -92,7 +92,7 @@ export class AccountMigrationHandler extends AccountBaseHandler { }).result; const newChainTypes = Object.values(AccountChainType).filter((type) => !unifiedAccount.chainTypes.includes(type) && SUPPORTED_ACCOUNT_CHAIN_TYPES.includes(type)); - const keypairTypes = newChainTypes.map((chainType) => getDefaultKeypairTypeFromAccountChainType(chainType)); + const keypairTypes = newChainTypes.flatMap((chainType) => getDefaultKeypairTypeFromAccountChainType(chainType)); keypairTypes.forEach((type) => { const suri = getSuri(mnemonic, type); @@ -208,7 +208,7 @@ export class AccountMigrationHandler extends AccountBaseHandler { try { const mnemonic = this.parentService.context.exportAccountProxyMnemonic({ password, proxyId: firstAccountOldProxyId }).result; - const keypairTypes = SUPPORTED_ACCOUNT_CHAIN_TYPES.map((chainType) => getDefaultKeypairTypeFromAccountChainType(chainType as AccountChainType)); + const keypairTypes = SUPPORTED_ACCOUNT_CHAIN_TYPES.flatMap((chainType) => getDefaultKeypairTypeFromAccountChainType(chainType as AccountChainType)); keypairTypes.forEach((type) => { const suri = getSuri(mnemonic, type); diff --git a/packages/extension-base/src/types/account/info/keyring.ts b/packages/extension-base/src/types/account/info/keyring.ts index a092645719d..933dcc7e010 100644 --- a/packages/extension-base/src/types/account/info/keyring.ts +++ b/packages/extension-base/src/types/account/info/keyring.ts @@ -135,7 +135,7 @@ export const ACCOUNT_CHAIN_TYPE_ORDINAL_MAP: Record = { [AccountChainType.BITCOIN]: 5 }; -export const SUPPORTED_ACCOUNT_CHAIN_TYPES = ['substrate', 'ethereum', 'ton', 'cardano']; +export const SUPPORTED_ACCOUNT_CHAIN_TYPES = ['substrate', 'ethereum', 'ton', 'cardano', 'bitcoin']; export enum AccountActions { DERIVE = 'DERIVE', diff --git a/packages/extension-base/src/utils/account/transform.ts b/packages/extension-base/src/utils/account/transform.ts index 6f1d6637bb3..7607d0fd2d6 100644 --- a/packages/extension-base/src/utils/account/transform.ts +++ b/packages/extension-base/src/utils/account/transform.ts @@ -58,17 +58,17 @@ export const getAccountChainTypeFromKeypairType = (type: KeypairType): AccountCh : AccountChainType.SUBSTRATE; }; -export const getDefaultKeypairTypeFromAccountChainType = (type: AccountChainType): KeypairType => { +export const getDefaultKeypairTypeFromAccountChainType = (type: AccountChainType): KeypairType[] => { if (type === AccountChainType.ETHEREUM) { - return 'ethereum'; + return ['ethereum']; } else if (type === AccountChainType.TON) { - return 'ton'; + return ['ton']; } else if (type === AccountChainType.BITCOIN) { - return 'bitcoin-84'; + return ['bitcoin-44', 'bitcoin-84', 'bitcoin-86', 'bittest-44', 'bittest-84', 'bittest-86']; } else if (type === AccountChainType.CARDANO) { - return 'cardano'; + return ['cardano']; } else { - return 'sr25519'; + return ['sr25519']; } }; From a52d3b6b7ab76f2d60e58c2690ff01d300cbe29e Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 11 Apr 2025 09:46:42 +0700 Subject: [PATCH 020/178] Revert "[Issue-4228] feat: Support watch-only account for Bitcoin" This reverts commit bed64c9799c247d9e0962779bf91b0e35eef4ff5. --- .../extension-koni-ui/src/utils/account/account.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index 24cd93c8aa5..ea8aec358b6 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -13,11 +13,11 @@ import { MODE_CAN_SIGN } from '@subwallet/extension-koni-ui/constants/signing'; import { AccountAddressType, AccountSignMode, AccountType, BitcoinAccountInfo } from '@subwallet/extension-koni-ui/types'; import { getNetworkKeyByGenesisHash } from '@subwallet/extension-koni-ui/utils/chain/getNetworkJsonByGenesisHash'; import { AccountInfoByNetwork } from '@subwallet/extension-koni-ui/utils/types'; -import { decodeAddress, encodeAddress, getKeypairTypeByAddress, isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; +import { isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; import { BitcoinTestnetKeypairTypes, KeypairType } from '@subwallet/keyring/types'; import { Web3LogoMap } from '@subwallet/react-ui/es/config-provider/context'; -import { isEthereumAddress } from '@polkadot/util-crypto'; +import { decodeAddress, encodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; import { isChainInfoAccordantAccountChainType } from '../chain'; import { getLogoByNetworkKey } from '../common'; @@ -126,7 +126,7 @@ export const funcSortByName = (a: AbstractAddressJson, b: AbstractAddressJson) = return ((a?.name || '').toLowerCase() > (b?.name || '').toLowerCase()) ? 1 : -1; }; -export const findContactByAddress = (contacts: AbstractAddressJson[], address?: string, networkPrefix = 42): AbstractAddressJson | null => { +export const findContactByAddress = (contacts: AbstractAddressJson[], address?: string): AbstractAddressJson | null => { try { const isAllAccount = address && isAccountAll(address); @@ -134,10 +134,7 @@ export const findContactByAddress = (contacts: AbstractAddressJson[], address?: return null; } - const type: KeypairType = getKeypairTypeByAddress(address); - - const originAddress = isAccountAll(address) ? address : isEthereumAddress(address) ? address : encodeAddress(decodeAddress(address), networkPrefix, type); - + const originAddress = isAccountAll(address) ? address : isEthereumAddress(address) ? address : encodeAddress(decodeAddress(address)); const result = contacts.find((contact) => contact.address.toLowerCase() === originAddress.toLowerCase()); return result || null; From 1c4ce9a1d0b75fdd0563caeb2f14b9fed951151c Mon Sep 17 00:00:00 2001 From: S2kael Date: Fri, 11 Apr 2025 16:43:59 +0700 Subject: [PATCH 021/178] [Issue-4200] Add branch to white list auto build --- .github/workflows/push-koni-dev.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index 3d8008410ba..6cc131eedb1 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -5,11 +5,13 @@ on: - koni-dev - upgrade-ui - subwallet-dev + - koni/dev/issue-4200-v2 push: branches: - koni-dev - upgrade-ui - subwallet-dev + - koni/dev/issue-4200-v2 jobs: master: From 204e6dd187dfac6f792d5d46f98ecae8c14b0482 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 11 Apr 2025 17:15:19 +0700 Subject: [PATCH 022/178] [Issue-4200] Update branch to white-list auto build --- .github/workflows/push-koni-dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index 7da7445fa8b..cd11c13c917 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -11,7 +11,6 @@ on: - koni-dev - upgrade-ui - subwallet-dev - - koni/dev/issue-4200-v2 jobs: master: From 3149602150adac54c4a84e741215c610fb51c964 Mon Sep 17 00:00:00 2001 From: Phong Le Nhat Date: Mon, 14 Apr 2025 16:06:19 +0700 Subject: [PATCH 023/178] Add get Rune Balance --- .../helpers/subscribe/bitcoin.ts | 17 ++- .../helpers/subscribe/index.ts | 2 +- .../bitcoin/strategy/BlockStream/index.ts | 47 ++++++- .../bitcoin/strategy/BlockStream/types.ts | 3 +- .../handler/bitcoin/strategy/types.ts | 6 +- .../src/services/chain-service/types.ts | 6 + .../src/services/rune-service/index.ts | 126 ++++++++++++++++++ .../src/utils/bitcoin/utxo-management.ts | 27 ++++ 8 files changed, 220 insertions(+), 14 deletions(-) create mode 100644 packages/extension-base/src/services/rune-service/index.ts diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index 1bd084ec5c7..d7aee6e3ed4 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -5,20 +5,19 @@ import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types import { BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; -import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; -import { filteredOutTxsUtxos, getInscriptionUtxos } from '@subwallet/extension-base/utils'; +import { UtxoResponseItem } from '@subwallet/extension-base/types'; +import { filteredOutTxsUtxos, getInscriptionUtxos, getRuneUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { try { - // const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ - const [utxos, inscriptionUtxos] = await Promise.all([ - // const [utxos] = await Promise.all([ + const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ await bitcoinApi.api.getUtxos(address), + await getRuneUtxos(bitcoinApi, address), await getInscriptionUtxos(bitcoinApi, address) ]); - let filteredUtxos: UtxoResponseItem[] = []; + let filteredUtxos: UtxoResponseItem[]; if (!utxos || !utxos.length) { return []; @@ -28,8 +27,9 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre // filteredUtxos = filterOutPendingTxsUtxos(utxos); // filter out rune utxos - // filteredUtxos = filteredOutTxsUtxos(utxos, runeTxsUtxos); + filteredUtxos = filteredOutTxsUtxos(utxos, runeTxsUtxos); + // filter out dust utxos // filter out inscription utxos filteredUtxos = filteredOutTxsUtxos(utxos, inscriptionUtxos); @@ -49,6 +49,7 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) bitcoinApi.api.getAddressSummaryInfo(address) ]); + console.log('addressSummaryInfo', addressSummaryInfo); const bitcoinBalanceMetadata = { inscriptionCount: addressSummaryInfo.total_inscription } as BitcoinBalanceMetadata; @@ -77,6 +78,8 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) } export const subscribeBitcoinBalance = async (addresses: string[], bitcoinApi: _BitcoinApi) => { + console.log('subscribeBitcoinBalanceBalance', addresses); + const getBalance = async () => { try { const balances = await getBitcoinBalance(bitcoinApi, addresses); diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index 158e81cff9a..d8db16765e5 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -193,7 +193,7 @@ export function subscribeBalance ( if (_isPureBitcoinChain(chainInfo)) { return subscribeBitcoinBalance( - ['bc1p24z4682fqmnfarcwzmxuzpslyj2f6r8yu2xnm8j7yy5w085kzrcst2mskw'], + ['bc1q9uewqm7495ddfcs900yg86ux5tdzzkcphkvd9l'], bitcoinApi ); } 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 1491fc37685..7e86c37c3be 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 @@ -3,10 +3,11 @@ import { SWError } from '@subwallet/extension-base/background/errors/SWError'; import { _BTC_SERVICE_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; -import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData, RuneTxs, RuneTxsResponse, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { BitcoinApiStrategy, BitcoinTransactionEventMap } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { OBResponse } from '@subwallet/extension-base/services/chain-service/types'; import { HiroService } from '@subwallet/extension-base/services/hiro-service'; +import { RunesService } from '@subwallet/extension-base/services/rune-service'; import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; import { getRequest, postRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; @@ -243,6 +244,50 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement }, 0); } + async getRunes (address: string) { + const runesFullList: RunesInfoByAddress[] = []; + const pageSize = 60; + let offset = 0; + + const runeService = RunesService.getInstance(this.isTestnet); + + try { + while (true) { + const response = await runeService.getAddressRunesInfo(address, { + limit: String(pageSize), + offset: String(offset) + }) as unknown as RunesInfoByAddressFetchedData; + + const runes = response.runes; + + if (runes.length !== 0) { + runesFullList.push(...runes); + offset += pageSize; + } else { + break; + } + } + + return runesFullList; + } catch (error) { + console.error(`Failed to get ${address} balances`, error); + throw error; + } + } + + async getRuneUtxos (address: string) { + const runeService = RunesService.getInstance(this.isTestnet); + + try { + const responseRuneUtxos = await runeService.getAddressRuneUtxos(address); + + return responseRuneUtxos.results; + } catch (error) { + console.error(`Failed to get ${address} rune utxos`, error); + throw error; + } + } + async getAddressInscriptions (address: string) { const inscriptionsFullList: Inscription[] = []; const pageSize = 60; diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts index 28b497c3cab..8a9a06f5a7a 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts @@ -247,9 +247,8 @@ export interface BlockStreamTransactionDetail { } export interface RuneUtxoResponse { - start: number, total: number, - utxo: RuneUtxo[] + results: RuneUtxo[] } export interface RuneUtxo { 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 2f5db7b0b5f..3743d75d2ea 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 @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { BitcoinAddressSummaryInfo, Inscription } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { BitcoinAddressSummaryInfo, Inscription, RunesInfoByAddress, RuneUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { ApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy/types'; import { BitcoinFeeInfo, BitcoinTransactionStatus, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; import EventEmitter from 'eventemitter3'; @@ -9,9 +9,9 @@ import EventEmitter from 'eventemitter3'; export interface BitcoinApiStrategy extends Omit { getBlockTime (): Promise; getAddressSummaryInfo (address: string): Promise; - // getRunes (address: string): Promise; + getRunes (address: string): Promise; // getRuneTxsUtxos (address: string): Promise; // noted: all rune utxos come in account - // getRuneUtxos (address: string): Promise; + getRuneUtxos (address: string): Promise; // getAddressBRC20FreeLockedBalance (address: string, ticker: string): Promise; getAddressInscriptions (address: string): Promise getAddressTransaction (address: string, limit?: number): Promise; diff --git a/packages/extension-base/src/services/chain-service/types.ts b/packages/extension-base/src/services/chain-service/types.ts index 8ab8db81221..c4ddc379287 100644 --- a/packages/extension-base/src/services/chain-service/types.ts +++ b/packages/extension-base/src/services/chain-service/types.ts @@ -257,3 +257,9 @@ export interface OBResponse { message: string, result: T, } + +export interface OBRuneResponse { + status_code: number, + message: string, + result: T, +} diff --git a/packages/extension-base/src/services/rune-service/index.ts b/packages/extension-base/src/services/rune-service/index.ts new file mode 100644 index 00000000000..3ae54e65d14 --- /dev/null +++ b/packages/extension-base/src/services/rune-service/index.ts @@ -0,0 +1,126 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { SWError } from '@subwallet/extension-base/background/errors/SWError'; +import { _BTC_SERVICE_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; +import { RuneMetadata, RunesCollectionInfoResponse, RunesInfoByAddressFetchedData, RuneTxsResponse, RuneUtxoResponse } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { OBResponse, OBRuneResponse } from '@subwallet/extension-base/services/chain-service/types'; +import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; +import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; +import { getRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; + +const OPENBIT_URL = 'https://api.openbit.app'; +const OPENBIT_URL_TEST = 'https://api-testnet.openbit.app'; + +export class RunesService extends BaseApiRequestStrategy { + baseUrl: string; + + private constructor (url: string) { + const context = new BaseApiRequestContext(); + + super(context); + + this.baseUrl = 'https://btc-api.koni.studio'; + } + + private headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${_BTC_SERVICE_TOKEN}` + }; + + isRateLimited (): boolean { + return false; + } + + getUrl (path: string): string { + return `${this.baseUrl}/${path}`; + } + + getAddressRunesInfo (address: string, params: Record): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`rune/address/${address}`), params, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('RuneScanService.getAddressRunesInfo', rs.message); + } + + return rs.result; + }, 1); + } + + // * Deprecated + getRuneCollectionsByBatch (params: Record): Promise { + return this.addRequest(async () => { + const url = this.getUrl('rune'); + const rs = await getRequest(url, params); + + if (rs.status !== 200) { + throw new SWError('RuneScanService.getRuneCollectionsByBatch', await rs.text()); + } + + return (await rs.json()) as RunesCollectionInfoResponse; + }, 1); + } + + // * Deprecated + getAddressRuneTxs (address: string, params: Record): Promise { + return this.addRequest(async () => { + const url = this.getUrl(`address/${address}/txs`); + const rs = await getRequest(url, params); + + if (rs.status !== 200) { + throw new SWError('RuneScanService.getAddressRuneTxs', await rs.text()); + } + + return (await rs.json()) as RuneTxsResponse; + }, 0); + } + + getRuneMetadata (runeid: string): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`rune/metadata/${runeid}`), undefined, this.headers); + const rs = await _rs.json() as OBResponse; + + if (rs.status_code !== 200) { + throw new SWError('RuneScanService.getRuneMetadata', rs.message); + } + + return rs.result; + }, 0); + } + + getAddressRuneUtxos (address: string): Promise { + return this.addRequest(async () => { + const _rs = await getRequest(this.getUrl(`rune/address/${address}/rune/utxo`), undefined, this.headers); + + const rs = await _rs.json() as OBRuneResponse; + + if (rs.status_code !== 200) { + throw new SWError('RuneScanService.getAddressRuneUtxos', rs.message); + } + + return rs.result; + }, 0); + } + + // Singleton + private static mainnet: RunesService; + private static testnet: RunesService; + + public static getInstance (isTestnet = false) { + if (isTestnet) { + if (!RunesService.testnet) { + RunesService.testnet = new RunesService(OPENBIT_URL_TEST); + } + + return RunesService.testnet; + } else { + if (!RunesService.mainnet) { + RunesService.mainnet = new RunesService(OPENBIT_URL); + } + + return RunesService.mainnet; + } + } +} diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index 5b91d1e8014..f36513d2df8 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -188,6 +188,33 @@ export function filteredOutTxsUtxos (allTxsUtxos: UtxoResponseItem[], filteredOu return allTxsUtxos.filter((element) => !listFilterOut.includes(`${element.txid}:${element.vout}`)); } +export async function getRuneUtxos (bitcoinApi: _BitcoinApi, address: string) { + const responseRuneUtxos = await bitcoinApi.api.getRuneUtxos(address); + + const runeUtxos: UtxoResponseItem[] = []; + + responseRuneUtxos.forEach((responseRuneUtxo) => { + const txid = responseRuneUtxo.txid; + const vout = responseRuneUtxo.vout; + const utxoValue = responseRuneUtxo.satoshi; + + if (txid && vout && utxoValue) { + const item = { + txid, + vout, + status: { + confirmed: true // not use in filter out rune utxos + }, + value: utxoValue + } as UtxoResponseItem; + + runeUtxos.push(item); + } + }); + + return runeUtxos; +} + export async function getInscriptionUtxos (bitcoinApi: _BitcoinApi, address: string) { try { const inscriptions = await bitcoinApi.api.getAddressInscriptions(address); From fd84153720e0f04340d6590d45d1ba4b8709936a Mon Sep 17 00:00:00 2001 From: nampc Date: Mon, 14 Apr 2025 16:31:28 +0700 Subject: [PATCH 024/178] [Issue 4182] chore: update interfaces --- .../extension-base/src/background/KoniTypes.ts | 6 ++++-- .../helpers/subscribe/bitcoin.ts | 18 +++++++----------- .../balance-service/helpers/subscribe/index.ts | 3 ++- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 6888498e568..6ddfc036e4b 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -2005,9 +2005,11 @@ export interface RequestPingSession { /* Core types */ export type _Address = string; -export type _BalanceMetadata = unknown; +export type _BalanceMetadata = BitcoinBalanceMetadata | unknown; export type BitcoinBalanceMetadata = { - inscriptionCount: number + inscriptionCount: number, + runeBalance: string, // sum of BTC in UTXO which contains rune + inscriptionBalance: string // sum of BTC in UTXO which contains rune } // Use stringify to communicate, pure boolean value will error with case 'false' value diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index d7aee6e3ed4..ce06ac3723d 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -5,7 +5,7 @@ import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types import { BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; -import { UtxoResponseItem } from '@subwallet/extension-base/types'; +import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; import { filteredOutTxsUtxos, getInscriptionUtxos, getRuneUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; @@ -41,7 +41,7 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre } }; -async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) { +async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]): Promise { return await Promise.all(addresses.map(async (address) => { try { const [filteredUtxos, addressSummaryInfo] = await Promise.all([ @@ -77,24 +77,20 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) })); } -export const subscribeBitcoinBalance = async (addresses: string[], bitcoinApi: _BitcoinApi) => { +export const subscribeBitcoinBalance = async (addresses: string[], bitcoinApi: _BitcoinApi, callback: (rs: BalanceItem[]) => void) => { console.log('subscribeBitcoinBalanceBalance', addresses); - const getBalance = async () => { + const getBalance = async (): Promise => { try { const balances = await getBitcoinBalance(bitcoinApi, addresses); - return balances[0].balance; + callback(balances); } catch (e) { - console.error('Error on get Bitcoin balance with token', e); - - return '0'; + console.error('Error while fetching cardano balance', e); } }; - const balanceBTC = await getBalance(); - - console.log('btc balance: ', balanceBTC); + await getBalance(); return () => { console.log('unsub'); diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index d8db16765e5..3ab8b9d2ec4 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -194,7 +194,8 @@ export function subscribeBalance ( if (_isPureBitcoinChain(chainInfo)) { return subscribeBitcoinBalance( ['bc1q9uewqm7495ddfcs900yg86ux5tdzzkcphkvd9l'], - bitcoinApi + bitcoinApi, + callback ); } From 76ff666cd396ce2397aad801d67c9f628e6be2fe Mon Sep 17 00:00:00 2001 From: Phong Le Nhat Date: Mon, 14 Apr 2025 21:01:19 +0700 Subject: [PATCH 025/178] add API get balance rune, inscription --- .../extension-base/src/constants/index.ts | 1 + .../helpers/subscribe/bitcoin.ts | 60 +++++++++++++------ .../bitcoin/strategy/BlockStream/types.ts | 4 +- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/packages/extension-base/src/constants/index.ts b/packages/extension-base/src/constants/index.ts index 3df08116ba3..aeecd59e739 100644 --- a/packages/extension-base/src/constants/index.ts +++ b/packages/extension-base/src/constants/index.ts @@ -10,6 +10,7 @@ export const CRON_AUTO_RECOVER_DOTSAMA_INTERVAL = 60000; export const CRON_AUTO_RECOVER_WEB3_INTERVAL = 90000; export const ACALA_REFRESH_CROWDLOAN_INTERVAL = 300000; export const ASTAR_REFRESH_BALANCE_INTERVAL = 60000; +export const BITCOIN_REFRESH_BALANCE_INTERVAL = 600000; export const SUB_TOKEN_REFRESH_BALANCE_INTERVAL = 60000; export const CRON_REFRESH_NFT_INTERVAL = 7200000; export const CRON_REFRESH_MKT_CAMPAIGN_INTERVAL = 15 * BASE_MINUTE_INTERVAL; diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index ce06ac3723d..f6dbeb32c92 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; -import { BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; +import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; +import { BITCOIN_REFRESH_BALANCE_INTERVAL } from '@subwallet/extension-base/constants'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; @@ -41,7 +42,7 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre } }; -async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]): Promise { +async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) { return await Promise.all(addresses.map(async (address) => { try { const [filteredUtxos, addressSummaryInfo] = await Promise.all([ @@ -51,7 +52,9 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]): console.log('addressSummaryInfo', addressSummaryInfo); const bitcoinBalanceMetadata = { - inscriptionCount: addressSummaryInfo.total_inscription + inscriptionCount: addressSummaryInfo.total_inscription, + runeBalance: addressSummaryInfo.balance_rune, + inscriptionBalance: addressSummaryInfo.balance_inscription } as BitcoinBalanceMetadata; let balanceValue = new BigN(0); @@ -77,22 +80,45 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]): })); } -export const subscribeBitcoinBalance = async (addresses: string[], bitcoinApi: _BitcoinApi, callback: (rs: BalanceItem[]) => void) => { - console.log('subscribeBitcoinBalanceBalance', addresses); - - const getBalance = async (): Promise => { - try { - const balances = await getBitcoinBalance(bitcoinApi, addresses); - - callback(balances); - } catch (e) { - console.error('Error while fetching cardano balance', e); - } +export function subscribeBitcoinBalance (addresses: string[], bitcoinApi: _BitcoinApi, callback: (rs: BalanceItem[]) => void): () => void { + const getBalance = () => { + getBitcoinBalance(bitcoinApi, addresses) + .then((balances) => { + return balances.map(({ balance, bitcoinBalanceMetadata }, index): BalanceItem => { + return { + address: addresses[index], + tokenSlug: 'bitcoin', + state: APIItemState.READY, + free: balance, + locked: '0', + metadata: bitcoinBalanceMetadata + }; + }); + }) + .catch((e) => { + console.error('Error on get Bitcoin balance with token bitcoin', e); + + return addresses.map((address): BalanceItem => { + return { + address: address, + tokenSlug: 'bitcoin', + state: APIItemState.READY, + free: '0', + locked: '0' + }; + }); + }) + .then((items) => { + callback(items); + }) + .catch(console.error); }; - await getBalance(); + const interval = setInterval(getBalance, BITCOIN_REFRESH_BALANCE_INTERVAL); + + getBalance(); return () => { - console.log('unsub'); + clearInterval(interval); }; -}; +} diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts index 8a9a06f5a7a..e907f5e69e4 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts @@ -34,7 +34,9 @@ export interface BitcoinAddressSummaryInfo { tx_count: number }, balance: number, - total_inscription: number + total_inscription: number, + balance_rune: string, + balance_inscription: string, } // todo: combine RunesByAddressResponse & RunesCollectionInfoResponse From 31919537fca1335a633daff82e40075d4e85320a Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 15 Apr 2025 15:26:25 +0700 Subject: [PATCH 026/178] [Issue-4262] refactor: Fix potential rendering issues with lists in React --- .../src/Popup/Account/RestoreJson/index.tsx | 22 +++++++++---------- .../SeedPhraseTermModal.tsx | 1 + 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Account/RestoreJson/index.tsx b/packages/extension-koni-ui/src/Popup/Account/RestoreJson/index.tsx index 2c9d4ddcef0..87efd7ff26f 100644 --- a/packages/extension-koni-ui/src/Popup/Account/RestoreJson/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Account/RestoreJson/index.tsx @@ -390,7 +390,7 @@ const Component: React.FC = ({ className }: Props) => { return (
{(item as ListItemGroupLabel).groupLabel}
@@ -398,17 +398,15 @@ const Component: React.FC = ({ className }: Props) => { } return ( - <> - - + ); }, [accountProxiesSelected, onSelect, submitting]); diff --git a/packages/extension-koni-ui/src/components/Modal/TermsAndConditions/SeedPhraseTermModal.tsx b/packages/extension-koni-ui/src/components/Modal/TermsAndConditions/SeedPhraseTermModal.tsx index 28f1bc8c963..b64f1f742ef 100644 --- a/packages/extension-koni-ui/src/components/Modal/TermsAndConditions/SeedPhraseTermModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/TermsAndConditions/SeedPhraseTermModal.tsx @@ -81,6 +81,7 @@ const Component = ({ className }: Props) => { return ( Date: Tue, 15 Apr 2025 16:52:46 +0700 Subject: [PATCH 027/178] [Issue-4228] chore: update Keyring library --- package.json | 4 ++-- yarn.lock | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index bcaa1ee2f64..910f08db471 100644 --- a/package.json +++ b/package.json @@ -107,9 +107,9 @@ "@polkadot/util-crypto": "^13.4.3", "@polkadot/x-global": "^13.4.3", "@subwallet/chain-list": "0.2.102", - "@subwallet/keyring": "^0.1.9", + "@subwallet/keyring": "^0.1.10", "@subwallet/react-ui": "5.1.2-b79", - "@subwallet/ui-keyring": "^0.1.9", + "@subwallet/ui-keyring": "^0.1.10", "@types/bn.js": "^5.1.6", "@zondax/ledger-substrate": "1.0.1", "babel-core": "^7.0.0-bridge.0", diff --git a/yarn.lock b/yarn.lock index 95335aac2a0..25f1f8c75b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7200,9 +7200,9 @@ __metadata: languageName: unknown linkType: soft -"@subwallet/keyring@npm:^0.1.9": - version: 0.1.9 - resolution: "@subwallet/keyring@npm:0.1.9" +"@subwallet/keyring@npm:^0.1.10": + version: 0.1.10 + resolution: "@subwallet/keyring@npm:0.1.10" dependencies: "@emurgo/cardano-serialization-lib-nodejs": ^13.2.0 "@ethereumjs/tx": ^5.0.0 @@ -7223,7 +7223,7 @@ __metadata: rxjs: ^7.5.6 tiny-secp256k1: ^2.2.3 tslib: ^2.6.2 - checksum: a04f8370c5af418e0d9220ad405dd2f376d5879b72a8cebfa214109cd52af173713df5d1e25ec92edae6d1df6871e59b90d1694eef57d2c5b0e039502631c2ee + checksum: 30ac451ff1d16f0b8deaf85a430e869e11a779f8c606f140459e8d16304c7ee493ca3c95368828601cf93e564b800be3bd8a4816dbd43b5554c3cdbed736e641 languageName: node linkType: hard @@ -7314,19 +7314,19 @@ __metadata: languageName: unknown linkType: soft -"@subwallet/ui-keyring@npm:^0.1.9": - version: 0.1.9 - resolution: "@subwallet/ui-keyring@npm:0.1.9" +"@subwallet/ui-keyring@npm:^0.1.10": + version: 0.1.10 + resolution: "@subwallet/ui-keyring@npm:0.1.10" dependencies: "@babel/runtime": ^7.20.1 "@polkadot/ui-settings": 2.9.14 "@polkadot/util": ^12.2.1 "@polkadot/util-crypto": ^12.2.1 - "@subwallet/keyring": ^0.1.8 + "@subwallet/keyring": ^0.1.10 mkdirp: ^1.0.4 rxjs: ^7.5.7 store: ^2.0.12 - checksum: 332502def6742a386e8c2001ea82ded2dfbcba274df669cb3b8166945ea975ca22b6e2a758733cc9cbbb38dca18a956ef92e6e7c45b85cb55f053d6a45bc07f9 + checksum: 48c4412d30f3b1fbbdffc546be5829067de83ad595a5d095eae59a2fb28f4ed5331dfed507a02e36e8a509b8b562cdaafe0d29be1e43a5d5bc3c3a2e295417ee languageName: node linkType: hard From 8c8845cfa98847c7fd50f04be3f887a8b980c868 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 16 Apr 2025 10:10:49 +0700 Subject: [PATCH 028/178] [Issue-4168] chore: resolve conflict after merging branch dev --- packages/extension-base/src/services/chain-service/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 9db8918f8fa..598fc3d3f66 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -13,7 +13,7 @@ import { SubstrateChainHandler } from '@subwallet/extension-base/services/chain- import { TonChainHandler } from '@subwallet/extension-base/services/chain-service/handler/TonChainHandler'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _CUSTOM_PREFIX, _DataMap, _EvmApi, _NetworkUpsertParams, _NFT_CONTRACT_STANDARDS, _SMART_CONTRACT_STANDARDS, _SmartContractTokenInfo, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isPureEvmChain, _isPureSubstrateChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isPureBitcoinChain, _isPureEvmChain, _isPureSubstrateChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { EventService } from '@subwallet/extension-base/services/event-service'; import { MYTHOS_MIGRATION_KEY } from '@subwallet/extension-base/services/migration-service/scripts'; import { IChain, IMetadataItem, IMetadataV15Item } from '@subwallet/extension-base/services/storage-service/databases'; @@ -1585,7 +1585,6 @@ export class ChainService { let evmInfo: _EvmInfo | null = null; const tonInfo: _TonInfo | null = null; const cardanoInfo: _CardanoInfo | null = null; - const bitcoinInfo: _BitcoinInfo | null = null; if (params.chainSpec.genesisHash !== '') { substrateInfo = { From 02d41142b67c3620c0668fc35efecaba3bfdc4cf Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 16 Apr 2025 14:45:39 +0700 Subject: [PATCH 029/178] [Issue-4200] feat: Hide RUNE and BRC-20 tokens --- .../src/services/chain-service/index.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 598fc3d3f66..d5ac7f0b16f 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AssetLogoMap, AssetRefMap, ChainAssetMap, ChainInfoMap, ChainLogoMap, MultiChainAssetMap } from '@subwallet/chain-list'; -import { _AssetRef, _AssetRefPath, _AssetType, _BitcoinInfo, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo } from '@subwallet/chain-list/types'; +import { _AssetRef, _AssetRefPath, _AssetType, _CardanoInfo, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo } from '@subwallet/chain-list/types'; import { AssetSetting, MetadataItem, TokenPriorityDetails, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes'; import { _DEFAULT_ACTIVE_CHAINS, _ZK_ASSET_PREFIX, LATEST_CHAIN_DATA_FETCHING_INTERVAL } from '@subwallet/extension-base/services/chain-service/constants'; import { BitcoinChainHandler } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/BitcoinChainHandler'; @@ -13,7 +13,7 @@ import { SubstrateChainHandler } from '@subwallet/extension-base/services/chain- import { TonChainHandler } from '@subwallet/extension-base/services/chain-service/handler/TonChainHandler'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _CUSTOM_PREFIX, _DataMap, _EvmApi, _NetworkUpsertParams, _NFT_CONTRACT_STANDARDS, _SMART_CONTRACT_STANDARDS, _SmartContractTokenInfo, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isPureBitcoinChain, _isPureEvmChain, _isPureSubstrateChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetOriginChain, _getTokenOnChainAssetId, _isAssetAutoEnable, _isAssetCanPayTxFee, _isAssetFungibleToken, _isChainBitcoinCompatible, _isChainEnabled, _isCustomAsset, _isCustomChain, _isCustomProvider, _isEqualContractAddress, _isEqualSmartContractAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isPureBitcoinChain, _isPureEvmChain, _isPureSubstrateChain, _parseAssetRefKey, randomizeProvider, updateLatestChainInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { EventService } from '@subwallet/extension-base/services/event-service'; import { MYTHOS_MIGRATION_KEY } from '@subwallet/extension-base/services/migration-service/scripts'; import { IChain, IMetadataItem, IMetadataV15Item } from '@subwallet/extension-base/services/storage-service/databases'; @@ -57,7 +57,15 @@ const ignoredList = [ export const filterAssetInfoMap = (chainInfo: Record, assets: Record, addedChains?: string[]): Record => { return Object.fromEntries( Object.entries(assets) - .filter(([, info]) => chainInfo[info.originChain] || addedChains?.includes(info.originChain)) + .filter(([, info]) => { + const isBitcoinChain = chainInfo?.[info.originChain] && _isChainBitcoinCompatible(chainInfo[info.originChain]); + + if (isBitcoinChain) { + return ![_AssetType.RUNE, _AssetType.BRC20].includes(info.assetType); + } + + return chainInfo[info.originChain] || addedChains?.includes(info.originChain); + }) ); }; From 670d6b6c41b6454df2aa9b44f49d133cca9f5a7e Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 17 Apr 2025 18:42:18 +0700 Subject: [PATCH 030/178] [Issue-4094] Extension - Improvements unified account after Bitcoin supported --- .../Modal/Global/AddressGroupModal.tsx | 150 +++++++++++++++++ .../Modal/Global/SelectAddressFormatModal.tsx | 23 ++- .../src/components/Modal/Global/index.ts | 1 + .../components/TokenItem/AddressGroupItem.tsx | 154 ++++++++++++++++++ .../extension-koni-ui/src/constants/modal.ts | 1 + .../contexts/WalletModalContextProvider.tsx | 45 ++++- .../screen/home/useCoreReceiveModalHelper.tsx | 112 +++++++++---- 7 files changed, 440 insertions(+), 46 deletions(-) create mode 100644 packages/extension-koni-ui/src/components/Modal/Global/AddressGroupModal.tsx create mode 100644 packages/extension-koni-ui/src/components/TokenItem/AddressGroupItem.tsx diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AddressGroupModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AddressGroupModal.tsx new file mode 100644 index 00000000000..fd24b26d5d7 --- /dev/null +++ b/packages/extension-koni-ui/src/components/Modal/Global/AddressGroupModal.tsx @@ -0,0 +1,150 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { AccountJson } from '@subwallet/extension-base/types'; +import { CloseIcon, GeneralEmptyList } from '@subwallet/extension-koni-ui/components'; +import AddressGroupItem from '@subwallet/extension-koni-ui/components/TokenItem/AddressGroupItem'; +import { ADDRESS_GROUP_MODAL } from '@subwallet/extension-koni-ui/constants/modal'; +import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; +import { useNotification } from '@subwallet/extension-koni-ui/hooks'; +import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { copyToClipboard } from '@subwallet/extension-koni-ui/utils'; +import { Icon, SwList, SwModal } from '@subwallet/react-ui'; +import CN from 'classnames'; +import { CaretLeft, FadersHorizontal } from 'phosphor-react'; +import React, { useCallback, useContext } from 'react'; +import styled from 'styled-components'; + +export type AddressGroupItemInfo = { + accountInfo: AccountJson; + tokenSlug: string; + chainSlug: string; +} + +export interface AddressGroupModalProps { + items: AddressGroupItemInfo[]; + onBack?: VoidFunction; + onCancel?: VoidFunction; +} + +type Props = ThemeProps & AddressGroupModalProps & { + onCancel: VoidFunction; +}; + +const modalId = ADDRESS_GROUP_MODAL; + +const Component: React.FC = ({ className, items, onBack, onCancel }: Props) => { + const { t } = useTranslation(); + const notify = useNotification(); + const { addressQrModal } = useContext(WalletModalContext); + + const onShowQr = useCallback((item: AddressGroupItemInfo) => { + return () => { + const processFunction = () => { + addressQrModal.open({ + address: item.accountInfo.address, + chainSlug: item.chainSlug, + onBack: addressQrModal.close, + onCancel: () => { + addressQrModal.close(); + onCancel(); + } + }); + }; + + processFunction(); + }; + }, [addressQrModal, onCancel]); + + const onCopyAddress = useCallback((item: AddressGroupItemInfo) => { + return () => { + const processFunction = () => { + copyToClipboard(item.accountInfo.address || ''); + notify({ + message: t('Copied to clipboard') + }); + }; + + processFunction(); + }; + }, [notify, t]); + + const renderItem = useCallback( + (item: AddressGroupItemInfo) => { + return ( + + ); + }, + [onCopyAddress, onShowQr] + ); + + const renderEmpty = useCallback(() => { + return ; + }, []); + + return ( + + ) + : undefined + } + destroyOnClose={true} + id={modalId} + onCancel={onBack || onCancel} + rightIconProps={onBack + ? { + icon: , + onClick: onCancel + } + : undefined} + title={t('Select address')} + > + } + className={'address-group-list'} + list={items} + renderItem={renderItem} + renderWhenEmpty={renderEmpty} + /> +
+
+ ); +}; + +const AddressGroupModal = styled(Component)(({ theme: { token } }: Props) => { + return { + '.address-group-list': { + display: 'flex', + flexDirection: 'column' + }, + + '.item-wrapper + .item-wrapper': { + marginTop: 8 + }, + + '.ant-sw-list-search-input': { + paddingBottom: token.paddingXS + }, + + '.ant-sw-list-section': { + height: '100%' + } + }; +}); + +export default AddressGroupModal; diff --git a/packages/extension-koni-ui/src/components/Modal/Global/SelectAddressFormatModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/SelectAddressFormatModal.tsx index 5731999e72a..ee4c876bb63 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/SelectAddressFormatModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/SelectAddressFormatModal.tsx @@ -101,19 +101,16 @@ const Component: React.FC = ({ address, chainSlug, className, name, onBac const renderItem = useCallback((item: AddressFormatInfo) => { return ( - <> -
- -
- + ); }, [onCopyAddress, onShowQr]); diff --git a/packages/extension-koni-ui/src/components/Modal/Global/index.ts b/packages/extension-koni-ui/src/components/Modal/Global/index.ts index d4b51ef9a00..3ad3566338b 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/index.ts +++ b/packages/extension-koni-ui/src/components/Modal/Global/index.ts @@ -4,3 +4,4 @@ export { default as AddressQrModal } from './AddressQrModal'; export { default as SelectAddressFormatModal } from './SelectAddressFormatModal'; export { default as AccountMigrationInProgressWarningModal } from './AccountMigrationInProgressWarningModal'; +export { default as AddressGroupModal } from './AddressGroupModal'; diff --git a/packages/extension-koni-ui/src/components/TokenItem/AddressGroupItem.tsx b/packages/extension-koni-ui/src/components/TokenItem/AddressGroupItem.tsx new file mode 100644 index 00000000000..926ce32d901 --- /dev/null +++ b/packages/extension-koni-ui/src/components/TokenItem/AddressGroupItem.tsx @@ -0,0 +1,154 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { AddressGroupItemInfo } from '@subwallet/extension-koni-ui/components/Modal/Global/AddressGroupModal'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { getBitcoinAccountDetails, toShort } from '@subwallet/extension-koni-ui/utils'; +import { Button, Icon, Logo } from '@subwallet/react-ui'; +import CN from 'classnames'; +import { Copy, QrCode } from 'phosphor-react'; +import React from 'react'; +import styled from 'styled-components'; + +type Props = ThemeProps & { + item: AddressGroupItemInfo; + onClick?: VoidFunction; + onClickCopyButton?: VoidFunction; + onClickQrButton?: VoidFunction; +} + +function Component (props: Props): React.ReactElement { + const { className, + item, + onClick, + onClickCopyButton, onClickQrButton } = props; + const _onClickCopyButton: React.MouseEventHandler = React.useCallback((event) => { + event.stopPropagation(); + onClickCopyButton?.(); + }, [onClickCopyButton]); + + const _onClickQrButton: React.MouseEventHandler = React.useCallback((event) => { + event.stopPropagation(); + onClickQrButton?.(); + }, [onClickQrButton]); + + return ( + <> +
+
+ +
+ +
+
+ {getBitcoinAccountDetails(item.accountInfo.type).name} +
+
+ {toShort(item.accountInfo.address, 4, 5)} +
+
+ +
+
+
+ + ); +} + +const AddressGroupItem = styled(Component)(({ theme: { token } }: Props) => { + return { + background: token.colorBgSecondary, + padding: token.paddingSM, + paddingRight: token.paddingXXS, + borderRadius: token.borderRadiusLG, + alignItems: 'center', + display: 'flex', + flexDirection: 'row', + cursor: 'pointer', + transition: `background ${token.motionDurationMid} ease-in-out`, + gap: token.sizeXS, + overflowX: 'hidden', + minHeight: 52, + + '.__item-center-part': { + display: 'flex', + overflowX: 'hidden', + 'white-space': 'nowrap', + flex: 1, + flexDirection: 'column' + }, + + '.__item-chain-name': { + fontSize: token.fontSizeLG, + lineHeight: token.lineHeightLG, + color: token.colorTextLight1, + overflow: 'hidden', + 'white-space': 'nowrap', + textOverflow: 'ellipsis' + }, + + '.__item-address': { + fontSize: token.fontSizeSM, + lineHeight: token.lineHeightSM, + color: token.colorTextLight4 + }, + + '.__item-right-part': { + display: 'flex' + + }, + + '.-show-on-hover': { + opacity: 0, + transition: `opacity ${token.motionDurationMid} ease-in-out` + }, + '.-hide-on-hover': { + opacity: 1, + transition: `opacity ${token.motionDurationMid} ease-in-out` + }, + + '&:hover': { + background: token.colorBgInput, + '.-hide-on-hover': { + opacity: 0 + }, + '.-show-on-hover': { + opacity: 1 + } + } + }; +}); + +export default AddressGroupItem; diff --git a/packages/extension-koni-ui/src/constants/modal.ts b/packages/extension-koni-ui/src/constants/modal.ts index bfc2ebfb893..f20202718cb 100644 --- a/packages/extension-koni-ui/src/constants/modal.ts +++ b/packages/extension-koni-ui/src/constants/modal.ts @@ -41,6 +41,7 @@ export const TON_WALLET_CONTRACT_SELECTOR_MODAL = 'ton-wallet-contract-selector- export const TON_ACCOUNT_SELECTOR_MODAL = 'ton-account-selector-modal'; export const CHOOSE_FEE_TOKEN_MODAL = 'choose-fee-token-modal'; export const SELECT_ADDRESS_FORMAT_MODAL = 'select-address-format-modal'; +export const ADDRESS_GROUP_MODAL = 'address-group-modal'; export const ACCOUNT_MIGRATION_IN_PROGRESS_WARNING_MODAL = 'account-migration-in-progress-warning-modal'; /* Campaign */ diff --git a/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx b/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx index c490ddd08ab..feab6a117c7 100644 --- a/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx +++ b/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx @@ -1,12 +1,13 @@ // Copyright 2019-2022 @polkadot/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AccountMigrationInProgressWarningModal, AddressQrModal, AlertModal, AttachAccountModal, ClaimDappStakingRewardsModal, CreateAccountModal, DeriveAccountActionModal, DeriveAccountListModal, ImportAccountModal, ImportSeedModal, NewSeedModal, RemindBackupSeedPhraseModal, RemindDuplicateAccountNameModal, RequestCameraAccessModal, RequestCreatePasswordModal, SelectAddressFormatModal, TransactionProcessDetailModal, TransactionStepsModal } from '@subwallet/extension-koni-ui/components'; +import { AccountMigrationInProgressWarningModal, AddressGroupModal, AddressQrModal, AlertModal, AttachAccountModal, ClaimDappStakingRewardsModal, CreateAccountModal, DeriveAccountActionModal, DeriveAccountListModal, ImportAccountModal, ImportSeedModal, NewSeedModal, RemindBackupSeedPhraseModal, RemindDuplicateAccountNameModal, RequestCameraAccessModal, RequestCreatePasswordModal, SelectAddressFormatModal, TransactionProcessDetailModal, TransactionStepsModal } from '@subwallet/extension-koni-ui/components'; import { CustomizeModal } from '@subwallet/extension-koni-ui/components/Modal/Customize/CustomizeModal'; import { AccountDeriveActionProps } from '@subwallet/extension-koni-ui/components/Modal/DeriveAccountActionModal'; +import { AddressGroupModalProps } from '@subwallet/extension-koni-ui/components/Modal/Global/AddressGroupModal'; import { SelectAddressFormatModalProps } from '@subwallet/extension-koni-ui/components/Modal/Global/SelectAddressFormatModal'; import { TransactionStepsModalProps } from '@subwallet/extension-koni-ui/components/Modal/TransactionStepsModal'; -import { ACCOUNT_MIGRATION_IN_PROGRESS_WARNING_MODAL, ADDRESS_QR_MODAL, DERIVE_ACCOUNT_ACTION_MODAL, EARNING_INSTRUCTION_MODAL, GLOBAL_ALERT_MODAL, SELECT_ADDRESS_FORMAT_MODAL, TRANSACTION_PROCESS_DETAIL_MODAL, TRANSACTION_STEPS_MODAL } from '@subwallet/extension-koni-ui/constants'; +import { ACCOUNT_MIGRATION_IN_PROGRESS_WARNING_MODAL, ADDRESS_GROUP_MODAL, ADDRESS_QR_MODAL, DERIVE_ACCOUNT_ACTION_MODAL, EARNING_INSTRUCTION_MODAL, GLOBAL_ALERT_MODAL, SELECT_ADDRESS_FORMAT_MODAL, TRANSACTION_PROCESS_DETAIL_MODAL, TRANSACTION_STEPS_MODAL } from '@subwallet/extension-koni-ui/constants'; import { useAlert, useGetConfig, useIsPopup, useSetSessionLatest } from '@subwallet/extension-koni-ui/hooks'; import Confirmations from '@subwallet/extension-koni-ui/Popup/Confirmations'; import { RootState } from '@subwallet/extension-koni-ui/stores'; @@ -69,6 +70,10 @@ export interface WalletModalContextType { open: (props: SelectAddressFormatModalProps) => void, close: VoidFunction }, + addressGroupModal: { + open: (props: AddressGroupModalProps) => void, + close: VoidFunction + }, alertModal: { open: (props: AlertDialogProps) => void, close: VoidFunction @@ -102,6 +107,12 @@ export const WalletModalContext = React.createContext({ // eslint-disable-next-line @typescript-eslint/no-empty-function close: () => {} }, + addressGroupModal: { + // eslint-disable-next-line @typescript-eslint/no-empty-function + open: () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + close: () => {} + }, alertModal: { // eslint-disable-next-line @typescript-eslint/no-empty-function open: () => {}, @@ -147,6 +158,7 @@ export const WalletModalContextProvider = ({ children }: Props) => { /* Address QR Modal */ const [addressQrModalProps, setAddressQrModalProps] = useState(); const [selectAddressFormatModalProps, setSelectAddressFormatModalProps] = useState(); + const [addressGroupModalProps, setAddressGroupModalProps] = useState(); const [deriveActionModalProps, setDeriveActionModalProps] = useState(); const [transactionProcessId, setTransactionProcessId] = useState(''); const [transactionStepsModalProps, setTransactionStepsModalProps] = useState(undefined); @@ -161,6 +173,11 @@ export const WalletModalContextProvider = ({ children }: Props) => { activeModal(SELECT_ADDRESS_FORMAT_MODAL); }, [activeModal]); + const openAddressGroupModal = useCallback((props: AddressGroupModalProps) => { + setAddressGroupModalProps(props); + activeModal(ADDRESS_GROUP_MODAL); + }, [activeModal]); + const checkAddressQrModalActive = useCallback(() => { return checkActive(ADDRESS_QR_MODAL); }, [checkActive]); @@ -175,6 +192,11 @@ export const WalletModalContextProvider = ({ children }: Props) => { setSelectAddressFormatModalProps(undefined); }, [inactiveModal]); + const closeAddressGroupModal = useCallback(() => { + inactiveModal(ADDRESS_GROUP_MODAL); + setAddressGroupModalProps(undefined); + }, [inactiveModal]); + const onCancelAddressQrModal = useCallback(() => { addressQrModalProps?.onCancel?.() || closeAddressQrModal(); }, [addressQrModalProps, closeAddressQrModal]); @@ -183,6 +205,10 @@ export const WalletModalContextProvider = ({ children }: Props) => { selectAddressFormatModalProps?.onCancel?.() || closeSelectAddressFormatModal(); }, [closeSelectAddressFormatModal, selectAddressFormatModalProps]); + const onCancelAddressGroupModal = useCallback(() => { + addressGroupModalProps?.onCancel?.() || closeAddressGroupModal(); + }, [addressGroupModalProps, closeAddressGroupModal]); + /* Address QR Modal */ /* Derive modal */ @@ -226,6 +252,10 @@ export const WalletModalContextProvider = ({ children }: Props) => { open: openSelectAddressFormatModal, close: closeSelectAddressFormatModal }, + addressGroupModal: { + open: openAddressGroupModal, + close: closeAddressGroupModal + }, alertModal: { open: openAlert, close: closeAlert @@ -239,7 +269,7 @@ export const WalletModalContextProvider = ({ children }: Props) => { transactionStepsModal: { open: openTransactionStepsModal } - }), [checkAddressQrModalActive, closeAddressQrModal, closeAlert, closeSelectAddressFormatModal, openAddressQrModal, openAlert, openDeriveModal, openProcessModal, openSelectAddressFormatModal, openTransactionStepsModal]); + }), [checkAddressQrModalActive, closeAddressGroupModal, closeAddressQrModal, closeAlert, closeSelectAddressFormatModal, openAddressGroupModal, openAddressQrModal, openAlert, openDeriveModal, openProcessModal, openSelectAddressFormatModal, openTransactionStepsModal]); useEffect(() => { if (hasMasterPassword && isLocked) { @@ -324,6 +354,15 @@ export const WalletModalContextProvider = ({ children }: Props) => { ) } + { + !!addressGroupModalProps && ( + + ) + } + { !!alertProps && ( { + const isBitcoinTestnet = chainInfo.isTestnet; + const keypairTypes = isBitcoinTestnet ? BitcoinTestnetKeypairTypes : BitcoinMainnetKeypairTypes; + + return accounts + .filter( + (acc) => + acc.chainType === AccountChainType.BITCOIN && + keypairTypes.includes(acc.type) + ) + .map((item) => ({ + accountInfo: item, + tokenSlug, + chainSlug + })); +}; + const tokenSelectorModalId = RECEIVE_MODAL_TOKEN_SELECTOR; const accountSelectorModalId = RECEIVE_MODAL_ACCOUNT_SELECTOR; @@ -36,7 +58,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const chainInfoMap = useSelector((state: RootState) => state.chainStore.chainInfoMap); const [selectedChain, setSelectedChain] = useState(); const [selectedAccountAddressItem, setSelectedAccountAddressItem] = useState(); - const { addressQrModal, selectAddressFormatModal } = useContext(WalletModalContext); + const { addressGroupModal, addressQrModal, selectAddressFormatModal } = useContext(WalletModalContext); const chainSupported = useGetChainSlugsByAccount(); const onHandleTonAccountWarning = useHandleTonAccountWarning(); const onHandleLedgerGenericAccountWarning = useHandleLedgerGenericAccountWarning(); @@ -90,6 +112,21 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo processFunction(); }, [selectAddressFormatModal]); + const openAddressGroupModal = useCallback((accounts: AddressGroupItemInfo[], closeCallback?: VoidCallback) => { + const processFunction = () => { + addressGroupModal.open({ + items: accounts, + onBack: addressGroupModal.close, + onCancel: () => { + addressGroupModal.close(); + closeCallback?.(); + } + }); + }; + + processFunction(); + }, [addressGroupModal]); + /* --- token Selector */ const tokenSelectorItems = useMemo<_ChainAsset[]>(() => { @@ -134,37 +171,52 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo // current account is not All, just do show QR logic const isPolkadotUnifiedChain = checkIsPolkadotUnifiedChain(chainSlug); + const isBitcoinChain = _isChainBitcoinCompatible(chainInfo); - for (const accountJson of currentAccountProxy.accounts) { - const reformatedAddress = getReformatAddress(accountJson, chainInfo); + if (isBitcoinChain) { + const addressGroupList = transformBitcoinAccounts( + currentAccountProxy?.accounts || [], + chainSlug, + item.slug, + chainInfo + ); - if (reformatedAddress) { - const accountAddressItem: AccountAddressItemType = { - accountName: accountJson.name || '', - accountProxyId: accountJson.proxyId || '', - accountProxyType: currentAccountProxy.accountType, - accountType: accountJson.type, - address: reformatedAddress - }; + openAddressGroupModal(addressGroupList, () => { + inactiveModal(tokenSelectorModalId); + setSelectedAccountAddressItem(undefined); + }); + } else { + for (const accountJson of currentAccountProxy.accounts) { + const reformatedAddress = getReformatAddress(accountJson, chainInfo); - setSelectedAccountAddressItem(accountAddressItem); + if (reformatedAddress) { + const accountAddressItem: AccountAddressItemType = { + accountName: accountJson.name || '', + accountProxyId: accountJson.proxyId || '', + accountProxyType: currentAccountProxy.accountType, + accountType: accountJson.type, + address: reformatedAddress + }; - if (isPolkadotUnifiedChain) { - openAddressFormatModal(chainInfo.name, reformatedAddress, chainSlug, () => { - inactiveModal(tokenSelectorModalId); - setSelectedAccountAddressItem(undefined); - }); - } else { - openAddressQrModal(reformatedAddress, accountJson.type, currentAccountProxy.id, chainSlug, () => { - inactiveModal(tokenSelectorModalId); - setSelectedAccountAddressItem(undefined); - }); - } + setSelectedAccountAddressItem(accountAddressItem); - break; + if (isPolkadotUnifiedChain) { + openAddressFormatModal(chainInfo.name, reformatedAddress, chainSlug, () => { + inactiveModal(tokenSelectorModalId); + setSelectedAccountAddressItem(undefined); + }); + } else { + openAddressQrModal(reformatedAddress, accountJson.type, currentAccountProxy.id, chainSlug, () => { + inactiveModal(tokenSelectorModalId); + setSelectedAccountAddressItem(undefined); + }); + } + + break; + } } } - }, [activeModal, chainInfoMap, checkIsPolkadotUnifiedChain, currentAccountProxy, inactiveModal, isAllAccount, openAddressFormatModal, openAddressQrModal, getReformatAddress]); + }, [currentAccountProxy, chainInfoMap, isAllAccount, checkIsPolkadotUnifiedChain, activeModal, openAddressGroupModal, inactiveModal, getReformatAddress, openAddressFormatModal, openAddressQrModal]); /* token Selector --- */ From 6a89a2d817aadcf348b9de855a69da9cd93d4143 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 18 Apr 2025 10:55:53 +0700 Subject: [PATCH 031/178] [Issue-4094] feat: Update token detail --- .../src/Popup/Home/Tokens/DetailModal.tsx | 38 +++++++---- .../TokenItem/AccountTokenBalanceItem.tsx | 64 +++++++++++++------ 2 files changed, 70 insertions(+), 32 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx index f504352fabb..1d992cacf0b 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx @@ -2,10 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { APIItemState } from '@subwallet/extension-base/background/KoniTypes'; +import { _isChainBitcoinCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { BalanceItem } from '@subwallet/extension-base/types'; import { AccountTokenBalanceItem, EmptyList, RadioGroup } from '@subwallet/extension-koni-ui/components'; import { useSelector } from '@subwallet/extension-koni-ui/hooks'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; +import { RootState } from '@subwallet/extension-koni-ui/stores'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; import { TokenBalanceItemType } from '@subwallet/extension-koni-ui/types/balance'; import { isAccountAll } from '@subwallet/extension-koni-ui/utils'; @@ -57,7 +59,7 @@ function Component ({ className = '', currentTokenInfo, id, onCancel, tokenBalan const { accounts, currentAccountProxy, isAllAccount } = useSelector((state) => state.accountState); const { balanceMap } = useSelector((state) => state.balance); - + const chainInfoMap = useSelector((state: RootState) => state.chainStore.chainInfoMap); const [form] = Form.useForm(); const view = Form.useWatch('view', form); @@ -82,25 +84,33 @@ function Component ({ className = '', currentTokenInfo, id, onCancel, tokenBalan const items = useMemo((): ItemType[] => { const symbol = currentTokenInfo?.symbol || ''; const balanceInfo = currentTokenInfo ? tokenBalanceMap[currentTokenInfo.slug] : undefined; + const chainInfo = balanceInfo?.chain && chainInfoMap[balanceInfo?.chain]; - const result: ItemType[] = []; + const isBitcoinChain = !!chainInfo && _isChainBitcoinCompatible(chainInfo); - result.push({ - key: 'transferable', + const createItem = (key: string, label: string, value: BigN): ItemType => ({ + key, symbol, - label: t('Transferable'), - value: balanceInfo ? balanceInfo.free.value : new BigN(0) + label, + value }); - result.push({ - key: 'locked', - symbol, - label: t('Locked'), - value: balanceInfo ? balanceInfo.locked.value : new BigN(0) - }); + if (isBitcoinChain) { + return [ + createItem('transferable', t('BTC Transferable'), new BigN(0)), + createItem('rune', t('BTC Rune (Locked)'), new BigN(0)), + createItem('inscription', t('BTC Inscription (Locked)'), new BigN(0)) + ]; + } - return result; - }, [currentTokenInfo, t, tokenBalanceMap]); + const transferableValue = balanceInfo?.free.value ?? new BigN(0); + const lockedValue = balanceInfo?.locked.value ?? new BigN(0); + + return [ + createItem('transferable', t('Transferable'), transferableValue), + createItem('locked', t('Locked'), lockedValue) + ]; + }, [chainInfoMap, currentTokenInfo, t, tokenBalanceMap]); const accountItems = useMemo((): BalanceItem[] => { if (!currentAccountProxy || !currentTokenInfo?.slug) { diff --git a/packages/extension-koni-ui/src/components/TokenItem/AccountTokenBalanceItem.tsx b/packages/extension-koni-ui/src/components/TokenItem/AccountTokenBalanceItem.tsx index c71e676fbcd..f95e49a94ef 100644 --- a/packages/extension-koni-ui/src/components/TokenItem/AccountTokenBalanceItem.tsx +++ b/packages/extension-koni-ui/src/components/TokenItem/AccountTokenBalanceItem.tsx @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { _ChainAsset } from '@subwallet/chain-list/types'; -import { _isChainTonCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { _BalanceMetadata, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; +import { _isChainBitcoinCompatible, _isChainTonCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { getExplorerLink } from '@subwallet/extension-base/services/transaction-service/utils'; import { BalanceItem } from '@subwallet/extension-base/types'; import { Avatar } from '@subwallet/extension-koni-ui/components'; @@ -23,11 +24,17 @@ interface Props extends ThemeProps { item: BalanceItem; } +interface BalanceDisplayItem { + label: string; + value: string; + key: string; +} + // todo: logic in this file may not be correct in some case, need to recheck const Component: React.FC = (props: Props) => { const { className, item } = props; - const { address, free, locked, tokenSlug } = item; + const { address, free, locked, metadata, tokenSlug } = item; const { t } = useTranslation(); const { assetRegistry } = useSelector((state) => state.assetRegistry); @@ -72,6 +79,42 @@ const Component: React.FC = (props: Props) => { const symbol = tokenInfo?.symbol || ''; const link = (chainInfo !== undefined) && getExplorerLink(chainInfo, reformatedAddress, 'account'); + const isBitcoinMetadata = (meta: _BalanceMetadata | undefined): meta is BitcoinBalanceMetadata => { + return !!meta && typeof meta === 'object' && 'runeBalance' in meta && 'inscriptionBalance' in meta; + }; + + const renderBalanceItem = useCallback( + ({ key, label, value }: BalanceDisplayItem) => ( + + ), + [decimals, symbol] + ); + + const isBitcoinChain = !!chainInfo && _isChainBitcoinCompatible(chainInfo); + + const balanceItems = useMemo(() => { + if (isBitcoinChain) { + return [ + { key: 'btc_transferable', label: t('BTC Transferable'), value: free }, + { key: 'btc_rune', label: t('BTC Rune (Locked)'), value: isBitcoinMetadata(metadata) ? String(metadata.runeBalance) : '0' }, + { key: 'btc_inscription', label: t('BTC Inscription (Locked)'), value: isBitcoinMetadata(metadata) ? String(metadata.inscriptionBalance) : '0' } + ]; + } + + return [ + { key: 'transferable', label: t('Transferable'), value: free }, + { key: 'locked', label: t('Locked'), value: locked } + ]; + }, [isBitcoinChain, free, locked, metadata, t]); + return ( = (props: Props) => { value={total} valueColorSchema='light' /> - - + {balanceItems.map(renderBalanceItem)} {!!link && (
+ )} + suffix={symbol} + value={total} + valueColorSchema='light' + /> )} - suffix={symbol} - value={total} - valueColorSchema='light' - /> {balanceItems.map(renderBalanceItem)} {!!link && ( + + {/* { */} + {/* signMode === AccountSignMode.QR && ( */} + {/* */} + {/* */} + {/* */} + {/* ) */} + {/* } */} + {signMode === AccountSignMode.QR && } + + ); +}; + +const BitcoinSignArea = styled(Component)(({ theme: { token } }: Props) => { + return {}; +}); + +export default BitcoinSignArea; diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/index.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/index.tsx index bc40e03e8ed..fec70ee64c9 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/index.tsx @@ -4,3 +4,4 @@ export { default as EvmSignArea } from './Evm'; export { default as SubstrateSignArea } from './Substrate'; export { default as TonSignArea } from './Ton'; +export { default as BitcoinSignArea } from './Bitcoin'; 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..6f3674645d3 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx @@ -0,0 +1,286 @@ +// 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, ResponseSubscribeTransferConfirmation, TransactionFee } from '@subwallet/extension-base/types'; +import { 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, useNotification } from '@subwallet/extension-koni-ui/hooks'; +import { cancelSubscription, subscribeTransferWhenConfirmation } 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, networkKey, to, tokenSlug, value } } = request; + const { t } = useTranslation(); + const transferAmountValue = useMemo(() => value?.toString() as string, [value]); + const fromValue = useMemo(() => account.address, [account.address]); + const toValue = useMemo(() => to ? to[0].address : '', [to]); + const chainValue = useMemo(() => networkKey as string, [networkKey]); + const assetValue = useMemo(() => tokenSlug as string, [tokenSlug]); + + const [transactionInfo, setTransactionInfo] = useState({ + id, + chain: networkKey as string, + from: account.address, + to: toValue, + tokenSlug: tokenSlug as string, + transferAll: false, + value: value?.toString() || '0' + }); + const [isFetchingInfo, setIsFetchingInfo] = useState(false); + const [transferInfo, setTransferInfo] = useState(); + const [transactionFeeInfo, setTransactionFeeInfo] = useState(undefined); + const [isErrorTransaction, setIsErrorTransaction] = useState(false); + const notify = useNotification(); + const assetRegistry = useSelector((root: RootState) => root.assetRegistry.assetRegistry); + + 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(() => { + let cancel = false; + let id = ''; + let timeout: NodeJS.Timeout; + + setIsFetchingInfo(true); + + const callback = (transferInfo: ResponseSubscribeTransferConfirmation) => { + if (transferInfo.error) { + notify({ + message: t(transferInfo.error), + type: 'error', + duration: 8 + }); + setIsErrorTransaction(true); + } else if (!cancel) { + setTransferInfo(transferInfo); + id = transferInfo.id; + } else { + cancelSubscription(transferInfo.id).catch(console.error); + } + }; + + if (fromValue && assetValue) { + timeout = setTimeout(() => { + subscribeTransferWhenConfirmation({ + address: fromValue, + chain: chainValue, + token: assetValue, + destChain: chainValue, + feeOption: transactionFeeInfo?.feeOption, + feeCustom: transactionFeeInfo?.feeCustom, + value: transferAmountValue || '0', + transferAll: false, + to: toValue + }, callback) + .then(callback) + .catch((e) => { + console.error(e); + notify({ + message: t(e), + type: 'error', + duration: 8 + }); + setIsErrorTransaction(true); + setTransferInfo(undefined); + }) + .finally(() => { + setIsFetchingInfo(false); + }); + }, 100); + } + + return () => { + cancel = true; + clearTimeout(timeout); + id && cancelSubscription(id).catch(console.error); + }; + }, [assetRegistry, assetValue, chainValue, fromValue, toValue, transactionFeeInfo, transferAmountValue, notify, t]); + + return ( + <> +
+
{getDomainFromUrl(request.url)}
+ + + + + + + + + + + + {!isErrorTransaction && } + + + {/* {!!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..ea51e7f878e --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignPsbtConfirmation.tsx @@ -0,0 +1,130 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { _ChainAsset } from '@subwallet/chain-list/types'; +import { BitcoinSignPsbtRequest, ConfirmationsQueueItem, PsbtTransactionArg } 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 { RootState } from '@subwallet/extension-koni-ui/stores'; +import { BitcoinSignatureSupportType, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { findAccountByAddress } from '@subwallet/extension-koni-ui/utils'; +import { Button, Number } from '@subwallet/react-ui'; +import CN from 'classnames'; +import React, { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +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 { tokenSlug, txInput, txOutput } = request.payload.payload; + const accounts = useSelector((state: RootState) => state.accountState.accounts); + const assetRegistry = useSelector((root: RootState) => root.assetRegistry.assetRegistry); + const onClickDetail = useOpenDetailModal(); + const assetInfo: _ChainAsset | undefined = useMemo(() => { + return assetRegistry[tokenSlug]; + }, [assetRegistry, tokenSlug]); + const renderAccount = useCallback((accountsPsbt: PsbtTransactionArg[]) => { + return ( +
+ { + accountsPsbt.map(({ address, amount }) => { + const account = findAccountByAddress(accounts, address); + + return ( + : <>} + />); + } + ) + } + +
+ ); + }, [accounts, assetInfo.decimals, assetInfo.symbol]); + + return ( + <> +
+ +
+ {t('Signature required')} +
+
+ {t('You are approving a request with the following account')} +
+ +
+ +
+
+ + + + + {renderAccount(txInput)} + + + {renderAccount(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..252a959146f --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignatureConfirmation.tsx @@ -0,0 +1,89 @@ +// 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 { 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(); + // TODO: Temporarily comment out the AccountItemWithName component and recheck later. + 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/Transaction/index.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/index.tsx index 696cfcd849f..660f39377e9 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/index.tsx @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { ConfirmationDefinitions, ConfirmationDefinitionsCardano, ConfirmationDefinitionsTon, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { ConfirmationDefinitions, ConfirmationDefinitionsBitcoin, ConfirmationDefinitionsCardano, ConfirmationDefinitionsTon, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { SigningRequest } from '@subwallet/extension-base/background/types'; import { SWTransactionResult } from '@subwallet/extension-base/services/transaction-service/types'; import { ProcessType, SwapBaseTxData } from '@subwallet/extension-base/types'; @@ -18,7 +18,7 @@ import React, { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; -import { EvmSignArea, SubstrateSignArea } from '../../parts/Sign'; +import { BitcoinSignArea, EvmSignArea, SubstrateSignArea } from '../../parts/Sign'; import { BaseProcessConfirmation, BaseTransactionConfirmation, BondTransactionConfirmation, CancelUnstakeTransactionConfirmation, ClaimBridgeTransactionConfirmation, ClaimRewardTransactionConfirmation, DefaultWithdrawTransactionConfirmation, EarnProcessConfirmation, FastWithdrawTransactionConfirmation, JoinPoolTransactionConfirmation, JoinYieldPoolConfirmation, LeavePoolTransactionConfirmation, SendNftTransactionConfirmation, SwapProcessConfirmation, SwapTransactionConfirmation, TokenApproveConfirmation, TransferBlock, UnbondTransactionConfirmation, WithdrawTransactionConfirmation } from './variants'; interface Props extends ThemeProps { @@ -232,6 +232,16 @@ const Component: React.FC = (props: Props) => { /> ) } + { + (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 2e20bc29c4d..c96fd5aab34 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/index.ts +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/index.ts @@ -11,6 +11,9 @@ 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'; export * from './Error'; export * from './Message'; diff --git a/packages/extension-koni-ui/src/messaging/confirmation/base.ts b/packages/extension-koni-ui/src/messaging/confirmation/base.ts index 9c7f992e5e5..e12596f6ad4 100644 --- a/packages/extension-koni-ui/src/messaging/confirmation/base.ts +++ b/packages/extension-koni-ui/src/messaging/confirmation/base.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { ConfirmationDefinitions, ConfirmationDefinitionsCardano, ConfirmationDefinitionsTon, ConfirmationType, ConfirmationTypeCardano, ConfirmationTypeTon, RequestSigningApprovePasswordV2 } from '@subwallet/extension-base/background/KoniTypes'; +import { ConfirmationDefinitions, ConfirmationDefinitionsBitcoin, ConfirmationDefinitionsCardano, ConfirmationDefinitionsTon, ConfirmationType, ConfirmationTypeBitcoin, ConfirmationTypeCardano, ConfirmationTypeTon, RequestSigningApprovePasswordV2 } from '@subwallet/extension-base/background/KoniTypes'; import { ResponseSigningIsLocked } from '@subwallet/extension-base/background/types'; import { HexString } from '@polkadot/util/types'; @@ -39,3 +39,7 @@ export async function completeConfirmationTon (t export async function completeConfirmationCardano (type: CT, payload: ConfirmationDefinitionsCardano[CT][1]): Promise { return sendMessage('pri(confirmationsCardano.complete)', { [type]: payload }); } + +export async function completeConfirmationBitcoin (type: CT, payload: ConfirmationDefinitionsBitcoin[CT][1]): Promise { + return sendMessage('pri(confirmations.bitcoin.complete)', { [type]: payload }); +} diff --git a/packages/extension-koni-ui/src/messaging/transaction/transfer.ts b/packages/extension-koni-ui/src/messaging/transaction/transfer.ts index 01361d10ab4..5b77ee36d2d 100644 --- a/packages/extension-koni-ui/src/messaging/transaction/transfer.ts +++ b/packages/extension-koni-ui/src/messaging/transaction/transfer.ts @@ -4,9 +4,9 @@ import { AmountData, RequestMaxTransferable } from '@subwallet/extension-base/background/KoniTypes'; import { RequestOptimalTransferProcess } from '@subwallet/extension-base/services/balance-service/helpers'; import { TokenPayFeeInfo } from '@subwallet/extension-base/services/fee-service/interfaces'; -import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; +import { BitcoinTransactionData, SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; import { CommonOptimalTransferPath, RequestCrossChainTransfer, RequestGetAmountForPair, RequestGetTokensCanPayFee, TokenSpendingApprovalParams } from '@subwallet/extension-base/types'; -import { RequestSubmitTransfer, RequestSubscribeTransfer, ResponseSubscribeTransfer } from '@subwallet/extension-base/types/balance/transfer'; +import { RequestSubmitSignPsbtTransfer, RequestSubmitTransfer, RequestSubmitTransferWithId, RequestSubscribeTransfer, ResponseSubscribeTransfer } from '@subwallet/extension-base/types/balance/transfer'; import { sendMessage } from '../base'; @@ -14,10 +14,22 @@ export async function makeTransfer (request: RequestSubmitTransfer): Promise { + return sendMessage('pri(accounts.bitcoin.dapp.transfer.confirmation)', request); +} + +export async function makePSBTTransferAfterConfirmation (request: RequestSubmitSignPsbtTransfer): Promise { + return sendMessage('pri(accounts.psbt.transfer.confirmation)', request); +} + export async function makeCrossChainTransfer (request: RequestCrossChainTransfer): Promise { return sendMessage('pri(accounts.crossChainTransfer)', request); } +export async function getBitcoinTransactionData (request: RequestSubmitTransfer): Promise { + return sendMessage('pri(accounts.getBitcoinTransactionData)', request); +} + export async function approveSpending (request: TokenSpendingApprovalParams): Promise { return sendMessage('pri(accounts.approveSpending)', request); } diff --git a/packages/extension-koni-ui/src/types/confirmation.ts b/packages/extension-koni-ui/src/types/confirmation.ts index 4a3edcf0ecc..4ac6f2a54d6 100644 --- a/packages/extension-koni-ui/src/types/confirmation.ts +++ b/packages/extension-koni-ui/src/types/confirmation.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { ConfirmationDefinitions, ConfirmationDefinitionsCardano, ConfirmationDefinitionsTon } from '@subwallet/extension-base/background/KoniTypes'; +import { ConfirmationDefinitions, ConfirmationDefinitionsBitcoin, ConfirmationDefinitionsCardano, ConfirmationDefinitionsTon } from '@subwallet/extension-base/background/KoniTypes'; export type EvmSignatureSupportType = keyof Pick; export type EvmErrorSupportType = keyof Pick; @@ -9,3 +9,5 @@ export type EvmErrorSupportType = keyof Pick; export type CardanoSignatureSupportType = keyof Pick; + +export type BitcoinSignatureSupportType = keyof Pick; diff --git a/packages/extension-koni-ui/src/utils/confirmation/confirmation.ts b/packages/extension-koni-ui/src/utils/confirmation/confirmation.ts index 1c3c45bc211..f82a113259d 100644 --- a/packages/extension-koni-ui/src/utils/confirmation/confirmation.ts +++ b/packages/extension-koni-ui/src/utils/confirmation/confirmation.ts @@ -1,8 +1,8 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { ConfirmationDefinitions } from '@subwallet/extension-base/background/KoniTypes'; -import { EvmSignatureSupportType } from '@subwallet/extension-koni-ui/types/confirmation'; +import { ConfirmationDefinitions, ConfirmationDefinitionsBitcoin } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinSignatureSupportType, EvmSignatureSupportType } from '@subwallet/extension-koni-ui/types/confirmation'; import { ExtrinsicPayload } from '@polkadot/types/interfaces'; @@ -11,3 +11,7 @@ export const isSubstrateMessage = (payload: string | ExtrinsicPayload): payload export const isEvmMessage = (request: ConfirmationDefinitions[EvmSignatureSupportType][0]): request is ConfirmationDefinitions['evmSignatureRequest'][0] => { return !!(request as ConfirmationDefinitions['evmSignatureRequest'][0]).payload.type; }; + +export const isBitcoinMessage = (request: ConfirmationDefinitionsBitcoin[BitcoinSignatureSupportType][0]): request is ConfirmationDefinitionsBitcoin['bitcoinSignatureRequest'][0] => { + return !!(request as ConfirmationDefinitionsBitcoin['bitcoinSignatureRequest'][0]).payload.payloadJson; +}; From f6ec03b709fae4c0050e23b174298d286d61dd8c Mon Sep 17 00:00:00 2001 From: Phong Le Nhat Date: Mon, 28 Apr 2025 18:55:16 +0700 Subject: [PATCH 057/178] remove autoEnableTokens and modify method get free balance bitcoin --- .../src/background/KoniTypes.ts | 5 +++ .../helpers/subscribe/bitcoin.ts | 40 ++++++++++--------- .../src/services/chain-service/index.ts | 7 ---- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 6ddfc036e4b..b87990a0307 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -2012,6 +2012,11 @@ export type BitcoinBalanceMetadata = { inscriptionBalance: string // sum of BTC in UTXO which contains rune } +export interface AddressBalanceResult { + balance: string; + bitcoinBalanceMetadata: BitcoinBalanceMetadata; +} + // Use stringify to communicate, pure boolean value will error with case 'false' value export interface KoniRequestSignatures { // Bonding functions diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index 4452fe1d332..a22fc84e8e9 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -1,13 +1,24 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; +import { AddressBalanceResult, APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { BITCOIN_REFRESH_BALANCE_INTERVAL } from '@subwallet/extension-base/constants'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; import { filteredOutTxsUtxos, getInscriptionUtxos, getRuneUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; +function getDefaultBalanceResult (): AddressBalanceResult { + return { + balance: '0', + bitcoinBalanceMetadata: { + inscriptionCount: 0, + runeBalance: '0', + inscriptionBalance: '0' + } + }; +} + export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { try { const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ @@ -43,39 +54,30 @@ export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, addre async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) { return await Promise.all(addresses.map(async (address) => { try { - const [filteredUtxos, addressSummaryInfo] = await Promise.all([ - getTransferableBitcoinUtxos(bitcoinApi, address), + const [addressSummaryInfo] = await Promise.all([ bitcoinApi.api.getAddressSummaryInfo(address) ]); console.log('addressSummaryInfo', addressSummaryInfo); + + if (Number(addressSummaryInfo.balance) < 0) { + return getDefaultBalanceResult(); + } + const bitcoinBalanceMetadata = { inscriptionCount: addressSummaryInfo.total_inscription, runeBalance: addressSummaryInfo.balance_rune, inscriptionBalance: addressSummaryInfo.balance_inscription } as BitcoinBalanceMetadata; - let balanceValue = new BigN(0); - - filteredUtxos.forEach((utxo) => { - balanceValue = balanceValue.plus(utxo.value); - }); - return { - balance: balanceValue.toString(), + balance: addressSummaryInfo.balance.toString(), bitcoinBalanceMetadata: bitcoinBalanceMetadata }; } catch (error) { console.log('Error while fetching Bitcoin balances', error); - return { - balance: '0', - bitcoinBalanceMetadata: { - inscriptionCount: 0, - runeBalance: 0, - inscriptionBalance: 0 - } - }; + return getDefaultBalanceResult(); } })); } @@ -104,7 +106,7 @@ export function subscribeBitcoinBalance (addresses: string[], bitcoinApi: _Bitco return addresses.map((address): BalanceItem => { return { address: address, - tokenSlug: 'bitcoin', + tokenSlug: 'bitcoin-NATIVE-BTC', state: APIItemState.READY, free: '0', locked: '0' diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 5d4c3250047..d15411dbcf3 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -791,18 +791,11 @@ export class ChainService { const assetSettings = this.assetSettingSubject.value; const chainStateMap = this.getChainStateMap(); - const chainInfoMap = this.getChainInfoMap(); for (const asset of autoEnableTokens) { const { originChain, slug: assetSlug } = asset; const assetState = assetSettings[assetSlug]; const chainState = chainStateMap[originChain]; - const chainInfo = chainInfoMap[originChain]; - - // todo: will add more condition if there are more networks to support - if (!(chainInfo && (_isPureEvmChain(chainInfo) || _isPureBitcoinChain(chainInfo)))) { - continue; - } if (!assetState) { // If this asset not has asset setting, this token is not enabled before (not turned off before) if (!chainState || !chainState.manualTurnOff) { From 0cb93d3dc9fa64f69e478ba21ba919113dec4294 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 29 Apr 2025 09:34:25 +0700 Subject: [PATCH 058/178] [Issue-4168] Resolve conflicts after merging `subwallet-dev` --- .../src/utils/account/account.ts | 2 -- yarn.lock | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index 8cdf882b1e8..8f366c3b684 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -14,8 +14,6 @@ import { AccountAddressType, AccountSignMode, AccountType, BitcoinAccountInfo } import { getNetworkKeyByGenesisHash } from '@subwallet/extension-koni-ui/utils/chain/getNetworkJsonByGenesisHash'; import { AccountInfoByNetwork } from '@subwallet/extension-koni-ui/utils/types'; import { isAddress, isCardanoAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; -import { KeypairType } from '@subwallet/keyring/types'; -import { isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; import { BitcoinTestnetKeypairTypes, KeypairType } from '@subwallet/keyring/types'; import { Web3LogoMap } from '@subwallet/react-ui/es/config-provider/context'; diff --git a/yarn.lock b/yarn.lock index 1b52e13c176..b59ff048abf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6810,7 +6810,7 @@ __metadata: bowser: ^2.11.0 browser-passworder: ^2.0.3 buffer: ^6.0.3 - cross-fetch: ^3.1.5 + cross-fetch: ^4.1.0 dexie: ^3.2.2 dexie-export-import: ^4.0.7 eth-simple-keyring: ^4.2.0 @@ -12789,7 +12789,7 @@ __metadata: languageName: node linkType: hard -"cross-fetch@npm:^3.1.4, cross-fetch@npm:^3.1.5": +"cross-fetch@npm:^3.1.4": version: 3.1.5 resolution: "cross-fetch@npm:3.1.5" dependencies: @@ -12807,6 +12807,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^4.1.0": + version: 4.1.0 + resolution: "cross-fetch@npm:4.1.0" + dependencies: + node-fetch: ^2.7.0 + checksum: c02fa85d59f83e50dbd769ee472c9cc984060c403ee5ec8654659f61a525c1a655eef1c7a35e365c1a107b4e72d76e786718b673d1cb3c97f61d4644cb0a9f9d + languageName: node + linkType: hard + "cross-spawn@npm:^6.0.5": version: 6.0.5 resolution: "cross-spawn@npm:6.0.5" @@ -22113,7 +22122,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.12": +"node-fetch@npm:^2.6.12, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: From f476ba16d2ce39d63a63d6a881a7402342547aff Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 29 Apr 2025 09:52:44 +0700 Subject: [PATCH 059/178] [Issue-4162] chore: Fix eslint --- .../src/services/balance-service/helpers/subscribe/bitcoin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index a22fc84e8e9..866320bed5a 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -6,7 +6,6 @@ import { BITCOIN_REFRESH_BALANCE_INTERVAL } from '@subwallet/extension-base/cons import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; import { filteredOutTxsUtxos, getInscriptionUtxos, getRuneUtxos } from '@subwallet/extension-base/utils'; -import BigN from 'bignumber.js'; function getDefaultBalanceResult (): AddressBalanceResult { return { From ab4ef9cfc002f5a9dd4fc6fd37ae2f4be39162ea Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 29 Apr 2025 10:25:26 +0700 Subject: [PATCH 060/178] [Issue-4162] chore: resolve conflict after merging `4094` --- .../services/balance-service/helpers/subscribe/bitcoin.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index dbacf09137a..67a6171d59a 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -1,11 +1,12 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 +import { _AssetType } from '@subwallet/chain-list/types'; import { AddressBalanceResult, APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { BITCOIN_REFRESH_BALANCE_INTERVAL } from '@subwallet/extension-base/constants'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; -import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; -import { filteredOutTxsUtxos, getInscriptionUtxos, getRuneUtxos } from '@subwallet/extension-base/utils'; +import { BalanceItem, SusbcribeBitcoinPalletBalance, UtxoResponseItem } from '@subwallet/extension-base/types'; +import { filterAssetsByChainAndType, filteredOutTxsUtxos, getInscriptionUtxos, getRuneUtxos } from '@subwallet/extension-base/utils'; function getDefaultBalanceResult (): AddressBalanceResult { return { From d297d6c98d7598f8f381b79c9b0bee666e510125 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 29 Apr 2025 16:26:28 +0700 Subject: [PATCH 061/178] [Issue-4263] feat: Handle fee for Bitcoin --- .../src/core/logic-validation/transfer.ts | 40 ++++++++++++++----- .../helpers/subscribe/bitcoin.ts | 38 +----------------- .../transfer/bitcoin-transfer.ts | 25 +----------- .../src/services/fee-service/service.ts | 12 +++++- .../transaction-service/helpers/index.ts | 9 ++--- .../src/services/transaction-service/index.ts | 21 +++------- .../src/services/transaction-service/types.ts | 1 + .../src/utils/bitcoin/common.ts | 34 ++++++++++++++++ 8 files changed, 88 insertions(+), 92 deletions(-) diff --git a/packages/extension-base/src/core/logic-validation/transfer.ts b/packages/extension-base/src/core/logic-validation/transfer.ts index 04231e65f36..680b86a9e86 100644 --- a/packages/extension-base/src/core/logic-validation/transfer.ts +++ b/packages/extension-base/src/core/logic-validation/transfer.ts @@ -14,15 +14,16 @@ import { _TRANSFER_CHAIN_GROUP } from '@subwallet/extension-base/services/chain- import { _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getAssetDecimals, _getAssetPriceId, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getTokenMinAmount, _isCIP26Token, _isNativeToken, _isNativeTokenBySlug, _isTokenEvmSmartContract, _isTokenTonSmartContract } from '@subwallet/extension-base/services/chain-service/utils'; import { calculateToAmountByReservePool, FEE_COVERAGE_PERCENTAGE_SPECIAL_CASE } from '@subwallet/extension-base/services/fee-service/utils'; -import { isCardanoTransaction, isSubstrateTransaction, isTonTransaction } from '@subwallet/extension-base/services/transaction-service/helpers'; +import { isBitcoinTransaction, isCardanoTransaction, isSubstrateTransaction, isTonTransaction } from '@subwallet/extension-base/services/transaction-service/helpers'; import { OptionalSWTransaction, SWTransactionInput, SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; -import { AccountSignMode, BasicTxErrorType, BasicTxWarningCode, EvmEIP1559FeeOption, EvmFeeInfo, TransferTxErrorType } from '@subwallet/extension-base/types'; -import { balanceFormatter, combineEthFee, formatNumber, pairToAccount } from '@subwallet/extension-base/utils'; +import { AccountSignMode, BasicTxErrorType, BasicTxWarningCode, BitcoinFeeInfo, BitcoinFeeRate, EvmEIP1559FeeOption, EvmFeeInfo, FeeInfo, TransferTxErrorType } from '@subwallet/extension-base/types'; +import { balanceFormatter, combineBitcoinFee, combineEthFee, formatNumber, getSizeInfo, pairToAccount } from '@subwallet/extension-base/utils'; import { isCardanoAddress, isTonAddress } from '@subwallet/keyring'; import { KeyringPair } from '@subwallet/keyring/types'; import { keyring } from '@subwallet/ui-keyring'; import BigN from 'bignumber.js'; import { t } from 'i18next'; +import { TransactionConfig } from 'web3-core'; import { isEthereumAddress } from '@polkadot/util-crypto'; @@ -347,7 +348,7 @@ export function checkSupportForTransaction (validationResponse: SWTransactionRes } } -export async function estimateFeeForTransaction (validationResponse: SWTransactionResponse, transaction: OptionalSWTransaction, chainInfo: _ChainInfo, evmApi: _EvmApi, substrateApi: _SubstrateApi, priceMap: Record, feeInfo: EvmFeeInfo, nativeTokenInfo: _ChainAsset, nonNativeTokenPayFeeInfo: _ChainAsset | undefined, isTransferLocalTokenAndPayThatTokenAsFee: boolean | undefined): Promise { +export async function estimateFeeForTransaction (validationResponse: SWTransactionResponse, transaction: OptionalSWTransaction, chainInfo: _ChainInfo, evmApi: _EvmApi, substrateApi: _SubstrateApi, priceMap: Record, feeInfo: FeeInfo, nativeTokenInfo: _ChainAsset, nonNativeTokenPayFeeInfo: _ChainAsset | undefined, isTransferLocalTokenAndPayThatTokenAsFee: boolean | undefined): Promise { const estimateFee: FeeData = { symbol: '', decimals: 0, @@ -358,6 +359,7 @@ export async function estimateFeeForTransaction (validationResponse: SWTransacti estimateFee.decimals = decimals; estimateFee.symbol = symbol; + const { address, feeCustom, feeOption } = validationResponse; if (transaction) { try { @@ -367,15 +369,33 @@ export async function estimateFeeForTransaction (validationResponse: SWTransacti estimateFee.value = transaction.estimateFee; // todo: might need to update logic estimate fee inside for future actions excluding normal transfer Ton and Jetton } else if (isCardanoTransaction(transaction)) { estimateFee.value = transaction.estimateCardanoFee; + } else if (isBitcoinTransaction(transaction)) { + const feeCombine = combineBitcoinFee(feeInfo as BitcoinFeeInfo, feeOption, feeCustom as BitcoinFeeRate); + + const recipients: string[] = []; + + for (const txOutput of transaction.txOutputs) { + txOutput.address && recipients.push(txOutput.address); + } + + // TODO: Need review + const sizeInfo = getSizeInfo({ + inputLength: transaction.inputCount, + recipients: recipients, + sender: address + }); + + estimateFee.value = (feeCombine.feeRate * sizeInfo.txVBytes).toString(); } else { - const gasLimit = transaction.gas || await evmApi.api.eth.estimateGas(transaction); + const _transaction = transaction as TransactionConfig; + const gasLimit = _transaction.gas || await evmApi.api.eth.estimateGas(_transaction); - const feeCombine = combineEthFee(feeInfo, validationResponse.feeOption, validationResponse.feeCustom as EvmEIP1559FeeOption); + const feeCombine = combineEthFee(feeInfo as EvmFeeInfo, validationResponse.feeOption, validationResponse.feeCustom as EvmEIP1559FeeOption); - if (transaction.maxFeePerGas) { - estimateFee.value = new BigN(transaction.maxFeePerGas.toString()).multipliedBy(gasLimit).toFixed(0); - } else if (transaction.gasPrice) { - estimateFee.value = new BigN(transaction.gasPrice.toString()).multipliedBy(gasLimit).toFixed(0); + if (_transaction.maxFeePerGas) { + estimateFee.value = new BigN(_transaction.maxFeePerGas.toString()).multipliedBy(gasLimit).toFixed(0); + } else if (_transaction.gasPrice) { + estimateFee.value = new BigN(_transaction.gasPrice.toString()).multipliedBy(gasLimit).toFixed(0); } else { if (feeCombine.maxFeePerGas) { const maxFee = new BigN(feeCombine.maxFeePerGas); // TODO: Need review diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index 199e32b94a1..ec1429f5755 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -1,47 +1,13 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { BITCOIN_REFRESH_BALANCE_INTERVAL } from '@subwallet/extension-base/constants'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; -import { BalanceItem, UtxoResponseItem } from '@subwallet/extension-base/types'; -import { filteredOutTxsUtxos, getInscriptionUtxos, getRuneUtxos } from '@subwallet/extension-base/utils'; +import { BalanceItem } from '@subwallet/extension-base/types'; +import { getTransferableBitcoinUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; -export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { - try { - const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ - await bitcoinApi.api.getUtxos(address), - await getRuneUtxos(bitcoinApi, address), - await getInscriptionUtxos(bitcoinApi, address) - ]); - - let filteredUtxos: UtxoResponseItem[]; - - if (!utxos || !utxos.length) { - return []; - } - - // filter out pending utxos - // filteredUtxos = filterOutPendingTxsUtxos(utxos); - - // filter out rune utxos - filteredUtxos = filteredOutTxsUtxos(utxos, runeTxsUtxos); - - // filter out dust utxos - // filter out inscription utxos - filteredUtxos = filteredOutTxsUtxos(utxos, inscriptionUtxos); - - return filteredUtxos; - } catch (error) { - console.log('Error while fetching Bitcoin balances', error); - - return []; - } -}; - async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) { return await Promise.all(addresses.map(async (address) => { try { diff --git a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts index f6b3f9c01cd..b698ebf5648 100644 --- a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts +++ b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts @@ -1,11 +1,10 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { getTransferableBitcoinUtxos } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/bitcoin'; import { _BITCOIN_CHAIN_SLUG, _BITCOIN_NAME, _BITCOIN_TESTNET_NAME } from '@subwallet/extension-base/services/chain-service/constants'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { BitcoinFeeInfo, BitcoinFeeRate, FeeInfo, TransactionFee } from '@subwallet/extension-base/types'; -import { combineBitcoinFee, determineUtxosForSpend, determineUtxosForSpendAll } from '@subwallet/extension-base/utils'; +import { combineBitcoinFee, determineUtxosForSpend, determineUtxosForSpendAll, getTransferableBitcoinUtxos } from '@subwallet/extension-base/utils'; import { keyring } from '@subwallet/ui-keyring'; import BigN from 'bignumber.js'; import { Network, Psbt } from 'bitcoinjs-lib'; @@ -25,21 +24,16 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P const { bitcoinApi, chain, feeCustom: _feeCustom, feeInfo: _feeInfo, feeOption, from, network, to, transferAll, value } = params; const feeCustom = _feeCustom as BitcoinFeeRate; - console.log('_feeInfo', _feeInfo); - const feeInfo = _feeInfo as BitcoinFeeInfo; const bitcoinFee = combineBitcoinFee(feeInfo, feeOption, feeCustom); const utxos = await getTransferableBitcoinUtxos(bitcoinApi, from); - console.log('create.btc.bitcoinFee', bitcoinFee); - console.log('create.btc.utxos', utxos); - try { const amountValue = parseFloat(value); const determineUtxosArgs = { amount: amountValue, - feeRate: 0, + feeRate: bitcoinFee.feeRate, recipient: to, sender: from, utxos @@ -49,23 +43,10 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P ? determineUtxosForSpendAll(determineUtxosArgs) : determineUtxosForSpend(determineUtxosArgs); - console.log('create.btc.inputs', inputs); - console.log('create.btc.outputs', outputs); - console.log('create.btc.fee', fee); - // console.log(inputs, inputs.reduce((v, i) => v + i.value, 0)); - // console.log(outputs, (outputs as Array<{value: number}>).reduce((v, i) => v + i.value, 0)); - console.log('create.btc.bitcoinFee', bitcoinFee); - const pair = keyring.getPair('bc1qqn6ggclhsk2h5rmzy8v8akkh0mawcjesvcy6c9'); const tx = new Psbt({ network }); let transferAmount = new BigN(0); - console.log('create.btc.from', from); - console.log('create.btc.pair', pair); - console.log('create.btc.network', network); - console.log('create.btc.pair.bitcoin', pair.bitcoin); - console.log('create.btc.pair.bitcoin.output', pair.bitcoin.output); - for (const input of inputs) { if (pair.type === 'bitcoin-44' || pair.type === 'bittest-44') { const hex = await bitcoinApi.api.getTxHex(input.txid); @@ -102,8 +83,6 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P console.log(outputs, (outputs as Array<{value: number}>).reduce((v, i) => v + i.value, 0)); console.log(fee, bitcoinFee); - console.log('Transfer Amount:', transferAmount.toString()); - return [tx, transferAmount.toString()]; } catch (e) { // const error = e as Error; diff --git a/packages/extension-base/src/services/fee-service/service.ts b/packages/extension-base/src/services/fee-service/service.ts index 70ee3ab3782..6ac6609ce85 100644 --- a/packages/extension-base/src/services/fee-service/service.ts +++ b/packages/extension-base/src/services/fee-service/service.ts @@ -133,10 +133,10 @@ export default class FeeService { if (cancel) { clearInterval(interval); } else { - const api = this.state.getEvmApi(chain); - // TODO: Handle case type === evm and not have api if (type === 'evm') { + const api = this.state.getEvmApi(chain); + if (api) { calculateGasFeeParams(api, chain) .then((info) => { @@ -161,6 +161,14 @@ export default class FeeService { options: undefined } as EvmFeeInfo); } + } else if (type === 'bitcoin') { + const api = this.state.getBitcoinApi(chain); + + api.api.getRecommendedFeeRate() + .then((info) => { + observer.next(info); + }) + .catch(console.error); } else { observer.next({ type, diff --git a/packages/extension-base/src/services/transaction-service/helpers/index.ts b/packages/extension-base/src/services/transaction-service/helpers/index.ts index d1cfb4ac5ca..9b156e2d710 100644 --- a/packages/extension-base/src/services/transaction-service/helpers/index.ts +++ b/packages/extension-base/src/services/transaction-service/helpers/index.ts @@ -3,10 +3,10 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; -import { TransferBitcoinProps } from '@subwallet/extension-base/services/balance-service/transfer/bitcoin-transfer'; import { CardanoTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/cardano-transfer'; import { TonTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; import { SWTransaction } from '@subwallet/extension-base/services/transaction-service/types'; +import { Psbt } from 'bitcoinjs-lib'; import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; @@ -37,11 +37,8 @@ export const isCardanoTransaction = (tx: SWTransaction['transaction']): tx is Ca return cardanoTransactionConfig.cardanoPayload !== null && cardanoTransactionConfig.cardanoPayload !== undefined; }; -// TODO: Implement logic to check if the transaction is a Bitcoin transaction. -export const isBitcoinTransaction = (tx: SWTransaction['transaction']): tx is TransferBitcoinProps => { - const bitcoinTransactionConfig = tx as TransferBitcoinProps; - - return true; +export const isBitcoinTransaction = (tx: SWTransaction['transaction']): tx is Psbt => { + return 'data' in tx && Array.isArray((tx as Psbt).data.inputs); }; const typeName = (type: SWTransaction['extrinsicType']) => { diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 06d0eb336f2..bcd25185779 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -22,7 +22,7 @@ import { getBaseTransactionInfo, getTransactionId, isBitcoinTransaction, isCarda import { SWPermitTransaction, SWPermitTransactionInput, SWTransaction, SWTransactionInput, SWTransactionResponse, TransactionEmitter, TransactionEventMap, TransactionEventResponse, ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types'; import { getExplorerLink, parseTransactionData } from '@subwallet/extension-base/services/transaction-service/utils'; import { isWalletConnectRequest } from '@subwallet/extension-base/services/wallet-connect-service/helpers'; -import { AccountJson, BaseStepType, BasicTxErrorType, BasicTxWarningCode, BriefProcessStep, EvmFeeInfo, LeavePoolAdditionalData, PermitSwapData, ProcessStep, ProcessTransactionData, RequestStakePoolingBonding, RequestYieldStepSubmit, SpecialYieldPoolInfo, StepStatus, SubmitJoinNominationPool, SubstrateTipInfo, TransactionErrorType, Web3Transaction, YieldPoolType } from '@subwallet/extension-base/types'; +import { AccountJson, BaseStepType, BasicTxErrorType, BasicTxWarningCode, BriefProcessStep, LeavePoolAdditionalData, PermitSwapData, ProcessStep, ProcessTransactionData, RequestStakePoolingBonding, RequestYieldStepSubmit, SpecialYieldPoolInfo, StepStatus, SubmitJoinNominationPool, SubstrateTipInfo, TransactionErrorType, Web3Transaction, YieldPoolType } from '@subwallet/extension-base/types'; import { anyNumberToBN, pairToAccount, reformatAddress } from '@subwallet/extension-base/utils'; import { mergeTransactionAndSignature } from '@subwallet/extension-base/utils/eth/mergeTransactionAndSignature'; import { isContractAddress, parseContractInput } from '@subwallet/extension-base/utils/eth/parseTransaction'; @@ -110,7 +110,7 @@ export default class TransactionService { warnings: transactionInput.warnings || [], processId: transactionInput.step?.processId }; - const { additionalValidator, address, chain, extrinsicType } = validationResponse; + const { additionalValidator, address, chain, chainType, extrinsicType } = validationResponse; const chainInfo = this.state.chainService.getChainInfoByKey(chain); const blockedConfigObjects = await fetchBlockedConfigObjects(); @@ -137,7 +137,6 @@ export default class TransactionService { checkSupportForTransaction(validationResponse, transaction); if (!chainInfo) { - console.log('validate.2'); validationResponse.errors.push(new TransactionError(BasicTxErrorType.INTERNAL_ERROR, t('Cannot find network'))); } @@ -148,22 +147,19 @@ export default class TransactionService { const bitcoinApi = this.state.chainService.getBitcoinApi(chainInfo.slug); // todo: should split into isEvmTx && isNoEvmApi. Because other chains type also has no Evm Api. Same to all blockchain. // todo: refactor check evmTransaction. - const isNoEvmApi = transaction && !isSubstrateTransaction(transaction) && !isTonTransaction(transaction) && !isCardanoTransaction(transaction) && !evmApi; + const isNoEvmApi = transaction && !isSubstrateTransaction(transaction) && !isTonTransaction(transaction) && !isCardanoTransaction(transaction) && !isBitcoinTransaction(transaction) && !evmApi; const isNoTonApi = transaction && isTonTransaction(transaction) && !tonApi; const isNoCardanoApi = transaction && isCardanoTransaction(transaction) && !cardanoApi; const isNoBitcoinApi = transaction && isBitcoinTransaction(transaction) && !bitcoinApi; // TODO: template pass validation for bitcoin transfer - if (isNoTonApi || isNoCardanoApi) { - console.log('validate.isNoEvmApi', isNoEvmApi); - console.log('validate.isNoTonApi', isNoTonApi); - console.log('validate.isNoCardanoApi', isNoCardanoApi); + if (isNoEvmApi || isNoTonApi || isNoCardanoApi || isNoBitcoinApi) { validationResponse.errors.push(new TransactionError(BasicTxErrorType.CHAIN_DISCONNECTED, undefined)); } // Estimate fee for transaction const id = getId(); - const feeInfo = await this.state.feeService.subscribeChainFee(id, chain, 'evm') as EvmFeeInfo; + const feeInfo = await this.state.feeService.subscribeChainFee(id, chain, chainType); const nativeTokenInfo = this.state.chainService.getNativeTokenInfo(chain); const tokenPayFeeSlug = transactionInput.tokenPayFeeSlug; const isNonNativeTokenPayFee = tokenPayFeeSlug && !_isNativeTokenBySlug(tokenPayFeeSlug); @@ -176,7 +172,7 @@ export default class TransactionService { // Check account signing transaction - // TODO: template pass validation for bitcoin transfer + // TODO: If you want to test, remove the line below. // checkSigningAccountForTransaction(validationResponse, chainInfoMap); const nativeTokenAvailable = await this.state.balanceService.getTransferableBalance(address, chain, nativeTokenInfo.slug, extrinsicType); @@ -304,11 +300,6 @@ export default class TransactionService { const stopByErrors = validatedTransaction.errors.length > 0; const stopByWarnings = validatedTransaction.warnings.length > 0 && validatedTransaction.warnings.some((warning) => !ignoreWarnings.includes(warning.warningType)); - console.log('handle.validatedTransaction', validatedTransaction); - console.log('handle.ignoreWarnings', ignoreWarnings); - console.log('handle.stopByErrors', stopByErrors); - console.log('handle.stopByWarnings', stopByWarnings); - if (stopByErrors || stopByWarnings) { // @ts-ignore 'transaction' in validatedTransaction && delete validatedTransaction.transaction; diff --git a/packages/extension-base/src/services/transaction-service/types.ts b/packages/extension-base/src/services/transaction-service/types.ts index 6b0d1555c40..c542fe7ff98 100644 --- a/packages/extension-base/src/services/transaction-service/types.ts +++ b/packages/extension-base/src/services/transaction-service/types.ts @@ -90,6 +90,7 @@ export interface TransactionEventResponse extends ValidateTransactionResponse { eventLogs?: EventRecord[], nonce?: number, startBlock?: number, + blockTime?: number, } export interface TransactionEventMap { send: (response: TransactionEventResponse) => void; diff --git a/packages/extension-base/src/utils/bitcoin/common.ts b/packages/extension-base/src/utils/bitcoin/common.ts index c6e1bffdac6..93152380d71 100644 --- a/packages/extension-base/src/utils/bitcoin/common.ts +++ b/packages/extension-base/src/utils/bitcoin/common.ts @@ -1,7 +1,9 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 +import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { UtxoResponseItem } from '@subwallet/extension-base/types'; +import { filteredOutTxsUtxos, getInscriptionUtxos, getRuneUtxos } from '@subwallet/extension-base/utils'; import { BitcoinAddressType } from '@subwallet/keyring/types'; import { BtcSizeFeeEstimator, getBitcoinAddressInfo, validateBitcoinAddress } from '@subwallet/keyring/utils'; import BigN from 'bignumber.js'; @@ -63,3 +65,35 @@ export function getSpendableAmount ({ feeRate, fee }; } + +export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { + try { + const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ + await bitcoinApi.api.getUtxos(address), + await getRuneUtxos(bitcoinApi, address), + await getInscriptionUtxos(bitcoinApi, address) + ]); + + let filteredUtxos: UtxoResponseItem[]; + + if (!utxos || !utxos.length) { + return []; + } + + // filter out pending utxos + // filteredUtxos = filterOutPendingTxsUtxos(utxos); + + // filter out rune utxos + filteredUtxos = filteredOutTxsUtxos(utxos, runeTxsUtxos); + + // filter out dust utxos + // filter out inscription utxos + filteredUtxos = filteredOutTxsUtxos(utxos, inscriptionUtxos); + + return filteredUtxos; + } catch (error) { + console.log('Error while fetching Bitcoin balances', error); + + return []; + } +}; From 44cc52f2fe4dc5033b61c171a3a32c68a287b0fd Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 29 Apr 2025 19:25:47 +0700 Subject: [PATCH 062/178] [Issue-4263] feat: refactor code & review --- .../core/logic-validation/recipientAddress.ts | 2 - packages/extension-base/src/core/utils.ts | 1 - .../src/services/chain-service/utils/index.ts | 2 +- .../handler/BitcoinRequestHandler.ts | 5 +- .../src/services/transaction-service/index.ts | 5 +- .../src/Popup/Confirmations/index.tsx | 46 +- .../Confirmations/parts/Sign/Bitcoin.tsx | 4 +- ...coinSendTransactionRequestConfirmation.tsx | 566 +++++++++--------- .../variants/BitcoinSignPsbtConfirmation.tsx | 254 ++++---- .../variants/BitcoinSignatureConfirmation.tsx | 172 +++--- .../src/Popup/Confirmations/variants/index.ts | 6 +- 11 files changed, 528 insertions(+), 535 deletions(-) diff --git a/packages/extension-base/src/core/logic-validation/recipientAddress.ts b/packages/extension-base/src/core/logic-validation/recipientAddress.ts index 1983940d29b..a6678004982 100644 --- a/packages/extension-base/src/core/logic-validation/recipientAddress.ts +++ b/packages/extension-base/src/core/logic-validation/recipientAddress.ts @@ -133,7 +133,5 @@ export function validateRecipientAddress (validateRecipientParams: ValidateRecip const conditions = getConditions(validateRecipientParams); const validationFunctions = getValidationFunctions(conditions); - console.log('conditions', conditions); - return runValidationFunctions(validateRecipientParams, validationFunctions); } diff --git a/packages/extension-base/src/core/utils.ts b/packages/extension-base/src/core/utils.ts index 6d8215467c3..6108fd5dd65 100644 --- a/packages/extension-base/src/core/utils.ts +++ b/packages/extension-base/src/core/utils.ts @@ -114,7 +114,6 @@ export function _isValidCardanoAddressFormat (validateRecipientParams: ValidateR return ''; } -// TODO: Temporarily allow the transfer flow, review this function later. export function _isValidBitcoinAddressFormat (validateRecipientParams: ValidateRecipientParams): string { const { destChainInfo, toAddress } = validateRecipientParams; const addressInfo = validateBitcoinAddress(toAddress) ? getBitcoinAddressInfo(toAddress) : null; diff --git a/packages/extension-base/src/services/chain-service/utils/index.ts b/packages/extension-base/src/services/chain-service/utils/index.ts index 4f5e01d73ce..1bca43fbda4 100644 --- a/packages/extension-base/src/services/chain-service/utils/index.ts +++ b/packages/extension-base/src/services/chain-service/utils/index.ts @@ -142,7 +142,7 @@ export function _isTokenTransferredByCardano (tokenInfo: _ChainAsset) { return _isCIP26Token(tokenInfo) || _isNativeToken(tokenInfo); } -// TODO Note: Currently supports transferring only the native token, Bitcoin. +// TODO [Review]: Currently supports transferring only the native token, Bitcoin. export function _isTokenTransferredByBitcoin (tokenInfo: _ChainAsset) { return _isNativeToken(tokenInfo); } 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 19152296d3a..ac7f86aff52 100644 --- a/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts +++ b/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts @@ -111,9 +111,6 @@ export default class BitcoinRequestHandler { }; }); - console.log('confirmations.next', confirmations); - console.log('confirmations.isInternal', isInternal); - this.confirmationsQueueSubjectBitcoin.next(confirmations); if (!isInternal) { @@ -238,6 +235,8 @@ export default class BitcoinRequestHandler { bitcoinApi, chain, from, + feeCustom, + feeOption, feeInfo, to, transferAll: false, diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index bcd25185779..c8936891dfa 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -94,8 +94,6 @@ export default class TransactionService { .filter((item) => item.address === transaction.address && item.chain === transaction.chain); if (existed.length > 0) { - console.log('validate.3'); - return [new TransactionError(BasicTxErrorType.DUPLICATE_TRANSACTION)]; } @@ -152,7 +150,6 @@ export default class TransactionService { const isNoCardanoApi = transaction && isCardanoTransaction(transaction) && !cardanoApi; const isNoBitcoinApi = transaction && isBitcoinTransaction(transaction) && !bitcoinApi; - // TODO: template pass validation for bitcoin transfer if (isNoEvmApi || isNoTonApi || isNoCardanoApi || isNoBitcoinApi) { validationResponse.errors.push(new TransactionError(BasicTxErrorType.CHAIN_DISCONNECTED, undefined)); } @@ -261,7 +258,7 @@ export default class TransactionService { warnings: transaction.warnings || [], url: transaction.url || EXTENSION_REQUEST_URL, status: ExtrinsicStatus.QUEUED, - isInternal: false, + isInternal, id: transactionId, extrinsicHash: transactionId } as SWTransaction; diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx index c77ee72505a..a80560396be 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx @@ -20,7 +20,7 @@ import { SignerPayloadJSON } from '@polkadot/types/types'; import { isEthereumAddress } from '@polkadot/util-crypto'; import { ConfirmationHeader } from './parts'; -import { AddNetworkConfirmation, AddTokenConfirmation, AuthorizeConfirmation, BitcoinSendTransactionRequestConfirmation, BitcoinSignatureConfirmation, BitcoinSignPsbtConfirmation, ConnectWalletConnectConfirmation, EvmSignatureConfirmation, EvmSignatureWithProcess, EvmTransactionConfirmation, MetadataConfirmation, NetworkConnectionErrorConfirmation, NotSupportConfirmation, NotSupportWCConfirmation, SignConfirmation, TransactionConfirmation } from './variants'; +import { AddNetworkConfirmation, AddTokenConfirmation, AuthorizeConfirmation, ConnectWalletConnectConfirmation, EvmSignatureConfirmation, EvmSignatureWithProcess, EvmTransactionConfirmation, MetadataConfirmation, NetworkConnectionErrorConfirmation, NotSupportConfirmation, NotSupportWCConfirmation, SignConfirmation, TransactionConfirmation } from './variants'; type Props = ThemeProps @@ -53,8 +53,6 @@ const Component = function ({ className }: Props) { const { alertProps, closeAlert, openAlert } = useAlert(alertModalId); const { transactionRequest } = useSelector((state) => state.requestState); - console.log('confirmation', confirmation); - const nextConfirmation = useCallback(() => { setIndex((val) => Math.min(val + 1, numberOfConfirmations - 1)); }, [numberOfConfirmations]); @@ -177,27 +175,27 @@ const Component = function ({ className }: Props) { /> ); - case 'bitcoinSignatureRequest': - return ( - - ); - case 'bitcoinSignPsbtRequest': - return ( - - ); - case 'bitcoinSendTransactionRequestAfterConfirmation': - return ( - - ); + // 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 ba4db43b666..424b5c97390 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 @@ -56,6 +56,7 @@ const Component: React.FC = (props: Props) => { const { canSign, className, editedPayload, extrinsicType, id, payload, type } = props; const { payload: { hashPayload } } = payload; const { account } = (payload.payload as BitcoinSignatureRequest); + // TODO: [Review] Error eslint const chainId = (payload.payload as EvmSendTransactionRequest)?.chainId || 1; const { t } = useTranslation(); @@ -178,7 +179,8 @@ 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); + // TODO: Review metadata of ledgerSignTransaction + const signPromise = isMessage ? ledgerSignMessage(u8aToU8a(hashPayload), account?.accountIndex, account?.addressOffset) : ledgerSignTransaction(hexToU8a(hashPayload), new Uint8Array(0), account?.accountIndex, account?.addressOffset); signPromise .then(({ signature }) => { diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx index 6f3674645d3..daa033ac1af 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx @@ -1,286 +1,286 @@ // 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, ResponseSubscribeTransferConfirmation, TransactionFee } from '@subwallet/extension-base/types'; -import { 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, useNotification } from '@subwallet/extension-koni-ui/hooks'; -import { cancelSubscription, subscribeTransferWhenConfirmation } 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, networkKey, to, tokenSlug, value } } = request; - const { t } = useTranslation(); - const transferAmountValue = useMemo(() => value?.toString() as string, [value]); - const fromValue = useMemo(() => account.address, [account.address]); - const toValue = useMemo(() => to ? to[0].address : '', [to]); - const chainValue = useMemo(() => networkKey as string, [networkKey]); - const assetValue = useMemo(() => tokenSlug as string, [tokenSlug]); - - const [transactionInfo, setTransactionInfo] = useState({ - id, - chain: networkKey as string, - from: account.address, - to: toValue, - tokenSlug: tokenSlug as string, - transferAll: false, - value: value?.toString() || '0' - }); - const [isFetchingInfo, setIsFetchingInfo] = useState(false); - const [transferInfo, setTransferInfo] = useState(); - const [transactionFeeInfo, setTransactionFeeInfo] = useState(undefined); - const [isErrorTransaction, setIsErrorTransaction] = useState(false); - const notify = useNotification(); - const assetRegistry = useSelector((root: RootState) => root.assetRegistry.assetRegistry); - - 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(() => { - let cancel = false; - let id = ''; - let timeout: NodeJS.Timeout; - - setIsFetchingInfo(true); - - const callback = (transferInfo: ResponseSubscribeTransferConfirmation) => { - if (transferInfo.error) { - notify({ - message: t(transferInfo.error), - type: 'error', - duration: 8 - }); - setIsErrorTransaction(true); - } else if (!cancel) { - setTransferInfo(transferInfo); - id = transferInfo.id; - } else { - cancelSubscription(transferInfo.id).catch(console.error); - } - }; - - if (fromValue && assetValue) { - timeout = setTimeout(() => { - subscribeTransferWhenConfirmation({ - address: fromValue, - chain: chainValue, - token: assetValue, - destChain: chainValue, - feeOption: transactionFeeInfo?.feeOption, - feeCustom: transactionFeeInfo?.feeCustom, - value: transferAmountValue || '0', - transferAll: false, - to: toValue - }, callback) - .then(callback) - .catch((e) => { - console.error(e); - notify({ - message: t(e), - type: 'error', - duration: 8 - }); - setIsErrorTransaction(true); - setTransferInfo(undefined); - }) - .finally(() => { - setIsFetchingInfo(false); - }); - }, 100); - } - - return () => { - cancel = true; - clearTimeout(timeout); - id && cancelSubscription(id).catch(console.error); - }; - }, [assetRegistry, assetValue, chainValue, fromValue, toValue, transactionFeeInfo, transferAmountValue, notify, t]); - - return ( - <> -
-
{getDomainFromUrl(request.url)}
- - - - - - - - - - - - {!isErrorTransaction && } - - - {/* {!!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; +// import { _ChainAsset } from '@subwallet/chain-list/types'; +// import { BitcoinSendTransactionRequest, ConfirmationsQueueItem } from '@subwallet/extension-base/background/KoniTypes'; +// import { BitcoinFeeDetail, RequestSubmitTransferWithId, ResponseSubscribeTransferConfirmation, TransactionFee } from '@subwallet/extension-base/types'; +// import { 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, useNotification } from '@subwallet/extension-koni-ui/hooks'; +// import { cancelSubscription, subscribeTransferWhenConfirmation } 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, networkKey, to, tokenSlug, value } } = request; +// const { t } = useTranslation(); +// const transferAmountValue = useMemo(() => value?.toString() as string, [value]); +// const fromValue = useMemo(() => account.address, [account.address]); +// const toValue = useMemo(() => to ? to[0].address : '', [to]); +// const chainValue = useMemo(() => networkKey as string, [networkKey]); +// const assetValue = useMemo(() => tokenSlug as string, [tokenSlug]); +// +// const [transactionInfo, setTransactionInfo] = useState({ +// id, +// chain: networkKey as string, +// from: account.address, +// to: toValue, +// tokenSlug: tokenSlug as string, +// transferAll: false, +// value: value?.toString() || '0' +// }); +// const [isFetchingInfo, setIsFetchingInfo] = useState(false); +// const [transferInfo, setTransferInfo] = useState(); +// const [transactionFeeInfo, setTransactionFeeInfo] = useState(undefined); +// const [isErrorTransaction, setIsErrorTransaction] = useState(false); +// const notify = useNotification(); +// const assetRegistry = useSelector((root: RootState) => root.assetRegistry.assetRegistry); +// +// 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(() => { +// let cancel = false; +// let id = ''; +// let timeout: NodeJS.Timeout; +// +// setIsFetchingInfo(true); +// +// const callback = (transferInfo: ResponseSubscribeTransferConfirmation) => { +// if (transferInfo.error) { +// notify({ +// message: t(transferInfo.error), +// type: 'error', +// duration: 8 +// }); +// setIsErrorTransaction(true); +// } else if (!cancel) { +// setTransferInfo(transferInfo); +// id = transferInfo.id; +// } else { +// cancelSubscription(transferInfo.id).catch(console.error); +// } +// }; +// +// if (fromValue && assetValue) { +// timeout = setTimeout(() => { +// subscribeTransferWhenConfirmation({ +// address: fromValue, +// chain: chainValue, +// token: assetValue, +// destChain: chainValue, +// feeOption: transactionFeeInfo?.feeOption, +// feeCustom: transactionFeeInfo?.feeCustom, +// value: transferAmountValue || '0', +// transferAll: false, +// to: toValue +// }, callback) +// .then(callback) +// .catch((e) => { +// console.error(e); +// notify({ +// message: t(e), +// type: 'error', +// duration: 8 +// }); +// setIsErrorTransaction(true); +// setTransferInfo(undefined); +// }) +// .finally(() => { +// setIsFetchingInfo(false); +// }); +// }, 100); +// } +// +// return () => { +// cancel = true; +// clearTimeout(timeout); +// id && cancelSubscription(id).catch(console.error); +// }; +// }, [assetRegistry, assetValue, chainValue, fromValue, toValue, transactionFeeInfo, transferAmountValue, notify, t]); +// +// return ( +// <> +//
+//
{getDomainFromUrl(request.url)}
+// +// +// +// +// +// +// +// +// +// +// +// {!isErrorTransaction && } +// +// +// {/* {!!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 index ea51e7f878e..d39fba0a1b5 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignPsbtConfirmation.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignPsbtConfirmation.tsx @@ -1,130 +1,130 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { _ChainAsset } from '@subwallet/chain-list/types'; -import { BitcoinSignPsbtRequest, ConfirmationsQueueItem, PsbtTransactionArg } 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 { RootState } from '@subwallet/extension-koni-ui/stores'; -import { BitcoinSignatureSupportType, ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { findAccountByAddress } from '@subwallet/extension-koni-ui/utils'; -import { Button, Number } from '@subwallet/react-ui'; -import CN from 'classnames'; -import React, { useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; -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 { tokenSlug, txInput, txOutput } = request.payload.payload; - const accounts = useSelector((state: RootState) => state.accountState.accounts); - const assetRegistry = useSelector((root: RootState) => root.assetRegistry.assetRegistry); - const onClickDetail = useOpenDetailModal(); - const assetInfo: _ChainAsset | undefined = useMemo(() => { - return assetRegistry[tokenSlug]; - }, [assetRegistry, tokenSlug]); - const renderAccount = useCallback((accountsPsbt: PsbtTransactionArg[]) => { - return ( -
- { - accountsPsbt.map(({ address, amount }) => { - const account = findAccountByAddress(accounts, address); - - return ( - : <>} - />); - } - ) - } - -
- ); - }, [accounts, assetInfo.decimals, assetInfo.symbol]); - - return ( - <> -
- -
- {t('Signature required')} -
-
- {t('You are approving a request with the following account')} -
- -
- -
-
- - - - - {renderAccount(txInput)} - - - {renderAccount(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; +// import { _ChainAsset } from '@subwallet/chain-list/types'; +// import { BitcoinSignPsbtRequest, ConfirmationsQueueItem, PsbtTransactionArg } 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 { RootState } from '@subwallet/extension-koni-ui/stores'; +// import { BitcoinSignatureSupportType, ThemeProps } from '@subwallet/extension-koni-ui/types'; +// import { findAccountByAddress } from '@subwallet/extension-koni-ui/utils'; +// import { Button, Number } from '@subwallet/react-ui'; +// import CN from 'classnames'; +// import React, { useCallback, useMemo } from 'react'; +// import { useTranslation } from 'react-i18next'; +// import { useSelector } from 'react-redux'; +// 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 { tokenSlug, txInput, txOutput } = request.payload.payload; +// const accounts = useSelector((state: RootState) => state.accountState.accounts); +// const assetRegistry = useSelector((root: RootState) => root.assetRegistry.assetRegistry); +// const onClickDetail = useOpenDetailModal(); +// const assetInfo: _ChainAsset | undefined = useMemo(() => { +// return assetRegistry[tokenSlug]; +// }, [assetRegistry, tokenSlug]); +// const renderAccount = useCallback((accountsPsbt: PsbtTransactionArg[]) => { +// return ( +//
+// { +// accountsPsbt.map(({ address, amount }) => { +// const account = findAccountByAddress(accounts, address); +// +// return ( +// : <>} +// />); +// } +// ) +// } +// +//
+// ); +// }, [accounts, assetInfo.decimals, assetInfo.symbol]); +// +// return ( +// <> +//
+// +//
+// {t('Signature required')} +//
+//
+// {t('You are approving a request with the following account')} +//
+// +//
+// +//
+//
+// +// +// +// +// {renderAccount(txInput)} +// +// +// {renderAccount(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 index 252a959146f..d78b7af97b6 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignatureConfirmation.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignatureConfirmation.tsx @@ -1,89 +1,89 @@ // 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 { 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(); - // TODO: Temporarily comment out the AccountItemWithName component and recheck later. - 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; +// import { BitcoinSignatureRequest, ConfirmationsQueueItem } from '@subwallet/extension-base/background/KoniTypes'; +// import { 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(); +// // TODO: Temporarily comment out the AccountItemWithName component and recheck later. +// 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/index.ts b/packages/extension-koni-ui/src/Popup/Confirmations/variants/index.ts index c96fd5aab34..a1804b3b547 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/index.ts +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/index.ts @@ -11,9 +11,9 @@ 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'; +// export { default as BitcoinSignatureConfirmation } from './BitcoinSignatureConfirmation'; +// export { default as BitcoinSignPsbtConfirmation } from './BitcoinSignPsbtConfirmation'; +// export { default as BitcoinSendTransactionRequestConfirmation } from './BitcoinSendTransactionRequestConfirmation'; export * from './Error'; export * from './Message'; From 65327bfc6a2e06fbd348d72fae6730bc3c1f8e6c Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 5 May 2025 12:12:39 +0700 Subject: [PATCH 063/178] [Issue-4263] refactor: restore checkSigningAccountForTransaction function --- .../extension-base/src/services/transaction-service/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index c8936891dfa..2b186bb269f 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -5,7 +5,7 @@ import { EvmProviderError } from '@subwallet/extension-base/background/errors/Ev import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { AmountData, BitcoinSignatureRequest, ChainType, EvmProviderErrorType, EvmSendTransactionRequest, EvmSignatureRequest, ExtrinsicStatus, ExtrinsicType, NotificationType, TransactionAdditionalInfo, TransactionDirection, TransactionHistoryItem } from '@subwallet/extension-base/background/KoniTypes'; import { _SUPPORT_TOKEN_PAY_FEE_GROUP, ALL_ACCOUNT_KEY, fetchBlockedConfigObjects, fetchLatestBlockedActionsAndFeatures, getPassConfigId } from '@subwallet/extension-base/constants'; -import { checkBalanceWithTransactionFee, checkSupportForAction, checkSupportForFeature, checkSupportForTransaction, estimateFeeForTransaction } from '@subwallet/extension-base/core/logic-validation/transfer'; +import { checkBalanceWithTransactionFee, checkSigningAccountForTransaction, checkSupportForAction, checkSupportForFeature, checkSupportForTransaction, estimateFeeForTransaction } from '@subwallet/extension-base/core/logic-validation/transfer'; import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; import { cellToBase64Str, externalMessage, getTransferCellPromise } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/utils'; import { CardanoTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/cardano-transfer'; @@ -169,8 +169,7 @@ export default class TransactionService { // Check account signing transaction - // TODO: If you want to test, remove the line below. - // checkSigningAccountForTransaction(validationResponse, chainInfoMap); + checkSigningAccountForTransaction(validationResponse, chainInfoMap); const nativeTokenAvailable = await this.state.balanceService.getTransferableBalance(address, chain, nativeTokenInfo.slug, extrinsicType); From fd018d60c6ab521318f41680a70c17497cca30e3 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 5 May 2025 15:34:02 +0700 Subject: [PATCH 064/178] [Issue-4263] refactor: update Code to adapt to MasterAccount Interface --- .../extension-base/src/services/transaction-service/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 2b186bb269f..67dfa1ced71 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -1711,7 +1711,7 @@ export default class TransactionService { // const chainInfo = this.state.chainService.getChainInfoByKey(chain); const accountPair = keyring.getPair(address); - const account: AccountJson = { address, ...accountPair.meta }; + const account: AccountJson = pairToAccount(accountPair); const payload: BitcoinSignatureRequest = { payload: undefined, From b3ea6a4438aa47f70fcb7a00f819c88c7dc70e1a Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 5 May 2025 16:11:27 +0700 Subject: [PATCH 065/178] [Issue-4263] chores: downgrade bitcoinjs-lib to 6.1.5 to adapt to version keyring --- package.json | 3 ++- yarn.lock | 24 +++++------------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 49c85a0b054..b01e884da26 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ }, "dependencies": { "@types/node": "^17.0.10", - "bitcoinjs-lib": "^6.1.7", + "bitcoinjs-lib": "6.1.5", "dexie": "^3.2.2", "loglevel": "^1.8.1", "react-markdown": "^9.0.1", @@ -109,6 +109,7 @@ "@polkadot/x-global": "^13.4.3", "@subwallet/chain-list": "0.2.103", "@subwallet/keyring": "^0.1.10", + "bitcoinjs-lib": "6.1.5", "@subwallet/react-ui": "5.1.2-b79", "@subwallet/ui-keyring": "^0.1.10", "@types/bn.js": "^5.1.6", diff --git a/yarn.lock b/yarn.lock index 70dc103498c..7b6d3328716 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11222,9 +11222,9 @@ __metadata: languageName: node linkType: hard -"bitcoinjs-lib@npm:^6.1.5": - version: 6.1.6 - resolution: "bitcoinjs-lib@npm:6.1.6" +"bitcoinjs-lib@npm:6.1.5": + version: 6.1.5 + resolution: "bitcoinjs-lib@npm:6.1.5" dependencies: "@noble/hashes": ^1.2.0 bech32: ^2.0.0 @@ -11232,21 +11232,7 @@ __metadata: bs58check: ^3.0.1 typeforce: ^1.11.3 varuint-bitcoin: ^1.1.2 - checksum: 04370cf6991c8343eb749fbeeff357e45b7c92e28272c4c731b6c3a45d3e67bfca90db96175cdca0479f84f17bf91aa02b3d113b7c80d73dd5494f431e140574 - languageName: node - linkType: hard - -"bitcoinjs-lib@npm:^6.1.7": - version: 6.1.7 - resolution: "bitcoinjs-lib@npm:6.1.7" - dependencies: - "@noble/hashes": ^1.2.0 - bech32: ^2.0.0 - bip174: ^2.1.1 - bs58check: ^3.0.1 - typeforce: ^1.11.3 - varuint-bitcoin: ^1.1.2 - checksum: 2fbac2bffc2fe0e1d5441fc09f092fa8a83a4572b10fd2601c960e19a60cdcab81b6bff8cefcefda80b952fe8795387c97219fc4676995bf554f26f3d9aadc58 + checksum: c45580863efca0abecfcfea194d7e6d2abeec29a4c7928c77b4af57936b9908f0d85175aa2208232a568de9cfb8ef75d1acfb1283c98dc41da20fe8f1462bb86 languageName: node linkType: hard @@ -25922,7 +25908,7 @@ __metadata: "@types/jest": ^29.5.0 "@types/node": ^17.0.10 axios: ^1.6.2 - bitcoinjs-lib: ^6.1.7 + bitcoinjs-lib: 6.1.5 dexie: ^3.2.2 discord-webhook-node: ^1.1.8 i18next-scanner: ^4.0.0 From 0f204a98b306916557f7e504450518b8d8a3f7de Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 5 May 2025 16:45:56 +0700 Subject: [PATCH 066/178] [Issue-4263] refactor: adjust to real data --- .../src/services/balance-service/transfer/bitcoin-transfer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts index b698ebf5648..2419bac90f6 100644 --- a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts +++ b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts @@ -43,7 +43,7 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P ? determineUtxosForSpendAll(determineUtxosArgs) : determineUtxosForSpend(determineUtxosArgs); - const pair = keyring.getPair('bc1qqn6ggclhsk2h5rmzy8v8akkh0mawcjesvcy6c9'); + const pair = keyring.getPair(from); const tx = new Psbt({ network }); let transferAmount = new BigN(0); @@ -70,7 +70,7 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P for (const output of outputs) { tx.addOutput({ - address: 'bc1qqn6ggclhsk2h5rmzy8v8akkh0mawcjesvcy6c9', + address: output.address || from, value: output.value }); From f54cfdec7fe92b6b8cf435a2c8318af95dd53799 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 5 May 2025 18:49:59 +0700 Subject: [PATCH 067/178] [Issue-4263] refactor: remove duplicated messaging --- .../src/koni/background/handlers/Extension.ts | 18 +----------------- .../src/messaging/confirmation/base.ts | 2 +- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 778564ef6c2..ea68336d962 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -2300,20 +2300,6 @@ export default class KoniExtension { return await this.#koniState.completeConfirmationCardano(request); } - private subscribeBitcoinConfirmations (id: string, port: chrome.runtime.Port) { - const cb = createSubscription<'pri(confirmations.bitcoin.subscribe)'>(id, port); - - const subscription = this.#koniState.requestService.confirmationsQueueSubjectBitcoin.subscribe(cb); - - this.createUnsubscriptionHandle(id, subscription.unsubscribe); - - port.onDisconnect.addListener((): void => { - this.cancelSubscription(id); - }); - - return this.#koniState.requestService.confirmationsQueueSubjectBitcoin.getValue(); - } - private async completeConfirmationBitcoin (request: RequestConfirmationCompleteBitcoin) { return await this.#koniState.completeConfirmationBitcoin(request); } @@ -5141,9 +5127,7 @@ export default class KoniExtension { return await this.completeConfirmationTon(request as RequestConfirmationCompleteTon); case 'pri(confirmationsCardano.complete)': return await this.completeConfirmationCardano(request as RequestConfirmationCompleteCardano); - case 'pri(confirmations.bitcoin.subscribe)': - return this.subscribeBitcoinConfirmations(id, port); - case 'pri(confirmations.bitcoin.complete)': + case 'pri(confirmationsBitcoin.complete)': return await this.completeConfirmationBitcoin(request as RequestConfirmationCompleteBitcoin); /// Stake diff --git a/packages/extension-koni-ui/src/messaging/confirmation/base.ts b/packages/extension-koni-ui/src/messaging/confirmation/base.ts index e12596f6ad4..d6464d74965 100644 --- a/packages/extension-koni-ui/src/messaging/confirmation/base.ts +++ b/packages/extension-koni-ui/src/messaging/confirmation/base.ts @@ -41,5 +41,5 @@ export async function completeConfirmationCardano (type: CT, payload: ConfirmationDefinitionsBitcoin[CT][1]): Promise { - return sendMessage('pri(confirmations.bitcoin.complete)', { [type]: payload }); + return sendMessage('pri(confirmationsBitcoin.complete)', { [type]: payload }); } From 7de357aeed111ed39e5c3aa72d53feceda7a18d0 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 5 May 2025 19:14:45 +0700 Subject: [PATCH 068/178] [Issue-4263] refactor: remove interface dapp --- packages/extension-base/src/background/KoniTypes.ts | 6 ++---- .../src/services/transaction-service/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index d501f8cedcf..78e0926c386 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -1202,7 +1202,7 @@ export interface EvmSendTransactionRequest extends TransactionConfig, EvmSignReq errors?: ErrorValidation[] } -export interface BitcoinSendTransactionRequest extends BitcoinSignRequest, BitcoinTransactionConfig {} +export type BitcoinSendTransactionRequest = BitcoinSignRequest export interface BitcoinSignatureRequest extends BitcoinSignRequest { id: string; @@ -2409,9 +2409,6 @@ export interface KoniRequestSignatures { 'pri(accounts.getOptimalTransferProcess)': [RequestOptimalTransferProcess, CommonOptimalTransferPath]; 'pri(accounts.approveSpending)': [TokenSpendingApprovalParams, SWTransactionResponse]; - 'pri(confirmations.bitcoin.subscribe)': [RequestConfirmationsSubscribe, ConfirmationsQueueBitcoin, ConfirmationsQueueBitcoin]; - 'pri(confirmations.bitcoin.complete)': [RequestConfirmationCompleteBitcoin, boolean]; - 'pri(accounts.checkCrossChainTransfer)': [RequestCheckCrossChainTransfer, ValidateTransactionResponse]; 'pri(accounts.crossChainTransfer)': [RequestCrossChainTransfer, SWTransactionResponse]; @@ -2426,6 +2423,7 @@ export interface KoniRequestSignatures { 'pri(confirmations.complete)': [RequestConfirmationComplete, boolean]; 'pri(confirmationsTon.complete)': [RequestConfirmationCompleteTon, boolean]; 'pri(confirmationsCardano.complete)': [RequestConfirmationCompleteCardano, boolean]; + 'pri(confirmationsBitcoin.complete)': [RequestConfirmationCompleteBitcoin, boolean]; 'pub(utils.getRandom)': [RandomTestRequest, number]; 'pub(accounts.listV2)': [RequestAccountList, InjectedAccount[]]; diff --git a/packages/extension-base/src/services/transaction-service/types.ts b/packages/extension-base/src/services/transaction-service/types.ts index c542fe7ff98..57c4352648d 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 { BitcoinTransactionConfig, ChainType, ExtrinsicDataTypeMap, ExtrinsicStatus, ExtrinsicType, FeeData, ValidateTransactionResponse } from '@subwallet/extension-base/background/KoniTypes'; +import { ChainType, ExtrinsicDataTypeMap, ExtrinsicStatus, ExtrinsicType, FeeData, ValidateTransactionResponse } from '@subwallet/extension-base/background/KoniTypes'; import { SignTypedDataMessageV3V4 } from '@subwallet/extension-base/core/logic-validation'; import { TonTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; import { BaseRequestSign, BriefProcessStep, ProcessTransactionData, TransactionFee } from '@subwallet/extension-base/types'; @@ -27,7 +27,7 @@ export interface SWTransaction extends ValidateTransactionResponse, Partial Promise; eventsHandler?: (eventEmitter: TransactionEmitter) => void; isPassConfirmation?: boolean; From 6833248b4a7cca6470e9387d766c3e0df08abff4 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 6 May 2025 10:53:38 +0700 Subject: [PATCH 069/178] [Issue-4263] feat: add `input bip86` into bitcoin transaction --- .../transfer/bitcoin-transfer.ts | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts index 2419bac90f6..9317daa1ec5 100644 --- a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts +++ b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts @@ -5,6 +5,8 @@ import { _BITCOIN_CHAIN_SLUG, _BITCOIN_NAME, _BITCOIN_TESTNET_NAME } from '@subw import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { BitcoinFeeInfo, BitcoinFeeRate, FeeInfo, TransactionFee } from '@subwallet/extension-base/types'; import { combineBitcoinFee, determineUtxosForSpend, determineUtxosForSpendAll, getTransferableBitcoinUtxos } from '@subwallet/extension-base/utils'; +import { BitcoinAddressType } from '@subwallet/keyring/types'; +import { getBitcoinAddressInfo } from '@subwallet/keyring/utils'; import { keyring } from '@subwallet/ui-keyring'; import BigN from 'bignumber.js'; import { Network, Psbt } from 'bitcoinjs-lib'; @@ -48,7 +50,10 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P let transferAmount = new BigN(0); for (const input of inputs) { - if (pair.type === 'bitcoin-44' || pair.type === 'bittest-44') { + const addressInfo = getBitcoinAddressInfo(pair.address); + + if (addressInfo.type === BitcoinAddressType.p2pkh || addressInfo.type === BitcoinAddressType.p2sh) { + // BIP-44 (Legacy) const hex = await bitcoinApi.api.getTxHex(input.txid); tx.addInput({ @@ -56,7 +61,8 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P index: input.vout, nonWitnessUtxo: Buffer.from(hex, 'hex') }); - } else { + } else if (addressInfo.type === BitcoinAddressType.p2wpkh) { + // BIP-84 (Native SegWit) tx.addInput({ hash: input.txid, index: input.vout, @@ -65,6 +71,19 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P value: input.value } }); + } else if (addressInfo.type === BitcoinAddressType.p2tr) { + // BIP-86 (Taproot) + tx.addInput({ + hash: input.txid, + index: input.vout, + witnessUtxo: { + script: pair.bitcoin.output, + value: input.value // UTXO value in satoshis + }, + tapInternalKey: pair.bitcoin.internalPubkey.slice(1) // X-only public key (32 bytes) + }); + } else { + throw new Error(`Unsupported address type: ${addressInfo.type}`); } } From e67ea9bdabec7e489f5713a3b6197c6f26175b01 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 6 May 2025 10:59:38 +0700 Subject: [PATCH 070/178] [Issue-4263] chore(autobuild): update configurations for Bitcoin transfer --- .github/workflows/push-koni-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index a49e56adb15..707b5a24fa4 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -6,6 +6,7 @@ on: - upgrade-ui - subwallet-dev - koni/dev/issue-4200-v2 + - koni/dev/issue-4094-v2 push: branches: - koni-dev From 45ceb86efc41547d1c1850072d0aac6b5ce33b05 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 6 May 2025 11:16:46 +0700 Subject: [PATCH 071/178] [Issue-4263] chore(lint): fix eslint after merge 4094 --- packages/extension-base/src/background/KoniTypes.ts | 3 +-- .../extension-base/src/core/logic-validation/transfer.ts | 3 +-- .../extension-base/src/koni/background/handlers/State.ts | 2 +- .../services/balance-service/helpers/subscribe/bitcoin.ts | 6 +++--- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 66b28d7631c..a0676578c53 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -1400,6 +1400,7 @@ export interface ConfirmationDefinitionsBitcoin { export type ConfirmationType = keyof ConfirmationDefinitions; export type ConfirmationTypeTon = keyof ConfirmationDefinitionsTon; export type ConfirmationTypeCardano = keyof ConfirmationDefinitionsCardano; +export type ConfirmationTypeBitcoin = keyof ConfirmationDefinitionsBitcoin; export type ConfirmationsQueue = { [CT in ConfirmationType]: Record; @@ -1411,8 +1412,6 @@ export type ConfirmationsQueueCardano = { [CT in ConfirmationTypeCardano]: Record; } -export type ConfirmationTypeBitcoin = keyof ConfirmationDefinitionsBitcoin; - export type ConfirmationsQueueBitcoin = { [CT in ConfirmationTypeBitcoin]: Record; } diff --git a/packages/extension-base/src/core/logic-validation/transfer.ts b/packages/extension-base/src/core/logic-validation/transfer.ts index 680b86a9e86..aa3f03094cb 100644 --- a/packages/extension-base/src/core/logic-validation/transfer.ts +++ b/packages/extension-base/src/core/logic-validation/transfer.ts @@ -23,7 +23,6 @@ import { KeyringPair } from '@subwallet/keyring/types'; import { keyring } from '@subwallet/ui-keyring'; import BigN from 'bignumber.js'; import { t } from 'i18next'; -import { TransactionConfig } from 'web3-core'; import { isEthereumAddress } from '@polkadot/util-crypto'; @@ -387,7 +386,7 @@ export async function estimateFeeForTransaction (validationResponse: SWTransacti estimateFee.value = (feeCombine.feeRate * sizeInfo.txVBytes).toString(); } else { - const _transaction = transaction as TransactionConfig; + const _transaction = transaction; const gasLimit = _transaction.gas || await evmApi.api.eth.estimateGas(_transaction); const feeCombine = combineEthFee(feeInfo as EvmFeeInfo, validationResponse.feeOption, validationResponse.feeCustom as EvmEIP1559FeeOption); diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index 9e0bef54ca9..d9c9a3e8732 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -8,7 +8,7 @@ import { EvmProviderError } from '@subwallet/extension-base/background/errors/Ev import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { isSubscriptionRunning, unsubscribe } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AddressCardanoTransactionBalance, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, ConfirmationsQueueBitcoin, CardanoKeyType, CardanoProviderErrorType, CardanoSignatureRequest, CardanoTransactionDappConfig, ChainStakingMetadata, ChainType, ConfirmationDefinitions, ConfirmationsQueue, ConfirmationsQueueCardano, ConfirmationsQueueTon, CrowdloanItem, CrowdloanJson, CurrencyType, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestCardanoSignData, RequestCardanoSignTransaction, RequestConfirmationComplete, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCardanoSignData, ResponseCardanoSignTransaction, ServiceInfo, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; +import { AddressCardanoTransactionBalance, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, CardanoKeyType, CardanoProviderErrorType, CardanoSignatureRequest, CardanoTransactionDappConfig, ChainStakingMetadata, ChainType, ConfirmationDefinitions, ConfirmationsQueue, ConfirmationsQueueBitcoin, ConfirmationsQueueCardano, ConfirmationsQueueTon, CrowdloanItem, CrowdloanJson, CurrencyType, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestCardanoSignData, RequestCardanoSignTransaction, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCardanoSignData, ResponseCardanoSignTransaction, ServiceInfo, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; import { RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning } from '@subwallet/extension-base/background/types'; import { BACKEND_API_URL, EnvConfig, MANTA_PAY_BALANCE_INTERVAL, REMIND_EXPORT_ACCOUNT } from '@subwallet/extension-base/constants'; import { convertErrorFormat, generateValidationProcess, PayloadValidated, ValidateStepFunction, validationAuthMiddleware, validationAuthWCMiddleware, validationCardanoSignDataMiddleware, validationConnectMiddleware, validationEvmDataTransactionMiddleware, validationEvmSignMessageMiddleware } from '@subwallet/extension-base/core/logic-validation'; diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index e988117809c..b5553e2f2c1 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -1,12 +1,12 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 +import { _AssetType } from '@subwallet/chain-list/types'; import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { BITCOIN_REFRESH_BALANCE_INTERVAL } from '@subwallet/extension-base/constants'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; -import { BalanceItem, SusbcribeBitcoinPalletBalance, UtxoResponseItem } from '@subwallet/extension-base/types'; -import { filterAssetsByChainAndType, filteredOutTxsUtxos, getInscriptionUtxos, getRuneUtxos } from '@subwallet/extension-base/utils'; -import { getTransferableBitcoinUtxos } from '@subwallet/extension-base/utils'; +import { BalanceItem, SusbcribeBitcoinPalletBalance } from '@subwallet/extension-base/types'; +import { filterAssetsByChainAndType, getTransferableBitcoinUtxos } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) { From fb77a698bf4769612629d5c83a29aa96ea544434 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 6 May 2025 16:12:10 +0700 Subject: [PATCH 072/178] [Issue-4263] feat: temporarily bypass subscribeMaxTransferable for bitcoin --- .../extension-base/src/utils/fee/transfer.ts | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/extension-base/src/utils/fee/transfer.ts b/packages/extension-base/src/utils/fee/transfer.ts index cde10cde46d..28c76ff0196 100644 --- a/packages/extension-base/src/utils/fee/transfer.ts +++ b/packages/extension-base/src/utils/fee/transfer.ts @@ -6,6 +6,7 @@ import { AmountData } from '@subwallet/extension-base/background/KoniTypes'; import { _SUPPORT_TOKEN_PAY_FEE_GROUP, XCM_FEE_RATIO } from '@subwallet/extension-base/constants'; import { _isSnowBridgeXcm } from '@subwallet/extension-base/core/substrate/xcm-parser'; import { DEFAULT_CARDANO_TTL_OFFSET } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano/consts'; +import { createBitcoinTransaction } from '@subwallet/extension-base/services/balance-service/transfer/bitcoin-transfer'; import { createCardanoTransaction } from '@subwallet/extension-base/services/balance-service/transfer/cardano-transfer'; import { getERC20TransactionObject, getEVMTransactionObject } from '@subwallet/extension-base/services/balance-service/transfer/smart-contract'; import { createSubstrateExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/token'; @@ -25,7 +26,9 @@ import { EvmEIP1559FeeOption, FeeChainType, FeeDetail, FeeInfo, SubstrateTipInfo import { ResponseSubscribeTransfer } from '@subwallet/extension-base/types/balance/transfer'; import { BN_ZERO } from '@subwallet/extension-base/utils'; import { isCardanoAddress, isTonAddress } from '@subwallet/keyring'; +import { isBitcoinAddress } from '@subwallet/keyring/utils/address/validate'; import BigN from 'bignumber.js'; +import { bitcoin, testnet } from 'bitcoinjs-lib/src/networks'; import { TransactionConfig } from 'web3-core'; import { SubmittableExtrinsic } from '@polkadot/api/types'; @@ -170,7 +173,20 @@ export const calculateTransferMaxTransferable = async (id: string, request: Calc cardanoApi, nativeTokenInfo: nativeToken }); - } else { // TODO: Support maxTransferable for bitcoin + } else if (isBitcoinAddress(address) && _isTokenTransferredByBitcoin(srcToken)) { + const network = srcChain.isTestnet ? testnet : bitcoin; + + [transaction] = await createBitcoinTransaction({ + chain: srcChain.slug, + from: address, + to: address, + value, + feeInfo: fee, + transferAll: false, + bitcoinApi, + network: network + }); + } else { [transaction] = await createSubstrateExtrinsic({ transferAll: false, value, @@ -223,6 +239,15 @@ export const calculateTransferMaxTransferable = async (id: string, request: Calc ...fee, estimatedFee }; + } else if (feeChainType === 'bitcoin') { + // Calculate fee for bitcoin transaction + // TODO: Support maxTransferable for bitcoin + estimatedFee = '0'; + feeOptions = { + ...fee, + vSize: 0, + estimatedFee + }; } else { if (transaction) { if (isTonTransaction(transaction)) { @@ -263,6 +288,12 @@ export const calculateTransferMaxTransferable = async (id: string, request: Calc estimatedFee, gasLimit: '0' }; + } else if (fee.type === 'bitcoin') { + feeOptions = { + ...fee, + estimatedFee, + vSize: 0 + }; } else { feeOptions = { ...fee, @@ -429,6 +460,12 @@ export const calculateXcmMaxTransferable = async (id: string, request: Calculate ...fee, estimatedFee }; + } else if (feeChainType === 'bitcoin') { + feeOptions = { + ...fee, + estimatedFee, + vSize: 0 + }; } else { // Not implemented yet estimatedFee = '0'; @@ -446,6 +483,12 @@ export const calculateXcmMaxTransferable = async (id: string, request: Calculate estimatedFee, gasLimit: '0' }; + } else if (fee.type === 'bitcoin') { + feeOptions = { + ...fee, + estimatedFee, + vSize: 0 + }; } else { feeOptions = { ...fee, From 1f1cecf4a90b4d3f5219d8891f72e0709b2d04d9 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 6 May 2025 16:23:11 +0700 Subject: [PATCH 073/178] [Issue-4263] refactor: comment logic related to Ledger to pass auto build --- .../Confirmations/parts/Sign/Bitcoin.tsx | 138 +++++++++--------- 1 file changed, 68 insertions(+), 70 deletions(-) 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 424b5c97390..0441c8eb580 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,23 +1,21 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { BitcoinSignatureRequest, BitcoinSignPsbtRequest, ConfirmationDefinitionsBitcoin, ConfirmationResult, EvmSendTransactionRequest, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinSignatureRequest, BitcoinSignPsbtRequest, ConfirmationDefinitionsBitcoin, ConfirmationResult, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { RequestSubmitTransferWithId } from '@subwallet/extension-base/types/balance/transfer'; 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 { useNotification, useUnlockChecker } from '@subwallet/extension-koni-ui/hooks'; import { completeConfirmationBitcoin, makeBitcoinDappTransferConfirmation, makePSBTTransferAfterConfirmation } 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 { getSignMode, removeTransactionPersist } from '@subwallet/extension-koni-ui/utils'; import { Button, Icon, ModalContext } from '@subwallet/react-ui'; import CN from 'classnames'; import { CheckCircle, QrCode, Swatches, Wallet, XCircle } from 'phosphor-react'; -import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import { hexToU8a, u8aToU8a } from '@polkadot/util'; - import { ScanSignature } from '../Qr'; interface Props extends ThemeProps { @@ -54,40 +52,40 @@ const handleSignature = async (type: BitcoinSignatureSupportType, id: string, si const Component: React.FC = (props: Props) => { const { canSign, className, editedPayload, extrinsicType, id, payload, type } = props; - const { payload: { hashPayload } } = payload; + // const { payload: { hashPayload } } = payload; const { account } = (payload.payload as BitcoinSignatureRequest); // TODO: [Review] Error eslint - const chainId = (payload.payload as EvmSendTransactionRequest)?.chainId || 1; + // const chainId = (payload.payload as EvmSendTransactionRequest)?.chainId || 1; const { t } = useTranslation(); const notify = useNotification(); const { activeModal } = useContext(ModalContext); - const chain = useGetChainInfoByChainId(chainId); + // const chain = useGetChainInfoByChainId(chainId); const checkUnlock = useUnlockChecker(); const signMode = useMemo(() => getSignMode(account), [account]); // TODO: [Review] type generic_ledger or legacy_ledger - const isLedger = useMemo(() => signMode === AccountSignMode.GENERIC_LEDGER, [signMode]); - const isMessage = isBitcoinMessage(payload); + // const isLedger = useMemo(() => signMode === AccountSignMode.GENERIC_LEDGER, [signMode]); + // const isMessage = isBitcoinMessage(payload); const [loading, setLoading] = useState(false); - const { error: ledgerError, - isLoading: isLedgerLoading, - isLocked, - ledger, - refresh: refreshLedger, - signMessage: ledgerSignMessage, - signTransaction: ledgerSignTransaction, - warning: ledgerWarning } = useLedger(chain?.slug, isLedger); - - const isLedgerConnected = useMemo(() => !isLocked && !isLedgerLoading && !!ledger, [ - isLedgerLoading, - isLocked, - ledger - ]); + // const { error: ledgerError, + // isLoading: isLedgerLoading, + // isLocked, + // ledger, + // refresh: refreshLedger, + // signMessage: ledgerSignMessage, + // signTransaction: ledgerSignTransaction, + // warning: ledgerWarning } = useLedger(chain?.slug, isLedger); + + // const isLedgerConnected = useMemo(() => !isLocked && !isLedgerLoading && !!ledger, [ + // isLedgerLoading, + // isLocked, + // ledger + // ]); const approveIcon = useMemo((): PhosphorIcon => { switch (signMode) { @@ -165,33 +163,33 @@ const Component: React.FC = (props: Props) => { activeModal(CONFIRMATION_QR_MODAL); }, [activeModal]); - const onConfirmLedger = useCallback(() => { - if (!hashPayload) { - return; - } - - if (!isLedgerConnected || !ledger) { - refreshLedger(); - - return; - } - - setLoading(true); - - setTimeout(() => { - // TODO: Review metadata of ledgerSignTransaction - const signPromise = isMessage ? ledgerSignMessage(u8aToU8a(hashPayload), account?.accountIndex, account?.addressOffset) : ledgerSignTransaction(hexToU8a(hashPayload), new Uint8Array(0), account?.accountIndex, account?.addressOffset); - - signPromise - .then(({ signature }) => { - onApproveSignature({ signature }); - }) - .catch((e: Error) => { - console.log(e); - setLoading(false); - }); - }); - }, [account?.accountIndex, account?.addressOffset, hashPayload, isLedgerConnected, isMessage, ledger, ledgerSignMessage, ledgerSignTransaction, onApproveSignature, refreshLedger]); + // const onConfirmLedger = useCallback(() => { + // if (!hashPayload) { + // return; + // } + // + // if (!isLedgerConnected || !ledger) { + // refreshLedger(); + // + // return; + // } + // + // setLoading(true); + // + // setTimeout(() => { + // // TODO: Review metadata of ledgerSignTransaction + // const signPromise = isMessage ? ledgerSignMessage(u8aToU8a(hashPayload), account?.accountIndex, account?.addressOffset) : ledgerSignTransaction(hexToU8a(hashPayload), new Uint8Array(0), account?.accountIndex, account?.addressOffset); + // + // signPromise + // .then(({ signature }) => { + // onApproveSignature({ signature }); + // }) + // .catch((e: Error) => { + // console.log(e); + // setLoading(false); + // }); + // }); + // }, [account?.accountIndex, account?.addressOffset, hashPayload, isLedgerConnected, isMessage, ledger, ledgerSignMessage, ledgerSignTransaction, onApproveSignature, refreshLedger]); const onConfirmInject = useCallback(() => { console.error('Not implemented yet'); @@ -240,9 +238,9 @@ const Component: React.FC = (props: Props) => { case AccountSignMode.QR: onConfirmQr(); break; - case AccountSignMode.GENERIC_LEDGER: - onConfirmLedger(); - break; + // case AccountSignMode.GENERIC_LEDGER: + // onConfirmLedger(); + // break; case AccountSignMode.INJECTED: onConfirmInject(); break; @@ -253,21 +251,21 @@ const Component: React.FC = (props: Props) => { // Unlock is cancelled }); } - }, [checkUnlock, extrinsicType, onConfirmInject, onApprovePassword, onConfirmLedger, onConfirmQr, signMode]); - - useEffect(() => { - !!ledgerError && notify({ - message: ledgerError, - type: 'error' - }); - }, [ledgerError, notify]); - - useEffect(() => { - !!ledgerWarning && notify({ - message: ledgerWarning, - type: 'warning' - }); - }, [ledgerWarning, notify]); + }, [checkUnlock, extrinsicType, onConfirmInject, onApprovePassword, onConfirmQr, signMode]); + + // useEffect(() => { + // !!ledgerError && notify({ + // message: ledgerError, + // type: 'error' + // }); + // }, [ledgerError, notify]); + + // useEffect(() => { + // !!ledgerWarning && notify({ + // message: ledgerWarning, + // type: 'warning' + // }); + // }, [ledgerWarning, notify]); return (
From 1e3a0becc832d7972c4c2a82a9e18e439e661fd2 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 6 May 2025 16:44:53 +0700 Subject: [PATCH 074/178] [issue-4263] refactor: change import to pass autobuild --- packages/extension-base/package.json | 1 + .../extension-base/src/koni/background/handlers/Extension.ts | 4 ++-- .../request-service/handler/BitcoinRequestHandler.ts | 5 ++--- packages/extension-base/src/utils/fee/transfer.ts | 4 ++-- yarn.lock | 1 + 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json index 2348b2385fe..477e9192d2c 100644 --- a/packages/extension-base/package.json +++ b/packages/extension-base/package.json @@ -72,6 +72,7 @@ "avail-js-sdk": "^0.2.12", "axios": "^1.6.2", "bignumber.js": "^9.1.1", + "bitcoinjs-lib": "6.1.5", "bn.js": "^5.2.1", "bowser": "^2.11.0", "browser-passworder": "^2.0.3", diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index a041a7bc4fa..35da4b77975 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -76,7 +76,7 @@ import { ProposalTypes } from '@walletconnect/types/dist/types/sign-client/propo import { SessionTypes } from '@walletconnect/types/dist/types/sign-client/session'; import { getSdkError } from '@walletconnect/utils'; import BigN from 'bignumber.js'; -import { bitcoin, testnet } from 'bitcoinjs-lib/src/networks'; +import * as bitcoin from 'bitcoinjs-lib'; import { t } from 'i18next'; import { combineLatest, Subject } from 'rxjs'; import { TransactionConfig } from 'web3-core'; @@ -1443,7 +1443,7 @@ export default class KoniExtension { chainType = ChainType.BITCOIN; const chainInfo = this.#koniState.getChainInfo(chain); - const network = chainInfo.isTestnet ? testnet : bitcoin; + const network = chainInfo.isTestnet ? bitcoin.networks.testnet : bitcoin.networks.bitcoin; const txVal: string = transferAll ? transferTokenAvailable.value : (value || '0'); const bitcoinApi = this.#koniState.getBitcoinApi(chain); const feeInfo = await this.#koniState.feeService.subscribeChainFee(getId(), chain, 'bitcoin'); 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 ac7f86aff52..8f09e3d06fd 100644 --- a/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts +++ b/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts @@ -17,7 +17,7 @@ import { getId } from '@subwallet/extension-base/utils/getId'; import { isInternalRequest } from '@subwallet/extension-base/utils/request'; import keyring from '@subwallet/ui-keyring'; import { Psbt } from 'bitcoinjs-lib'; -import { bitcoin, testnet } from 'bitcoinjs-lib/src/networks'; +import * as bitcoin from 'bitcoinjs-lib'; import { t } from 'i18next'; import { BehaviorSubject } from 'rxjs'; @@ -228,8 +228,7 @@ export default class BitcoinRequestHandler { extrinsicHash: id }; - const network = chainInfo.isTestnet ? testnet : bitcoin; - + const network = chainInfo.isTestnet ? bitcoin.networks.testnet : bitcoin.networks.bitcoin; const feeInfo = await this.#feeService.subscribeChainFee(getId(), chain, 'bitcoin'); const [psbt] = await createBitcoinTransaction({ bitcoinApi, diff --git a/packages/extension-base/src/utils/fee/transfer.ts b/packages/extension-base/src/utils/fee/transfer.ts index 28c76ff0196..eb471af67d9 100644 --- a/packages/extension-base/src/utils/fee/transfer.ts +++ b/packages/extension-base/src/utils/fee/transfer.ts @@ -28,7 +28,7 @@ import { BN_ZERO } from '@subwallet/extension-base/utils'; import { isCardanoAddress, isTonAddress } from '@subwallet/keyring'; import { isBitcoinAddress } from '@subwallet/keyring/utils/address/validate'; import BigN from 'bignumber.js'; -import { bitcoin, testnet } from 'bitcoinjs-lib/src/networks'; +import * as bitcoin from 'bitcoinjs-lib'; import { TransactionConfig } from 'web3-core'; import { SubmittableExtrinsic } from '@polkadot/api/types'; @@ -174,7 +174,7 @@ export const calculateTransferMaxTransferable = async (id: string, request: Calc nativeTokenInfo: nativeToken }); } else if (isBitcoinAddress(address) && _isTokenTransferredByBitcoin(srcToken)) { - const network = srcChain.isTestnet ? testnet : bitcoin; + const network = srcChain.isTestnet ? bitcoin.networks.testnet : bitcoin.networks.bitcoin; [transaction] = await createBitcoinTransaction({ chain: srcChain.slug, diff --git a/yarn.lock b/yarn.lock index df7c304dae1..68012a346e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6806,6 +6806,7 @@ __metadata: avail-js-sdk: ^0.2.12 axios: ^1.6.2 bignumber.js: ^9.1.1 + bitcoinjs-lib: 6.1.5 bn.js: ^5.2.1 bowser: ^2.11.0 browser-passworder: ^2.0.3 From 435bae20452af924095723fd72fe8aaa4257b86a Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 7 May 2025 18:18:47 +0700 Subject: [PATCH 075/178] [issue-4263] refactor: fix route explorer link and correct get bitcoin api --- .../extension-base/src/services/transaction-service/index.ts | 3 ++- .../extension-base/src/services/transaction-service/utils.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 67dfa1ced71..3fc04b30523 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -1751,7 +1751,8 @@ export default class TransactionService { emitter.emit('signed', eventData); // Add start info emitter.emit('send', eventData); - const event = this.chainService.getBitcoinApi(chain).api.sendRawTransaction(payload); + const bitcoinApi = this.state.chainService.getBitcoinApi(chain); + const event = bitcoinApi.api.sendRawTransaction(payload); event.on('extrinsicHash', (txHash) => { eventData.extrinsicHash = txHash; diff --git a/packages/extension-base/src/services/transaction-service/utils.ts b/packages/extension-base/src/services/transaction-service/utils.ts index 4d3d6f87e05..5c171856ba2 100644 --- a/packages/extension-base/src/services/transaction-service/utils.ts +++ b/packages/extension-base/src/services/transaction-service/utils.ts @@ -3,7 +3,7 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { ExtrinsicDataTypeMap, ExtrinsicsDataResponse, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; -import { _getBlockExplorerFromChain, _isChainTestNet, _isPureCardanoChain, _isPureEvmChain } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getBlockExplorerFromChain, _isChainTestNet, _isPureBitcoinChain, _isPureCardanoChain, _isPureEvmChain } from '@subwallet/extension-base/services/chain-service/utils'; import { CHAIN_FLIP_MAINNET_EXPLORER, CHAIN_FLIP_TESTNET_EXPLORER, SIMPLE_SWAP_EXPLORER } from '@subwallet/extension-base/services/swap-service/utils'; import { ChainflipSwapTxData, SimpleSwapTxData } from '@subwallet/extension-base/types/swap'; import { SWApiResponse } from '@subwallet/subwallet-api-sdk/types'; @@ -55,7 +55,7 @@ function getBlockExplorerAccountRoute (explorerLink: string) { } function getBlockExplorerTxRoute (chainInfo: _ChainInfo) { - if (_isPureEvmChain(chainInfo)) { + if (_isPureEvmChain(chainInfo) || _isPureBitcoinChain(chainInfo)) { return 'tx'; } From bdc3bac0297042cb93376fc69153b7adb72ff710 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 8 May 2025 11:09:07 +0700 Subject: [PATCH 076/178] [issue-4263] refactor: fix related url provider --- .../handler/bitcoin/strategy/BlockStream/index.ts | 2 +- .../src/services/hiro-service/index.ts | 12 ++++++------ .../src/services/rune-service/index.ts | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) 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 431ff5be250..2ef3090c6f3 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 @@ -25,7 +25,7 @@ export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implement super(context); - this.baseUrl = 'https://btc-api.koni.studio'; + this.baseUrl = url; this.isTestnet = url.includes('testnet'); this.getBlockTime() diff --git a/packages/extension-base/src/services/hiro-service/index.ts b/packages/extension-base/src/services/hiro-service/index.ts index 590cb7506be..98dba5e4d23 100644 --- a/packages/extension-base/src/services/hiro-service/index.ts +++ b/packages/extension-base/src/services/hiro-service/index.ts @@ -9,8 +9,8 @@ import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-r import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; import { getRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; -const OPENBIT_URL = 'https://api.openbit.app'; -const OPENBIT_URL_TEST = 'https://api-testnet.openbit.app'; +const BITCOIN_API_URL = 'https://btc-api.koni.studio'; +const BITCOIN_API_URL_TEST = 'https://api-testnet.openbit.app'; export class HiroService extends BaseApiRequestStrategy { baseUrl: string; @@ -20,7 +20,7 @@ export class HiroService extends BaseApiRequestStrategy { super(context); - this.baseUrl = 'https://btc-api.koni.studio'; + this.baseUrl = url; } private headers = { @@ -90,7 +90,7 @@ export class HiroService extends BaseApiRequestStrategy { // todo: handle token authen for url preview getPreviewUrl (inscriptionId: string) { - return `${OPENBIT_URL}/inscriptions/${inscriptionId}/content`; + return `${BITCOIN_API_URL}/inscriptions/${inscriptionId}/content`; } // Singleton @@ -100,13 +100,13 @@ export class HiroService extends BaseApiRequestStrategy { public static getInstance (isTestnet = false) { if (isTestnet) { if (!HiroService.testnet) { - HiroService.testnet = new HiroService(OPENBIT_URL_TEST); + HiroService.testnet = new HiroService(BITCOIN_API_URL_TEST); } return HiroService.testnet; } else { if (!HiroService.mainnet) { - HiroService.mainnet = new HiroService(OPENBIT_URL); + HiroService.mainnet = new HiroService(BITCOIN_API_URL); } return HiroService.mainnet; diff --git a/packages/extension-base/src/services/rune-service/index.ts b/packages/extension-base/src/services/rune-service/index.ts index 3ae54e65d14..edecf56db83 100644 --- a/packages/extension-base/src/services/rune-service/index.ts +++ b/packages/extension-base/src/services/rune-service/index.ts @@ -9,8 +9,8 @@ import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-r import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; import { getRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; -const OPENBIT_URL = 'https://api.openbit.app'; -const OPENBIT_URL_TEST = 'https://api-testnet.openbit.app'; +const BITCOIN_API_URL = 'https://btc-api.koni.studio'; +const BITCOIN_API_URL_TEST = 'https://api-testnet.openbit.app'; export class RunesService extends BaseApiRequestStrategy { baseUrl: string; @@ -20,7 +20,7 @@ export class RunesService extends BaseApiRequestStrategy { super(context); - this.baseUrl = 'https://btc-api.koni.studio'; + this.baseUrl = url; } private headers = { @@ -111,13 +111,13 @@ export class RunesService extends BaseApiRequestStrategy { public static getInstance (isTestnet = false) { if (isTestnet) { if (!RunesService.testnet) { - RunesService.testnet = new RunesService(OPENBIT_URL_TEST); + RunesService.testnet = new RunesService(BITCOIN_API_URL_TEST); } return RunesService.testnet; } else { if (!RunesService.mainnet) { - RunesService.mainnet = new RunesService(OPENBIT_URL); + RunesService.mainnet = new RunesService(BITCOIN_API_URL); } return RunesService.mainnet; From f96bfc39b1ec1fdb530725d926d79f441102edf8 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 8 May 2025 18:13:28 +0700 Subject: [PATCH 077/178] [issue-4263] feat: handling logic related to a Bitcoin Testnet handler --- .../handler/bitcoin/BitcoinApi.ts | 22 +- .../bitcoin/strategy/BlockStream/types.ts | 303 -------------- .../strategy/BlockStreamTestnet/index.ts | 383 ++++++++++++++++++ .../index.ts | 5 +- .../handler/bitcoin/strategy/types.ts | 305 +++++++++++++- .../src/services/hiro-service/index.ts | 2 +- .../src/services/hiro-service/utils/index.ts | 2 +- .../src/services/rune-service/index.ts | 2 +- .../src/utils/bitcoin/common.ts | 18 +- 9 files changed, 726 insertions(+), 316 deletions(-) delete mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts rename packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/{BlockStream => SubWalletMainnet}/index.ts (94%) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts index d51625f8d8b..c5e367e6213 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts @@ -3,7 +3,8 @@ import '@polkadot/types-augment'; -import { BlockStreamRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream'; +import { BlockStreamTestnetRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet'; +import { SubWalletMainnetRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/SubWalletMainnet'; import { BitcoinApiStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { createPromiseHandler, PromiseHandler } from '@subwallet/extension-base/utils/promise'; import { BehaviorSubject } from 'rxjs'; @@ -32,7 +33,14 @@ export class BitcoinApi implements _BitcoinApi { this.apiUrl = apiUrl; this.providerName = providerName || 'unknown'; this.isReadyHandler = createPromiseHandler<_BitcoinApi>(); - this.api = new BlockStreamRequestStrategy(apiUrl); + + const isTestnet = apiUrl.includes('testnet'); + + if (isTestnet) { + this.api = new BlockStreamTestnetRequestStrategy(apiUrl); + } else { + this.api = new SubWalletMainnetRequestStrategy(apiUrl); + } this.connect(); } @@ -68,7 +76,15 @@ export class BitcoinApi implements _BitcoinApi { await this.disconnect(); this.apiUrl = apiUrl; - this.api = new BlockStreamRequestStrategy(apiUrl); + + const isTestnet = apiUrl.includes('testnet'); + + if (isTestnet) { + this.api = new BlockStreamTestnetRequestStrategy(apiUrl); + } else { + this.api = new SubWalletMainnetRequestStrategy(apiUrl); + } + this.connect(); } diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts deleted file mode 100644 index e907f5e69e4..00000000000 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/types.ts +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright 2019-2022 @subwallet/extension-base -// SPDX-License-Identifier: Apache-2.0 - -export interface BlockStreamBlock { - id: string; - height: number; - version: number; - timestamp: number; - tx_count: number; - size: number; - weight: number; - merkle_root: string; - previousblockhash: string; - mediantime: number; - nonce: number; - bits: number; - difficulty: number; -} - -export interface BitcoinAddressSummaryInfo { - address: string, - chain_stats: { - funded_txo_count: number, - funded_txo_sum: number, - spent_txo_count: number, - spent_txo_sum: number, - tx_count: number - }, - mempool_stats: { - funded_txo_count: number, - funded_txo_sum: number, - spent_txo_count: number, - spent_txo_sum: number, - tx_count: number - }, - balance: number, - total_inscription: number, - balance_rune: string, - balance_inscription: string, -} - -// todo: combine RunesByAddressResponse & RunesCollectionInfoResponse - -export interface RunesInfoByAddressResponse { - statusCode: number, - data: RunesInfoByAddressFetchedData -} - -export interface RunesInfoByAddressFetchedData { - limit: number, - offset: number, - total: number, - runes: RunesInfoByAddress[] -} - -// todo: check is_hot and turbo and cenotaph attributes meaning in RuneInfoByAddress - -export interface RunesInfoByAddress { - amount: string, - address: string, - rune_id: string, - rune: { - rune: string, - rune_name: string, - divisibility: number, - premine: string, - spacers: string, - symbol: string - } -} - -export interface RunesCollectionInfoResponse { - statusCode: number, - data: RunesCollectionInfoFetchedData -} - -interface RunesCollectionInfoFetchedData { - limit: number, - offset: number, - total: number, - runes: RunesCollectionInfo[] -} - -export interface RunesCollectionInfo { - rune_id: string, - rune: string, - rune_name: string, - divisibility: string, - spacers: string -} - -export interface RuneTxsResponse { - statusCode: number, - data: RuneTxsFetchedData -} - -interface RuneTxsFetchedData { - limit: number, - offset: number, - total: number, - transactions: RuneTxs[] -} - -export interface RuneTxs { - txid: string, - vout: RuneTxsUtxosVout[] -} - -interface RuneTxsUtxosVout { - n: number, - value: number, - runeInject: any -} - -export interface Brc20MetadataFetchedData { - token: Brc20Metadata -} - -export interface Brc20Metadata { - ticker: string, - decimals: number -} - -export interface Brc20BalanceFetchedData { - limit: number, - offset: number, - total: number, - results: Brc20Balance[] -} - -export interface Brc20Balance { - ticker: string, - available_balance: string, - transferrable_balance: string, - overall_balance: string -} - -export interface Brc20BalanceItem { - free: string, - locked: string -} - -export interface InscriptionFetchedData { - limit: number, - offset: number, - total: number, - results: Inscription[] -} - -export interface Inscription { - id: string; - number: number; - address: string; - genesis_block_height: number; - genesis_block_hash: string; - genesis_timestamp: number; - tx_id: string; - location: string; - output: string; - value: string; - offset: string; - fee: number; - sat_ordinal: string; - sat_rarity: string; - content_type: string; - content_length: number; - // content: any -} - -export interface UpdateOpenBitUtxo { - totalUtxo: number, - utxoItems: BlockStreamUtxo[] -} - -export interface BlockStreamUtxo { - txid: string; - vout: number; - status: { - confirmed: boolean; - block_height?: number; - block_hash: string; - block_time?: number; - }, - value: number; -} - -export interface BlockStreamTransactionStatus { - confirmed: boolean; - block_height: number; - block_hash: string; - block_time: number; -} - -export interface BlockStreamFeeEstimates { - 1: number; - 2: number; - 3: number; - 4: number; - 5: number; - 6: number; - 7: number; - 8: number; -} - -export interface RecommendedFeeEstimates { - fastestFee: number, - halfHourFee: number, - hourFee: number, - economyFee: number, - minimumFee: number -} - -export interface BlockStreamTransactionVectorOutput { - scriptpubkey: string; - scriptpubkey_asm: string; - scriptpubkey_type: string; - scriptpubkey_address: string; - value: number; -} - -export interface BlockStreamTransactionVectorInput { - is_coinbase: boolean; - prevout: BlockStreamTransactionVectorOutput; - scriptsig: string; - scriptsig_asm: string; - sequence: number; - txid: string; - vout: number; - witness: string[]; -} - -export interface BlockStreamTransactionDetail { - txid: string; - version: number; - locktime: number; - totalVin: number; - totalVout: number; - size: number; - weight: number; - fee: number; - status: { - confirmed: boolean; - block_height?: number; - block_hash?: string; - block_time?: number; - } - vin: BlockStreamTransactionVectorInput[]; - vout: BlockStreamTransactionVectorOutput[]; -} - -export interface RuneUtxoResponse { - total: number, - results: RuneUtxo[] -} - -export interface RuneUtxo { - height: number, - confirmations: number, - address: string, - satoshi: number, - scriptPk: string, - txid: string, - vout: number, - runes: RuneInject[] -} - -interface RuneInject { - rune: string, - runeid: string, - spacedRune: string, - amount: string, - symbol: string, - divisibility: number -} - -export interface RuneMetadata { - id: string, - mintable: boolean, - parent: string, - entry: RuneInfo -} - -interface RuneInfo { - block: number, - burned: string, - divisibility: number, - etching: string, - mints: string, - number: number, - premine: string, - spaced_rune: string, - symbol: string, - terms: RuneTerms - timestamp: string, - turbo: boolean -} - -interface RuneTerms { - amount: string, - cap: string, - height: string[], - offset: string[] -} diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts new file mode 100644 index 00000000000..4bf4369a98a --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts @@ -0,0 +1,383 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { SWError } from '@subwallet/extension-base/background/errors/SWError'; +import { BitcoinAddressSummaryInfo, BitcoinApiStrategy, BitcoinTransactionEventMap, BlockstreamAddressResponse, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, BlockStreamUtxo, Inscription, InscriptionFetchedData, RunesInfoByAddress, RunesInfoByAddressFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; +import { HiroService } from '@subwallet/extension-base/services/hiro-service'; +import { RunesService } from '@subwallet/extension-base/services/rune-service'; +import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; +import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; +import { getRequest, postRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; +import { BitcoinFeeInfo, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; +import BigN from 'bignumber.js'; +import EventEmitter from 'eventemitter3'; + +export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy implements BitcoinApiStrategy { + private readonly baseUrl: string; + private readonly isTestnet: boolean; + private timePerBlock = 0; // in milliseconds + + constructor (url: string) { + const context = new BaseApiRequestContext(); + + super(context); + + this.baseUrl = url; + this.isTestnet = url.includes('testnet'); + + this.getBlockTime() + .then((rs) => { + this.timePerBlock = rs; + }) + .catch(() => { + this.timePerBlock = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; + }); + } + + private headers = { + 'Content-Type': 'application/json' + }; + + isRateLimited (): boolean { + return false; + } + + getUrl (path: string): string { + return `${this.baseUrl}/${path}`; + } + + getBlockTime (): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl('blocks'), undefined, this.headers); + const blocks = await response.json() as BlockStreamBlock[]; + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getBlockTime', 'Failed to fetch blocks'); + } + + const length = blocks.length; + const sortedBlocks = blocks.sort((a, b) => b.timestamp - a.timestamp); + const time = (sortedBlocks[0].timestamp - sortedBlocks[length - 1].timestamp) * 1000; + + return time / length; + }, 0); + } + + getAddressSummaryInfo (address: string): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`address/${address}`), undefined, this.headers); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getAddressSummaryInfo', 'Failed to fetch address info'); + } + + const rsRaw = await response.json() as BlockstreamAddressResponse; + const rs: BitcoinAddressSummaryInfo = { + address: rsRaw.address, + chain_stats: { + funded_txo_count: rsRaw.chain_stats.funded_txo_count, + funded_txo_sum: rsRaw.chain_stats.funded_txo_sum, + spent_txo_count: rsRaw.chain_stats.spent_txo_count, + spent_txo_sum: rsRaw.chain_stats.spent_txo_sum, + tx_count: rsRaw.chain_stats.tx_count + }, + mempool_stats: { + funded_txo_count: rsRaw.mempool_stats.funded_txo_count, + funded_txo_sum: rsRaw.mempool_stats.funded_txo_sum, + spent_txo_count: rsRaw.mempool_stats.spent_txo_count, + spent_txo_sum: rsRaw.mempool_stats.spent_txo_sum, + tx_count: rsRaw.mempool_stats.tx_count + }, + balance: rsRaw.chain_stats.funded_txo_sum - rsRaw.chain_stats.spent_txo_sum, + total_inscription: 0, + balance_rune: '0', + balance_inscription: '0' + }; + + return rs; + }, 0); + } + + getAddressTransaction (address: string, limit = 100): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`address/${address}/txs`), { limit: `${limit}` }, this.headers); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getAddressTransaction', 'Failed to fetch transactions'); + } + + return await response.json() as BitcoinTx[]; + }, 1); + } + + getTransactionStatus (txHash: string): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`tx/${txHash}/status`), undefined, {}); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.getTransactionStatus', `Failed to fetch transaction status: ${errorText}`); + } + + // Blockstream API trả về object thô + const data = await response.json() as BlockStreamTransactionStatus; + + return { + confirmed: data.confirmed || false, + block_time: data.block_time || 0, + block_height: data.block_height, + block_hash: data.block_hash + }; + }, 1); + } + + getTransactionDetail (txHash: string): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`tx/${txHash}`), undefined, this.headers); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getTransactionDetail', 'Failed to fetch transaction detail'); + } + + return await response.json() as BlockStreamTransactionDetail; + }, 1); + } + + getFeeRate (): Promise { + return this.addRequest(async (): Promise => { + const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); + const estimates = await response.json() as BlockStreamFeeEstimates; + + console.log('getRecommendedFeeRate: rs', estimates); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getFeeRate', 'Failed to fetch fee estimates'); + } + + const low = 6; + const average = 3; + const fast = 1; + + const convertFee = (fee: number) => parseFloat(new BigN(fee).toFixed(2)); + + return { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: convertFee(estimates[low] || 10), time: this.timePerBlock * low }, + average: { feeRate: convertFee(estimates[average || 12]), time: this.timePerBlock * average }, + fast: { feeRate: convertFee(estimates[fast] || 15), time: this.timePerBlock * fast }, + default: 'slow' + } + }; + }, 0); + } + + getRecommendedFeeRate (): Promise { + return this.addRequest(async (): Promise => { + const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getRecommendedFeeRate', `Failed to fetch fee estimates: ${response.statusText}`); + } + + const convertTimeMilisec = { + fastestFee: 10 * 60000, + halfHourFee: 30 * 60000, + hourFee: 60 * 60000 + }; + + const estimates = await response.json() as BlockStreamFeeEstimates; + + const low = 6; + const average = 4; + const fast = 2; + + console.log('getRecommendedFeeRate: estimates', estimates); + + const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed(), 10); + + return { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: convertFee(estimates[low] || 10), time: convertTimeMilisec.hourFee }, + average: { feeRate: convertFee(estimates[average] || 12), time: convertTimeMilisec.halfHourFee }, + fast: { feeRate: convertFee(estimates[fast] || 15), time: convertTimeMilisec.fastestFee }, + default: 'slow' + } + }; + }, 0); + } + + getUtxos (address: string): Promise { + return this.addRequest(async (): Promise => { + const response = await getRequest(this.getUrl(`address/${address}/utxo`), undefined, {}); + const rs = await response.json() as BlockStreamUtxo[]; + + console.log('getUtxos: rs', rs); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.getUtxos', `Failed to fetch UTXOs: ${errorText}`); + } + + return rs.map((item: BlockStreamUtxo) => ({ + txid: item.txid, + vout: item.vout, + value: item.value, + status: item.status + })); + }, 0); + } + + sendRawTransaction (rawTransaction: string) { + const eventEmitter = new EventEmitter(); + + this.addRequest(async (): Promise => { + const response = await postRequest( + this.getUrl('tx'), + rawTransaction, + { 'Content-Type': 'text/plain' }, + false + ); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.sendRawTransaction', `Failed to broadcast transaction: ${errorText}`); + } + + return await response.text(); + }, 0) + .then((extrinsicHash) => { + eventEmitter.emit('extrinsicHash', extrinsicHash); + + // Check transaction status + const interval = setInterval(() => { + this.getTransactionStatus(extrinsicHash) + .then((transactionStatus) => { + if (transactionStatus.confirmed && transactionStatus.block_time > 0) { + clearInterval(interval); + eventEmitter.emit('success', transactionStatus); + } + }) + .catch(console.error); + }, 30000); + }) + .catch((error: Error) => { + eventEmitter.emit('error', error.message); + }) + ; + + return eventEmitter; + } + + simpleSendRawTransaction (rawTransaction: string) { + return this.addRequest(async (): Promise => { + const response = await postRequest(this.getUrl('tx'), rawTransaction, { 'Content-Type': 'text/plain' }, false); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.simpleSendRawTransaction', `Failed to broadcast transaction: ${errorText}`); + } + + return await response.text(); + }, 0); + } + + async getRunes (address: string) { + const runesFullList: RunesInfoByAddress[] = []; + const pageSize = 60; + let offset = 0; + + const runeService = RunesService.getInstance(this.isTestnet); + + try { + while (true) { + const response = await runeService.getAddressRunesInfo(address, { + limit: String(pageSize), + offset: String(offset) + }) as unknown as RunesInfoByAddressFetchedData; + + const runes = response.runes; + + if (runes.length !== 0) { + runesFullList.push(...runes); + offset += pageSize; + } else { + break; + } + } + + return runesFullList; + } catch (error) { + console.error(`Failed to get ${address} balances`, error); + throw error; + } + } + + async getRuneUtxos (address: string) { + const runeService = RunesService.getInstance(this.isTestnet); + + try { + const responseRuneUtxos = await runeService.getAddressRuneUtxos(address); + + return responseRuneUtxos.results; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + throw new SWError('BlockStreamTestnetRequestStrategy.getRuneUtxos', `Failed to get ${address} rune utxos: ${errorMessage}`); + } + } + + async getAddressInscriptions (address: string) { + const inscriptionsFullList: Inscription[] = []; + const pageSize = 60; + let offset = 0; + + const hiroService = HiroService.getInstance(this.isTestnet); + + try { + while (true) { + const response = await hiroService.getAddressInscriptionsInfo({ + limit: String(pageSize), + offset: String(offset), + address: String(address) + }) as unknown as InscriptionFetchedData; + + const inscriptions = response.results; + + if (inscriptions.length !== 0) { + inscriptionsFullList.push(...inscriptions); + offset += pageSize; + } else { + break; + } + } + + return inscriptionsFullList; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + throw new SWError('BlockStreamTestnetRequestStrategy.getAddressInscriptions', `Failed to get ${address} inscriptions: ${errorMessage}`); + } + } + + getTxHex (txHash: string): Promise { + return this.addRequest(async (): Promise => { + const response = await getRequest(this.getUrl(`tx/${txHash}/hex`), undefined, this.headers); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.getTxHex', `Failed to fetch transaction hex: ${errorText}`); + } + + return await response.text(); + }, 0); + } +} 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/SubWalletMainnet/index.ts similarity index 94% rename from packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStream/index.ts rename to packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/SubWalletMainnet/index.ts index 2ef3090c6f3..4ba2d08c96d 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/SubWalletMainnet/index.ts @@ -3,8 +3,7 @@ import { SWError } from '@subwallet/extension-base/background/errors/SWError'; import { _BTC_SERVICE_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; -import { BitcoinAddressSummaryInfo, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; -import { BitcoinApiStrategy, BitcoinTransactionEventMap } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; +import { BitcoinAddressSummaryInfo, BitcoinApiStrategy, BitcoinTransactionEventMap, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData, UpdateOpenBitUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { OBResponse } from '@subwallet/extension-base/services/chain-service/types'; import { HiroService } from '@subwallet/extension-base/services/hiro-service'; import { RunesService } from '@subwallet/extension-base/services/rune-service'; @@ -15,7 +14,7 @@ import { BitcoinFeeInfo, BitcoinTx, UtxoResponseItem } from '@subwallet/extensio import BigN from 'bignumber.js'; import EventEmitter from 'eventemitter3'; -export class BlockStreamRequestStrategy extends BaseApiRequestStrategy implements BitcoinApiStrategy { +export class SubWalletMainnetRequestStrategy extends BaseApiRequestStrategy implements BitcoinApiStrategy { private readonly baseUrl: string; private readonly isTestnet: boolean; private timePerBlock = 0; // in milliseconds 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 3743d75d2ea..d229cb098e6 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 @@ -1,7 +1,6 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { BitcoinAddressSummaryInfo, Inscription, RunesInfoByAddress, RuneUtxo } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { ApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy/types'; import { BitcoinFeeInfo, BitcoinTransactionStatus, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; import EventEmitter from 'eventemitter3'; @@ -30,3 +29,307 @@ export interface BitcoinTransactionEventMap { error: (error: string) => void; success: (data: BitcoinTransactionStatus) => void; } + +export interface BlockStreamBlock { + id: string; + height: number; + version: number; + timestamp: number; + tx_count: number; + size: number; + weight: number; + merkle_root: string; + previousblockhash: string; + mediantime: number; + nonce: number; + bits: number; + difficulty: number; +} + +export interface BlockstreamAddressResponse { + address: string; + chain_stats: { + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + }; + mempool_stats: { + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + }; +} + +export interface BitcoinAddressSummaryInfo extends BlockstreamAddressResponse{ + balance: number, + total_inscription: number, + balance_rune: string, + balance_inscription: string, +} + +// todo: combine RunesByAddressResponse & RunesCollectionInfoResponse + +export interface RunesInfoByAddressResponse { + statusCode: number, + data: RunesInfoByAddressFetchedData +} + +export interface RunesInfoByAddressFetchedData { + limit: number, + offset: number, + total: number, + runes: RunesInfoByAddress[] +} + +// todo: check is_hot and turbo and cenotaph attributes meaning in RuneInfoByAddress + +export interface RunesInfoByAddress { + amount: string, + address: string, + rune_id: string, + rune: { + rune: string, + rune_name: string, + divisibility: number, + premine: string, + spacers: string, + symbol: string + } +} + +export interface RunesCollectionInfoResponse { + statusCode: number, + data: RunesCollectionInfoFetchedData +} + +interface RunesCollectionInfoFetchedData { + limit: number, + offset: number, + total: number, + runes: RunesCollectionInfo[] +} + +export interface RunesCollectionInfo { + rune_id: string, + rune: string, + rune_name: string, + divisibility: string, + spacers: string +} + +export interface RuneTxsResponse { + statusCode: number, + data: RuneTxsFetchedData +} + +interface RuneTxsFetchedData { + limit: number, + offset: number, + total: number, + transactions: RuneTxs[] +} + +export interface RuneTxs { + txid: string, + vout: RuneTxsUtxosVout[] +} + +interface RuneTxsUtxosVout { + n: number, + value: number, + runeInject: any +} + +export interface Brc20MetadataFetchedData { + token: Brc20Metadata +} + +export interface Brc20Metadata { + ticker: string, + decimals: number +} + +export interface Brc20BalanceFetchedData { + limit: number, + offset: number, + total: number, + results: Brc20Balance[] +} + +export interface Brc20Balance { + ticker: string, + available_balance: string, + transferrable_balance: string, + overall_balance: string +} + +export interface Brc20BalanceItem { + free: string, + locked: string +} + +export interface InscriptionFetchedData { + limit: number, + offset: number, + total: number, + results: Inscription[] +} + +export interface Inscription { + id: string; + number: number; + address: string; + genesis_block_height: number; + genesis_block_hash: string; + genesis_timestamp: number; + tx_id: string; + location: string; + output: string; + value: string; + offset: string; + fee: number; + sat_ordinal: string; + sat_rarity: string; + content_type: string; + content_length: number; + // content: any +} + +export interface UpdateOpenBitUtxo { + totalUtxo: number, + utxoItems: BlockStreamUtxo[] +} + +export interface BlockStreamUtxo { + txid: string; + vout: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash: string; + block_time?: number; + }, + value: number; +} + +export interface BlockStreamTransactionStatus { + confirmed: boolean; + block_height: number; + block_hash: string; + block_time: number; +} + +export interface BlockStreamFeeEstimates { + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; +} + +export interface RecommendedFeeEstimates { + fastestFee: number, + halfHourFee: number, + hourFee: number, + economyFee: number, + minimumFee: number +} + +export interface BlockStreamTransactionVectorOutput { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; +} + +export interface BlockStreamTransactionVectorInput { + is_coinbase: boolean; + prevout: BlockStreamTransactionVectorOutput; + scriptsig: string; + scriptsig_asm: string; + sequence: number; + txid: string; + vout: number; + witness: string[]; +} + +export interface BlockStreamTransactionDetail { + txid: string; + version: number; + locktime: number; + totalVin: number; + totalVout: number; + size: number; + weight: number; + fee: number; + status: { + confirmed: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; + } + vin: BlockStreamTransactionVectorInput[]; + vout: BlockStreamTransactionVectorOutput[]; +} + +export interface RuneUtxoResponse { + total: number, + results: RuneUtxo[] +} + +export interface RuneUtxo { + height: number, + confirmations: number, + address: string, + satoshi: number, + scriptPk: string, + txid: string, + vout: number, + runes: RuneInject[] +} + +interface RuneInject { + rune: string, + runeid: string, + spacedRune: string, + amount: string, + symbol: string, + divisibility: number +} + +export interface RuneMetadata { + id: string, + mintable: boolean, + parent: string, + entry: RuneInfo +} + +interface RuneInfo { + block: number, + burned: string, + divisibility: number, + etching: string, + mints: string, + number: number, + premine: string, + spaced_rune: string, + symbol: string, + terms: RuneTerms + timestamp: string, + turbo: boolean +} + +interface RuneTerms { + amount: string, + cap: string, + height: string[], + offset: string[] +} diff --git a/packages/extension-base/src/services/hiro-service/index.ts b/packages/extension-base/src/services/hiro-service/index.ts index 98dba5e4d23..f125245066d 100644 --- a/packages/extension-base/src/services/hiro-service/index.ts +++ b/packages/extension-base/src/services/hiro-service/index.ts @@ -3,7 +3,7 @@ import { SWError } from '@subwallet/extension-base/background/errors/SWError'; import { _BTC_SERVICE_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; -import { Brc20BalanceFetchedData, Brc20MetadataFetchedData, InscriptionFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { Brc20BalanceFetchedData, Brc20MetadataFetchedData, InscriptionFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { OBResponse } from '@subwallet/extension-base/services/chain-service/types'; import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; diff --git a/packages/extension-base/src/services/hiro-service/utils/index.ts b/packages/extension-base/src/services/hiro-service/utils/index.ts index ec24aebb45d..082218c411a 100644 --- a/packages/extension-base/src/services/hiro-service/utils/index.ts +++ b/packages/extension-base/src/services/hiro-service/utils/index.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { Brc20Metadata, InscriptionFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { Brc20Metadata, InscriptionFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { HiroService } from '@subwallet/extension-base/services/hiro-service'; // todo: handle inscription testnet diff --git a/packages/extension-base/src/services/rune-service/index.ts b/packages/extension-base/src/services/rune-service/index.ts index edecf56db83..d932dc31713 100644 --- a/packages/extension-base/src/services/rune-service/index.ts +++ b/packages/extension-base/src/services/rune-service/index.ts @@ -3,7 +3,7 @@ import { SWError } from '@subwallet/extension-base/background/errors/SWError'; import { _BTC_SERVICE_TOKEN } from '@subwallet/extension-base/services/chain-service/constants'; -import { RuneMetadata, RunesCollectionInfoResponse, RunesInfoByAddressFetchedData, RuneTxsResponse, RuneUtxoResponse } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; +import { RuneMetadata, RunesCollectionInfoResponse, RunesInfoByAddressFetchedData, RuneTxsResponse, RuneUtxoResponse } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { OBResponse, OBRuneResponse } from '@subwallet/extension-base/services/chain-service/types'; import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; diff --git a/packages/extension-base/src/utils/bitcoin/common.ts b/packages/extension-base/src/utils/bitcoin/common.ts index 93152380d71..76bc96ec6ab 100644 --- a/packages/extension-base/src/utils/bitcoin/common.ts +++ b/packages/extension-base/src/utils/bitcoin/common.ts @@ -69,9 +69,21 @@ export function getSpendableAmount ({ feeRate, export const getTransferableBitcoinUtxos = async (bitcoinApi: _BitcoinApi, address: string) => { try { const [utxos, runeTxsUtxos, inscriptionUtxos] = await Promise.all([ - await bitcoinApi.api.getUtxos(address), - await getRuneUtxos(bitcoinApi, address), - await getInscriptionUtxos(bitcoinApi, address) + bitcoinApi.api.getUtxos(address).catch((error) => { + console.log('Error fetching UTXOs:', error); + + return []; + }), + getRuneUtxos(bitcoinApi, address).catch((error) => { + console.log('Error fetching Rune UTXOs:', error); + + return []; + }), + getInscriptionUtxos(bitcoinApi, address).catch((error) => { + console.log('Error fetching Inscription UTXOs:', error); + + return []; + }) ]); let filteredUtxos: UtxoResponseItem[]; From 9cac448f1bad2ab4c788f2cd4aac133fa92f2c9c Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 8 May 2025 19:33:47 +0700 Subject: [PATCH 078/178] [issue-4263] chore: review this function --- .../src/services/transaction-service/helpers/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/extension-base/src/services/transaction-service/helpers/index.ts b/packages/extension-base/src/services/transaction-service/helpers/index.ts index 9b156e2d710..5e2e2a3bc20 100644 --- a/packages/extension-base/src/services/transaction-service/helpers/index.ts +++ b/packages/extension-base/src/services/transaction-service/helpers/index.ts @@ -37,6 +37,7 @@ export const isCardanoTransaction = (tx: SWTransaction['transaction']): tx is Ca return cardanoTransactionConfig.cardanoPayload !== null && cardanoTransactionConfig.cardanoPayload !== undefined; }; +// TODO: [Review] this function export const isBitcoinTransaction = (tx: SWTransaction['transaction']): tx is Psbt => { return 'data' in tx && Array.isArray((tx as Psbt).data.inputs); }; From 53c3a56099e243f83e0256448855f2fd54e8636d Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 13 May 2025 15:36:23 +0700 Subject: [PATCH 079/178] [issue-4263] feat: Add blocktime to history --- packages/extension-base/src/background/KoniTypes.ts | 1 + packages/extension-base/src/core/utils.ts | 2 +- .../src/services/transaction-service/index.ts | 7 ++++--- .../src/Popup/Home/History/Detail/parts/Layout.tsx | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index a0676578c53..0d4f49f7e7b 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -717,6 +717,7 @@ export interface TransactionHistoryItem { - 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; diff --git a/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx b/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx index 29214c7dc78..e66444113fa 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx @@ -57,6 +57,7 @@ const Component: React.FC = (props: Props) => { /> {extrinsicHash} {formatHistoryDate(data.time, language, 'detail')} + {data.blockTime && ({formatHistoryDate(data.blockTime, language, 'detail')})} { From fcd9a19d8fe251acc9b896dd064ab13d485511f3 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 13 May 2025 17:04:39 +0700 Subject: [PATCH 080/178] [issue-4263] chore: resolve conflicts after merge --- .../src/koni/background/handlers/Extension.ts | 4 ++-- .../extension-base/src/koni/background/handlers/State.ts | 2 +- .../services/balance-service/helpers/subscribe/bitcoin.ts | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 0cb133600ae..f624cf97c56 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -7,7 +7,7 @@ import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _MultiChainAsset } from import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { createSubscription } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, HistoryTokenPriceJSON, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetHistoryTokenPriceData, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMigratePassword, RequestMigrateSoloAccount, RequestMigrateUnifiedAndFetchEligibleSoloAccounts, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestPingSession, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveMigrationAcknowledgedStatus, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSaveUnifiedAccountMigrationInProgress, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseMigrateSoloAccount, ResponseMigrateUnifiedAndFetchEligibleSoloAccounts, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeCurrentTokenPrice, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, ThemeNames, TokenPriorityDetails, TransactionHistoryItem, TransactionResponse, UiSettings, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, HistoryTokenPriceJSON, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetHistoryTokenPriceData, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMigratePassword, RequestMigrateSoloAccount, RequestMigrateUnifiedAndFetchEligibleSoloAccounts, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestPingSession, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveMigrationAcknowledgedStatus, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSaveUnifiedAccountMigrationInProgress, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseMigrateSoloAccount, ResponseMigrateUnifiedAndFetchEligibleSoloAccounts, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeCurrentTokenPrice, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, ThemeNames, TokenPriorityDetails, TransactionHistoryItem, TransactionResponse, UiSettings, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountExport, RequestAuthorizeCancel, RequestAuthorizeReject, RequestCurrentAccountAddress, RequestMetadataApprove, RequestMetadataReject, RequestSigningApproveSignature, RequestSigningCancel, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseType, SigningRequest, WindowOpenParams } from '@subwallet/extension-base/background/types'; import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning'; import { _SUPPORT_TOKEN_PAY_FEE_GROUP, ALL_ACCOUNT_KEY, LATEST_SESSION } from '@subwallet/extension-base/constants'; @@ -43,7 +43,7 @@ import { _isPosChainBridge, getClaimPosBridge } from '@subwallet/extension-base/ import { DryRunInfo } from '@subwallet/extension-base/services/balance-service/transfer/xcm/utils'; import { _DEFAULT_MANTA_ZK_CHAIN, _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX } from '@subwallet/extension-base/services/chain-service/constants'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _NetworkUpsertParams, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse, EnableChainParams, EnableMultiChainParams } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getEvmChainId, _isAssetSmartContractNft, _isChainEvmCompatible, _isChainSubstrateCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isNativeTokenBySlug, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByCardano, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getEvmChainId, _isAssetSmartContractNft, _isChainEvmCompatible, _isChainSubstrateCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isNativeTokenBySlug, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByBitcoin, _isTokenTransferredByCardano, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; import { TokenHasBalanceInfo, TokenPayFeeInfo } from '@subwallet/extension-base/services/fee-service/interfaces'; import { calculateToAmountByReservePool } from '@subwallet/extension-base/services/fee-service/utils'; import { batchExtrinsicSetFeeHydration, getAssetHubTokensCanPayFee, getHydrationTokensCanPayFee } from '@subwallet/extension-base/services/fee-service/utils/tokenPayFee'; diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index 413d3648d9b..78486d1724d 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -8,7 +8,7 @@ import { EvmProviderError } from '@subwallet/extension-base/background/errors/Ev import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { isSubscriptionRunning, unsubscribe } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AddressCardanoTransactionBalance, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, CardanoKeyType, CardanoProviderErrorType, CardanoSignatureRequest, CardanoTransactionDappConfig, ChainStakingMetadata, ChainType, ConfirmationsQueue, ConfirmationsQueueCardano, ConfirmationsQueueTon, ConfirmationType, CrowdloanItem, CrowdloanJson, CurrencyType, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestCardanoSignData, RequestCardanoSignTransaction, RequestConfirmationComplete, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCardanoSignData, ResponseCardanoSignTransaction, ServiceInfo, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; +import { AddressCardanoTransactionBalance, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, CardanoKeyType, CardanoProviderErrorType, CardanoSignatureRequest, CardanoTransactionDappConfig, ChainStakingMetadata, ChainType, ConfirmationsQueue, ConfirmationsQueueBitcoin, ConfirmationsQueueCardano, ConfirmationsQueueTon, ConfirmationType, CrowdloanItem, CrowdloanJson, CurrencyType, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestCardanoSignData, RequestCardanoSignTransaction, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCardanoSignData, ResponseCardanoSignTransaction, ServiceInfo, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; import { RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning } from '@subwallet/extension-base/background/types'; import { BACKEND_API_URL, BACKEND_PRICE_HISTORY_URL, EnvConfig, MANTA_PAY_BALANCE_INTERVAL, REMIND_EXPORT_ACCOUNT } from '@subwallet/extension-base/constants'; import { convertErrorFormat, generateValidationProcess, PayloadValidated, ValidateStepFunction, validationAuthMiddleware, validationAuthWCMiddleware, validationCardanoSignDataMiddleware, validationConnectMiddleware, validationEvmDataTransactionMiddleware, validationEvmSignMessageMiddleware } from '@subwallet/extension-base/core/logic-validation'; diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index 8cd68ef2cdc..ea53cbabf6e 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -2,12 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import { _AssetType } from '@subwallet/chain-list/types'; -import {AddressBalanceResult, APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; +import { AddressBalanceResult, APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { BITCOIN_REFRESH_BALANCE_INTERVAL } from '@subwallet/extension-base/constants'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; -import { BalanceItem, SusbcribeBitcoinPalletBalance, UtxoResponseItem } from '@subwallet/extension-base/types'; -import { filterAssetsByChainAndType, filteredOutTxsUtxos, getInscriptionUtxos, getRuneUtxos } from '@subwallet/extension-base/utils'; -import BigN from 'bignumber.js'; +import { BalanceItem, SusbcribeBitcoinPalletBalance } from '@subwallet/extension-base/types'; +import { filterAssetsByChainAndType } from '@subwallet/extension-base/utils'; function getDefaultBalanceResult (): AddressBalanceResult { return { From a90a2626775a6d2c79098df761b10e8ace189922 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 20 May 2025 19:08:40 +0700 Subject: [PATCH 081/178] [issue-4316] Extension - Improve UI after Bitcoin integration --- .../extension-base/src/types/balance/index.ts | 1 + .../src/Popup/Account/AccountDetail/index.tsx | 63 ++++++-------- .../src/Popup/Home/Tokens/DetailModal.tsx | 7 +- .../AccountProxy/AccountProxySelectorItem.tsx | 4 + .../AccountProxy/AddressSelectorItem.tsx | 86 +++++++++++++++---- .../src/components/MetaInfo/MetaInfo.tsx | 12 +++ .../src/components/MetaInfo/parts/types.ts | 2 +- .../Modal/Global/AccountTokenAddressModal.tsx | 5 +- .../Modal/Global/AddressQrModal.tsx | 73 +++++++++++++--- .../TokenItem/AccountTokenBalanceItem.tsx | 18 ++-- .../src/utils/account/account.ts | 22 ++--- 11 files changed, 206 insertions(+), 87 deletions(-) diff --git a/packages/extension-base/src/types/balance/index.ts b/packages/extension-base/src/types/balance/index.ts index 161cb1bed89..67c531383c4 100644 --- a/packages/extension-base/src/types/balance/index.ts +++ b/packages/extension-base/src/types/balance/index.ts @@ -40,6 +40,7 @@ export interface BalanceItem { export interface BalanceItemWithAddressType extends BalanceItem { addressTypeLabel?: string + schema?: string } /** Balance info of all tokens on an address */ diff --git a/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx b/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx index 783e1562415..8965e0321f6 100644 --- a/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx @@ -1,29 +1,29 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { NotificationType } from '@subwallet/extension-base/background/KoniTypes'; -import { AccountActions, AccountProxy, AccountProxyType } from '@subwallet/extension-base/types'; -import { AccountProxyTypeTag, CloseIcon, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; -import { FilterTabItemType, FilterTabs } from '@subwallet/extension-koni-ui/components/FilterTabs'; -import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import { useDefaultNavigate, useGetAccountProxyById, useNotification } from '@subwallet/extension-koni-ui/hooks'; -import { editAccount, forgetAccount, validateAccountName } from '@subwallet/extension-koni-ui/messaging'; -import { RootState } from '@subwallet/extension-koni-ui/stores'; -import { AccountDetailParam, ThemeProps, VoidFunction } from '@subwallet/extension-koni-ui/types'; -import { FormCallbacks, FormFieldData } from '@subwallet/extension-koni-ui/types/form'; -import { convertFieldToObject } from '@subwallet/extension-koni-ui/utils/form/form'; -import { Button, Form, Icon, Input } from '@subwallet/react-ui'; +import {NotificationType} from '@subwallet/extension-base/background/KoniTypes'; +import {AccountActions, AccountProxy, AccountProxyType} from '@subwallet/extension-base/types'; +import { AccountChainTypeLogos, AccountProxyTypeTag, CloseIcon, Layout, PageWrapper} from '@subwallet/extension-koni-ui/components'; +import {FilterTabItemType, FilterTabs} from '@subwallet/extension-koni-ui/components/FilterTabs'; +import {WalletModalContext} from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; +import {useDefaultNavigate, useGetAccountProxyById, useNotification} from '@subwallet/extension-koni-ui/hooks'; +import {editAccount, forgetAccount, validateAccountName} from '@subwallet/extension-koni-ui/messaging'; +import {RootState} from '@subwallet/extension-koni-ui/stores'; +import {AccountDetailParam, ThemeProps, VoidFunction} from '@subwallet/extension-koni-ui/types'; +import {FormCallbacks, FormFieldData} from '@subwallet/extension-koni-ui/types/form'; +import {convertFieldToObject} from '@subwallet/extension-koni-ui/utils/form/form'; +import {Button, Form, Icon, Input} from '@subwallet/react-ui'; import CN from 'classnames'; -import { CircleNotch, Export, FloppyDiskBack, GitMerge, Trash } from 'phosphor-react'; -import { RuleObject } from 'rc-field-form/lib/interface'; -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; -import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import {Export, GitMerge, Trash} from 'phosphor-react'; +import {RuleObject} from 'rc-field-form/lib/interface'; +import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import {useSelector} from 'react-redux'; +import {useLocation, useNavigate, useParams} from 'react-router-dom'; import styled from 'styled-components'; -import { AccountAddressList } from './AccountAddressList'; -import { DerivedAccountList } from './DerivedAccountList'; +import {AccountAddressList} from './AccountAddressList'; +import {DerivedAccountList} from './DerivedAccountList'; enum FilterTabType { ACCOUNT_ADDRESS = 'account-address', @@ -95,7 +95,6 @@ const Component: React.FC = ({ accountProxy, onBack, requestView const [deleting, setDeleting] = useState(false); // @ts-ignore const [deriving, setDeriving] = useState(false); - const [saving, setSaving] = useState(false); const filterTabItems = useMemo(() => { const result = [ @@ -211,7 +210,6 @@ const Component: React.FC = ({ accountProxy, onBack, requestView if (changeMap[FormFieldName.NAME]) { clearTimeout(saveTimeOutRef.current); - setSaving(true); const isValidForm = form.getFieldsError().every((field) => !field.errors.length); @@ -219,8 +217,6 @@ const Component: React.FC = ({ accountProxy, onBack, requestView saveTimeOutRef.current = setTimeout(() => { form.submit(); }, 1000); - } else { - setSaving(false); } } }, [form]); @@ -230,25 +226,18 @@ const Component: React.FC = ({ accountProxy, onBack, requestView const name = values[FormFieldName.NAME]; if (name === accountProxy.name) { - setSaving(false); - return; } const accountProxyId = accountProxy.id; if (!accountProxyId) { - setSaving(false); - return; } editAccount(accountProxyId, name.trim()) .catch((error: Error) => { form.setFields([{ name: FormFieldName.NAME, errors: [error.message] }]); - }) - .finally(() => { - setSaving(false); }); }, [accountProxy.id, accountProxy.name, form]); @@ -427,10 +416,9 @@ const Component: React.FC = ({ accountProxy, onBack, requestView onBlur={form.submit} placeholder={t('Account name')} suffix={( - )} /> @@ -526,6 +514,11 @@ const AccountDetail = styled(Wrapper)(({ theme: { token } }: Props) => { gap: token.sizeSM }, + '.__item-chain-type-logos': { + minHeight: 20, + marginRight: 12 + }, + '.account-detail-form, .derivation-info-form': { paddingTop: token.padding, paddingLeft: token.padding, diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx index 3645f09b221..14ea3905cf4 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx @@ -10,7 +10,7 @@ import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTransla import { RootState } from '@subwallet/extension-koni-ui/stores'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; import { TokenBalanceItemType } from '@subwallet/extension-koni-ui/types/balance'; -import { getBitcoinLabelByKeypair, isAccountAll } from '@subwallet/extension-koni-ui/utils'; +import { getBitcoinKeypairAttributes, isAccountAll } from '@subwallet/extension-koni-ui/utils'; import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; import { Form, Icon, ModalContext, Number, SwModal } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; @@ -161,7 +161,10 @@ function Component ({ className = '', currentTokenInfo, id, onCancel, tokenBalan if (isBitcoinAddress(item.address)) { const keyPairType = getKeypairTypeByAddress(item.address); - resultItem.addressTypeLabel = getBitcoinLabelByKeypair(keyPairType); + const attributes = getBitcoinKeypairAttributes(keyPairType); + + resultItem.addressTypeLabel = attributes.label; + resultItem.schema = attributes.schema; } result.push(resultItem); diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx index 49b9e350146..df59e97c7f6 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx @@ -161,6 +161,10 @@ function Component (props: Props): React.ReactElement {
{accountProxy.suri || ''}
+
) : ( diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx index cbfc9f6125c..7caf635ede4 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { toShort } from '@subwallet/extension-koni-ui/utils'; +import { getBitcoinKeypairAttributes, toShort } from '@subwallet/extension-koni-ui/utils'; +import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; import { Icon } from '@subwallet/react-ui'; import CN from 'classnames'; import { CheckCircle } from 'phosphor-react'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import AccountProxyAvatar from './AccountProxyAvatar'; @@ -25,9 +26,23 @@ function Component (props: Props): React.ReactElement { avatarValue, className, isSelected, name, onClick, showUnselectIcon } = props; + const bitcoinAttributes = useMemo(() => { + if (isBitcoinAddress(address)) { + const keyPairType = getKeypairTypeByAddress(address); + + const attributes = getBitcoinKeypairAttributes(keyPairType); + + return attributes; + } + + return undefined; + }, [address]); + + console.log('bitcoinAttributes', bitcoinAttributes); + return (
@@ -39,16 +54,28 @@ function Component (props: Props): React.ReactElement {
- { - !!name && ( -
- {name} -
- ) - } +
+ { + !!name && ( +
+ {name} +
+ ) + } + {!!bitcoinAttributes && !!bitcoinAttributes.schema + ? ( + <> +
  -  
+
+ {bitcoinAttributes.label} +
+ + ) + : null} +
- {name ? `(${toShort(address, 4, 5)})` : toShort(address, 9, 10)} + {toShort(address, 9, 10)}
@@ -87,19 +114,24 @@ const AddressSelectorItem = styled(Component)(({ theme: { token } }: Prop minHeight: 52, '.__avatar': { - marginRight: token.marginSM + marginRight: token.marginXS }, '.__item-center-part': { display: 'flex', + flexDirection: 'column', overflowX: 'hidden', 'white-space': 'nowrap', - gap: token.sizeXXS, flex: 1, fontSize: token.fontSize, lineHeight: token.lineHeight }, + '.__item-name-wrapper': { + display: 'flex', + alignItems: 'center' + }, + '.__item-right-part': { display: 'flex' }, @@ -119,16 +151,40 @@ const AddressSelectorItem = styled(Component)(({ theme: { token } }: Prop '.__name': { color: token.colorTextLight1, overflow: 'hidden', - textOverflow: 'ellipsis' + textOverflow: 'ellipsis', + fontWeight: token.fontWeightStrong }, '.__address': { - color: token.colorTextLight4 + color: token.colorTextLight4, + fontSize: token.fontSizeSM, + fontWeight: token.bodyFontWeight, + lineHeight: token.lineHeightSM }, '&:hover': { background: token.colorBgInput + }, + + '.__label, .__name-label-divider': { + fontSize: token.fontSizeXS, + lineHeight: token.lineHeightXS, + fontWeight: 700, + '&.orange-7': { + color: token['orange-7'] + }, + '&.lime-7': { + color: token['lime-7'] + }, + '&.cyan-7': { + color: token['cyan-7'] + } + }, + + '.__name-label-divider': { + color: token.colorTextTertiary } + }; }); diff --git a/packages/extension-koni-ui/src/components/MetaInfo/MetaInfo.tsx b/packages/extension-koni-ui/src/components/MetaInfo/MetaInfo.tsx index 47fc251d309..b3038303d03 100644 --- a/packages/extension-koni-ui/src/components/MetaInfo/MetaInfo.tsx +++ b/packages/extension-koni-ui/src/components/MetaInfo/MetaInfo.tsx @@ -189,6 +189,18 @@ const _MetaInfo = styled(Component)(({ theme: { token } }: Props) => { color: token.blue }, + '.__value.-schema-cyan-7': { + color: token['cyan-7'] + }, + + '.__value.-schema-lime-7': { + color: token['lime-7'] + }, + + '.__value.-schema-orange-7': { + color: token['orange-7'] + }, + '.__value.-schema-even-odd': { color: token.colorTextLight2, diff --git a/packages/extension-koni-ui/src/components/MetaInfo/parts/types.ts b/packages/extension-koni-ui/src/components/MetaInfo/parts/types.ts index 720742f8ac1..42f16416eff 100644 --- a/packages/extension-koni-ui/src/components/MetaInfo/parts/types.ts +++ b/packages/extension-koni-ui/src/components/MetaInfo/parts/types.ts @@ -6,5 +6,5 @@ import React from 'react'; export interface InfoItemBase extends ThemeProps { label?: React.ReactNode, - valueColorSchema?: 'default' | 'light' | 'gray' | 'success' | 'gold' | 'danger' | 'warning' | 'magenta' | 'green' | 'blue' + valueColorSchema?: 'default' | 'light' | 'gray' | 'success' | 'gold' | 'danger' | 'warning' | 'magenta' | 'green' | 'blue' | 'orange-7' | 'lime-7' | 'cyan-7' } diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx index 66d49273425..85a3d84d3ae 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx @@ -35,6 +35,7 @@ const Component: React.FC = ({ className, items, onBack, onCancel }: Prop return () => { const processFunction = () => { addressQrModal.open({ + accountTokenAddresses: items, address: item.accountInfo.address, chainSlug: item.chainSlug, onBack: addressQrModal.close, @@ -47,7 +48,7 @@ const Component: React.FC = ({ className, items, onBack, onCancel }: Prop processFunction(); }; - }, [addressQrModal, onCancel]); + }, [addressQrModal, items, onCancel]); const onCopyAddress = useCallback((item: AccountTokenAddress) => { return () => { @@ -104,7 +105,7 @@ const Component: React.FC = ({ className, items, onBack, onCancel }: Prop onClick: onCancel } : undefined} - title={t('Select address')} + title={t('Select address type')} > } diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx index e1e8cd2c2e1..1ef2e78d61e 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx @@ -10,17 +10,19 @@ import { ADDRESS_QR_MODAL, TON_WALLET_CONTRACT_SELECTOR_MODAL } from '@subwallet import { useDefaultNavigate, useFetchChainInfo, useGetAccountByAddress } from '@subwallet/extension-koni-ui/hooks'; import useNotification from '@subwallet/extension-koni-ui/hooks/common/useNotification'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; -import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { AccountTokenAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { toShort } from '@subwallet/extension-koni-ui/utils'; import { Button, Icon, Logo, ModalContext, SwModal, SwQRCode, Tag } from '@subwallet/react-ui'; import CN from 'classnames'; -import { ArrowSquareOut, CaretLeft, CopySimple, Gear, House } from 'phosphor-react'; -import React, { useCallback, useContext, useMemo } from 'react'; +import {ArrowSquareOut, CaretLeft, CaretRight, CopySimple, Gear, House} from 'phosphor-react'; +import React, {useCallback, useContext, useMemo, useState} from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; import styled from 'styled-components'; +import {_isChainBitcoinCompatible} from "@subwallet/extension-base/services/chain-service/utils"; export interface AddressQrModalProps { address: string; + accountTokenAddresses?: AccountTokenAddress[]; chainSlug: string; onBack?: VoidFunction; onCancel?: VoidFunction; @@ -34,18 +36,42 @@ type Props = ThemeProps & AddressQrModalProps & { const modalId = ADDRESS_QR_MODAL; const tonWalletContractSelectorModalId = TON_WALLET_CONTRACT_SELECTOR_MODAL; -const Component: React.FC = ({ address, chainSlug, className, isNewFormat, onBack, onCancel }: Props) => { +const Component: React.FC = ({ address: initialAddress, chainSlug, accountTokenAddresses, className, isNewFormat, onBack, onCancel }: Props) => { const { t } = useTranslation(); const { activeModal, checkActive, inactiveModal } = useContext(ModalContext); const notify = useNotification(); const chainInfo = useFetchChainInfo(chainSlug); - const accountInfo = useGetAccountByAddress(address); + const accountInfo = useGetAccountByAddress(initialAddress); const isTonWalletContactSelectorModalActive = checkActive(tonWalletContractSelectorModalId); const goHome = useDefaultNavigate().goHome; + const showNavigationButtons = useMemo(() => { + return !!chainInfo && _isChainBitcoinCompatible(chainInfo) && accountTokenAddresses && accountTokenAddresses.length > 1; + }, [chainInfo, accountTokenAddresses]); + + const [currentIndex, setCurrentIndex] = useState(() => { + if (!showNavigationButtons) return 0; + const index = accountTokenAddresses.findIndex(item => item.accountInfo.address === initialAddress); + return index !== -1 ? index : 0; + }); + + const currentAddress = showNavigationButtons ? accountTokenAddresses[currentIndex]?.accountInfo.address || initialAddress : initialAddress; + const scanExplorerAddressUrl = useMemo(() => { - return getExplorerLink(chainInfo, address, 'account'); - }, [address, chainInfo]); + return getExplorerLink(chainInfo, currentAddress, 'account'); + }, [currentAddress, chainInfo]); + + // Hàm xử lý nút Previous + const handlePrevious = useCallback(() => { + setCurrentIndex(prev => Math.max(0, prev - 1)); + }, []); + + // Hàm xử lý nút Next + const handleNext = useCallback(() => { + if (accountTokenAddresses) { + setCurrentIndex(prev => Math.min(accountTokenAddresses.length - 1, prev + 1)); + } + }, [accountTokenAddresses]); const onGoHome = useCallback(() => { goHome(); @@ -130,14 +156,35 @@ const Component: React.FC = ({ address, chainSlug, className, isNewFormat > <>
+ {showNavigationButtons && ( +
@@ -150,7 +197,7 @@ const Component: React.FC = ({ address, chainSlug, className, isNewFormat />
- {toShort(address || '', 7, 7)} + {toShort(currentAddress || '', 7, 7)}
{isNewFormat !== undefined &&
@@ -163,7 +210,7 @@ const Component: React.FC = ({ address, chainSlug, className, isNewFormat
} - +
+ {!!bitcoinAttributes && !!bitcoinAttributes.label + ? ( +
+
{bitcoinAttributes.label} BTC
+  address +
+ ) + : null}
(({ theme: { token } }: Props) => display: 'flex', alignItems: 'center' }, + + '.__label-address-wrapper': { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + '.__label-address-prefix': { + fontWeight: 700, + fontSize: token.fontSizeSM, + lineHeight: token.lineHeightSM, + color: token.colorTextLight2 + }, + + '.__label-address-suffix': { + fontWeight: 500, + fontSize: token.fontSizeSM, + lineHeight: token.lineHeightSM, + color: token.colorTextTertiary + } + }, + '.ant-sw-sub-header-title': { fontSize: token.fontSizeXL, lineHeight: token.lineHeightHeading4, @@ -328,7 +376,9 @@ const AddressQrModal = styled(Component)(({ theme: { token } }: Props) => overflow: 'hidden', 'white-space': 'nowrap', color: token.colorTextLight4, - flexShrink: 1 + flexShrink: 1, + fontSize: token.fontSizeSM, + lineHeight: token.lineHeightSM }, '.__change-version-icon': { From d7dbaa97ca32cb15c9bd3d915a3acbc2dd84ab8c Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 21 May 2025 10:27:01 +0700 Subject: [PATCH 083/178] [Issue-4316] refactor: eslint --- .../src/Popup/Account/AccountDetail/index.tsx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx b/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx index 8965e0321f6..69f5ee247ac 100644 --- a/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx @@ -1,29 +1,29 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import {NotificationType} from '@subwallet/extension-base/background/KoniTypes'; -import {AccountActions, AccountProxy, AccountProxyType} from '@subwallet/extension-base/types'; -import { AccountChainTypeLogos, AccountProxyTypeTag, CloseIcon, Layout, PageWrapper} from '@subwallet/extension-koni-ui/components'; -import {FilterTabItemType, FilterTabs} from '@subwallet/extension-koni-ui/components/FilterTabs'; -import {WalletModalContext} from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import {useDefaultNavigate, useGetAccountProxyById, useNotification} from '@subwallet/extension-koni-ui/hooks'; -import {editAccount, forgetAccount, validateAccountName} from '@subwallet/extension-koni-ui/messaging'; -import {RootState} from '@subwallet/extension-koni-ui/stores'; -import {AccountDetailParam, ThemeProps, VoidFunction} from '@subwallet/extension-koni-ui/types'; -import {FormCallbacks, FormFieldData} from '@subwallet/extension-koni-ui/types/form'; -import {convertFieldToObject} from '@subwallet/extension-koni-ui/utils/form/form'; -import {Button, Form, Icon, Input} from '@subwallet/react-ui'; +import { NotificationType } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountActions, AccountProxy, AccountProxyType } from '@subwallet/extension-base/types'; +import { AccountChainTypeLogos, AccountProxyTypeTag, CloseIcon, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; +import { FilterTabItemType, FilterTabs } from '@subwallet/extension-koni-ui/components/FilterTabs'; +import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; +import { useDefaultNavigate, useGetAccountProxyById, useNotification } from '@subwallet/extension-koni-ui/hooks'; +import { editAccount, forgetAccount, validateAccountName } from '@subwallet/extension-koni-ui/messaging'; +import { RootState } from '@subwallet/extension-koni-ui/stores'; +import { AccountDetailParam, ThemeProps, VoidFunction } from '@subwallet/extension-koni-ui/types'; +import { FormCallbacks, FormFieldData } from '@subwallet/extension-koni-ui/types/form'; +import { convertFieldToObject } from '@subwallet/extension-koni-ui/utils/form/form'; +import { Button, Form, Icon, Input } from '@subwallet/react-ui'; import CN from 'classnames'; -import {Export, GitMerge, Trash} from 'phosphor-react'; -import {RuleObject} from 'rc-field-form/lib/interface'; -import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; -import {useTranslation} from 'react-i18next'; -import {useSelector} from 'react-redux'; -import {useLocation, useNavigate, useParams} from 'react-router-dom'; +import { Export, GitMerge, Trash } from 'phosphor-react'; +import { RuleObject } from 'rc-field-form/lib/interface'; +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import styled from 'styled-components'; -import {AccountAddressList} from './AccountAddressList'; -import {DerivedAccountList} from './DerivedAccountList'; +import { AccountAddressList } from './AccountAddressList'; +import { DerivedAccountList } from './DerivedAccountList'; enum FilterTabType { ACCOUNT_ADDRESS = 'account-address', From 0b7a42ad7cdcd7d664ac9479eb449c65de128d48 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 22 May 2025 11:40:04 +0700 Subject: [PATCH 084/178] [Issue-4316] refactor: Related UI/UX for Bitcoin --- packages/extension-base/src/types/balance/index.ts | 6 ------ .../src/Popup/Home/Tokens/DetailModal.tsx | 3 +-- .../components/AccountProxy/AddressSelectorItem.tsx | 10 +++++----- .../Modal/Global/AccountTokenAddressModal.tsx | 1 + .../src/components/Modal/Global/AddressQrModal.tsx | 5 ++--- .../components/TokenItem/AccountTokenBalanceItem.tsx | 3 +-- packages/extension-koni-ui/src/types/balance.ts | 6 ++++++ 7 files changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/extension-base/src/types/balance/index.ts b/packages/extension-base/src/types/balance/index.ts index 67c531383c4..d5c204584f7 100644 --- a/packages/extension-base/src/types/balance/index.ts +++ b/packages/extension-base/src/types/balance/index.ts @@ -37,12 +37,6 @@ export interface BalanceItem { // substrate fields metadata?: _BalanceMetadata; } - -export interface BalanceItemWithAddressType extends BalanceItem { - addressTypeLabel?: string - schema?: string -} - /** Balance info of all tokens on an address */ export type BalanceInfo = Record; // Key is tokenSlug /** Balance info of all addresses */ diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx index 14ea3905cf4..2463b4c18eb 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx @@ -3,12 +3,11 @@ import { APIItemState } from '@subwallet/extension-base/background/KoniTypes'; import { _isChainBitcoinCompatible } from '@subwallet/extension-base/services/chain-service/utils'; -import { BalanceItemWithAddressType } from '@subwallet/extension-base/types'; import { AccountTokenBalanceItem, EmptyList, RadioGroup } from '@subwallet/extension-koni-ui/components'; import { useSelector } from '@subwallet/extension-koni-ui/hooks'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; import { RootState } from '@subwallet/extension-koni-ui/stores'; -import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { BalanceItemWithAddressType, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { TokenBalanceItemType } from '@subwallet/extension-koni-ui/types/balance'; import { getBitcoinKeypairAttributes, isAccountAll } from '@subwallet/extension-koni-ui/utils'; import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx index 1756f9437f9..4f0c7696f39 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx @@ -38,7 +38,7 @@ function Component (props: Props): React.ReactElement { return (
@@ -62,7 +62,7 @@ function Component (props: Props): React.ReactElement { ? ( <>
  -  
-
+
{bitcoinAttributes.label}
@@ -166,13 +166,13 @@ const AddressSelectorItem = styled(Component)(({ theme: { token } }: Prop fontSize: token.fontSizeXS, lineHeight: token.lineHeightXS, fontWeight: 700, - '&.orange-7': { + '&.-schema-orange-7': { color: token['orange-7'] }, - '&.lime-7': { + '&.-schema-lime-7': { color: token['lime-7'] }, - '&.cyan-7': { + '&.-schema-cyan-7': { color: token['cyan-7'] } }, diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx index 85a3d84d3ae..8f81becec54 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx @@ -31,6 +31,7 @@ const Component: React.FC = ({ className, items, onBack, onCancel }: Prop const notify = useNotification(); const { addressQrModal } = useContext(WalletModalContext); + // Note: This component only supports Bitcoin addresses. Please review it if you want to use it for other use cases. const onShowQr = useCallback((item: AccountTokenAddress) => { return () => { const processFunction = () => { diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx index 0eb3b93d42e..29c5557d69f 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx @@ -3,7 +3,6 @@ import type { ButtonProps } from '@subwallet/react-ui/es/button/button'; -import { _isChainBitcoinCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { getExplorerLink } from '@subwallet/extension-base/services/transaction-service/utils'; import { AccountActions } from '@subwallet/extension-base/types'; import { CloseIcon, TonWalletContractSelectorModal } from '@subwallet/extension-koni-ui/components'; @@ -47,8 +46,8 @@ const Component: React.FC = ({ accountTokenAddresses = [], address: initi const goHome = useDefaultNavigate().goHome; const showNavigationButtons = useMemo(() => { - return !!chainInfo && _isChainBitcoinCompatible(chainInfo) && accountTokenAddresses.length > 1; - }, [chainInfo, accountTokenAddresses]); + return accountTokenAddresses.length > 1; + }, [accountTokenAddresses]); const [currentIndex, setCurrentIndex] = useState(() => { if (!showNavigationButtons) { diff --git a/packages/extension-koni-ui/src/components/TokenItem/AccountTokenBalanceItem.tsx b/packages/extension-koni-ui/src/components/TokenItem/AccountTokenBalanceItem.tsx index ffd727551c7..dfef12dfe69 100644 --- a/packages/extension-koni-ui/src/components/TokenItem/AccountTokenBalanceItem.tsx +++ b/packages/extension-koni-ui/src/components/TokenItem/AccountTokenBalanceItem.tsx @@ -5,11 +5,10 @@ import { _ChainAsset } from '@subwallet/chain-list/types'; import { _BalanceMetadata, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { _isChainBitcoinCompatible, _isChainTonCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { getExplorerLink } from '@subwallet/extension-base/services/transaction-service/utils'; -import { BalanceItemWithAddressType } from '@subwallet/extension-base/types'; import { AccountProxyAvatar, InfoItemBase } from '@subwallet/extension-koni-ui/components'; import { useGetAccountByAddress, useGetChainPrefixBySlug, useSelector, useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { RootState } from '@subwallet/extension-koni-ui/stores'; -import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { BalanceItemWithAddressType, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { reformatAddress, toShort } from '@subwallet/extension-koni-ui/utils'; import { Button, Icon } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; diff --git a/packages/extension-koni-ui/src/types/balance.ts b/packages/extension-koni-ui/src/types/balance.ts index 9586e05b021..7a559a8ad79 100644 --- a/packages/extension-koni-ui/src/types/balance.ts +++ b/packages/extension-koni-ui/src/types/balance.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { CurrencyJson } from '@subwallet/extension-base/background/KoniTypes'; +import { BalanceItem } from '@subwallet/extension-base/types'; import BigN from 'bignumber.js'; export type BalanceValueInfo = { @@ -29,3 +30,8 @@ export interface TokenBalanceItemType { isReady: boolean; symbol: string } + +export interface BalanceItemWithAddressType extends BalanceItem { + addressTypeLabel?: string + schema?: string +} From 98a9280530441614b7bdf708f2f434459eaad6cb Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 22 May 2025 17:04:31 +0700 Subject: [PATCH 085/178] [Issue-4263] update: content on transaction history --- .../src/Popup/Home/History/Detail/parts/Layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx b/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx index e66444113fa..72feb50a115 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx @@ -56,8 +56,8 @@ const Component: React.FC = (props: Props) => { valueColorSchema={HistoryStatusMap[data.status].schema} /> {extrinsicHash} - {formatHistoryDate(data.time, language, 'detail')} - {data.blockTime && ({formatHistoryDate(data.blockTime, language, 'detail')})} + {formatHistoryDate(data.time, language, 'detail')} + {data.blockTime && ({formatHistoryDate(data.blockTime, language, 'detail')})} { From f6f81652e0f57f34c7ea4370ad793d5b01f11297 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 22 May 2025 17:08:55 +0700 Subject: [PATCH 086/178] [Issue-4263] fix: Reset transferInfo when changing assetValue --- .../src/Popup/Transaction/variants/SendFund.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 48e6824d3b0..e1fd747de89 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -130,7 +130,7 @@ const hiddenFields: Array = ['chain', 'fromAccountProxy', const alertModalId = 'confirmation-alert-modal'; const defaultAddressInputRenderKey = 'address-input-render-key'; -const FEE_SHOW_TYPES: Array = ['substrate', 'evm']; +const FEE_SHOW_TYPES: Array = ['substrate', 'evm', 'bitcoin']; const Component = ({ className = '', isAllAccount, targetAccountProxy }: ComponentProps): React.ReactElement => { useSetCurrentPage('/transaction/send-fund'); @@ -432,6 +432,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone setForceUpdateMaxValue(undefined); setSelectedTransactionFee(undefined); setCurrentTokenPayFee(values.chain === chain ? defaultTokenPayFee : undefined); + setTransferInfo(undefined); } if (part.destChain || part.chain || part.value || part.asset) { From 63555224970e511526d0bad1e19996db5caa80b0 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 23 May 2025 12:41:49 +0700 Subject: [PATCH 087/178] [Issue-4263] Prevent dust limit for bitcoin transaction --- .../src/utils/bitcoin/utxo-management.ts | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index f36513d2df8..289920dcf23 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -67,6 +67,7 @@ export function determineUtxosForSpendAll ({ feeRate, if (!validateBitcoinAddress(recipient)) { throw new Error('Cannot calculate spend of invalid address type'); } + // TODO: Prevent dust limit when transferring all const recipients = [recipient]; @@ -107,6 +108,15 @@ export function determineUtxosForSpend ({ amount, throw new Error('Cannot calculate spend of invalid address type'); } + const recipientAddressInfo = getBitcoinAddressInfo(recipient); + const recipientDustLimit = BTC_DUST_AMOUNT[recipientAddressInfo.type] || 546; + + if (amount < recipientDustLimit) { + throw new Error( + `Transfer amount ${amount} satoshis is below dust limit (${recipientDustLimit} satoshis for ${recipientAddressInfo.type})` + ); + } + const orderedUtxos = utxos.sort((a, b) => b.value - a.value); const recipients = [recipient, sender]; const filteredUtxos = filterUneconomicalUtxos({ @@ -156,13 +166,33 @@ export function determineUtxosForSpend ({ amount, throw new InsufficientFundsError(); } + const senderAddressInfo = getBitcoinAddressInfo(sender); + const dustLimit = BTC_DUST_AMOUNT[senderAddressInfo.type] || 546; + const outputs = [ // outputs[0] = the desired amount going to recipient - { value: amount, address: recipient }, - // outputs[1] = the remainder to be returned to a change address - { value: amountLeft.toNumber(), address: sender } + { value: amount, address: recipient } ]; + if (amountLeft.gte(dustLimit)) { + // outputs[1] = the remainder to be returned to a change address + outputs.push({ value: amountLeft.toNumber(), address: sender }); + } else { + console.warn( + `Change output of ${amountLeft.toString()} satoshis is below dust limit (${dustLimit} satoshis for ${senderAddressInfo.type}). Omitting change output and adding to fee.` + ); + // Increase the fee to use the remaining balance + const newFee = sum.minus(amount).toNumber(); + + return { + filteredUtxos, + inputs: neededUtxos, + outputs, + size: sizeInfo.txVBytes, + fee: newFee + }; + } + return { filteredUtxos, inputs: neededUtxos, From 0827341fb0e4830ac303fa7c782007aba4731761 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 23 May 2025 14:37:47 +0700 Subject: [PATCH 088/178] [Issue-4263] rollback logic and improve later --- .../src/utils/bitcoin/utxo-management.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index 289920dcf23..59772978f25 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -178,19 +178,25 @@ export function determineUtxosForSpend ({ amount, // outputs[1] = the remainder to be returned to a change address outputs.push({ value: amountLeft.toNumber(), address: sender }); } else { - console.warn( - `Change output of ${amountLeft.toString()} satoshis is below dust limit (${dustLimit} satoshis for ${senderAddressInfo.type}). Omitting change output and adding to fee.` + // Todo: This solution for improve later, current throw error + + // console.warn( + // `Change output of ${amountLeft.toString()} satoshis is below dust limit (${dustLimit} satoshis for ${senderAddressInfo.type}). Omitting change output and adding to fee.` + // ); + // // Increase the fee to use the remaining balance + // const newFee = sum.minus(amount).toNumber(); + // + // return { + // filteredUtxos, + // inputs: neededUtxos, + // outputs, + // size: sizeInfo.txVBytes, + // fee: newFee + // }; + + throw new Error( + `The change output (${amountLeft.toString()} satoshis) is below the dust limit (${dustLimit} satoshis for ${senderAddressInfo.type})` ); - // Increase the fee to use the remaining balance - const newFee = sum.minus(amount).toNumber(); - - return { - filteredUtxos, - inputs: neededUtxos, - outputs, - size: sizeInfo.txVBytes, - fee: newFee - }; } return { From c16fb7f3b47dcf434b35fcc32bcca5fa9d37cf3b Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 26 May 2025 15:37:56 +0700 Subject: [PATCH 089/178] [Issue-4263] Hide the "Max" button and update the UI for the "Estimated Fee" fields --- .../src/Popup/Transaction/variants/SendFund.tsx | 4 ++-- .../src/components/Field/TransactionFee/FeeEditor/index.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 e1fd747de89..2b28bf4918d 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -12,7 +12,7 @@ import { _isAcrossChainBridge } from '@subwallet/extension-base/services/balance import { isAvailChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/availBridge'; import { _isPolygonChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/polygonBridge'; import { _isPosChainBridge, _isPosChainL2Bridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/posBridge'; -import { _getAssetDecimals, _getAssetName, _getAssetOriginChain, _getAssetSymbol, _getChainNativeTokenSlug, _getContractAddressOfToken, _getEvmChainId, _getMultiChainAsset, _getOriginChainOfAsset, _getTokenMinAmount, _isChainCardanoCompatible, _isChainEvmCompatible, _isNativeToken, _isTokenTransferredByEvm } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getAssetName, _getAssetOriginChain, _getAssetSymbol, _getChainNativeTokenSlug, _getContractAddressOfToken, _getEvmChainId, _getMultiChainAsset, _getOriginChainOfAsset, _getTokenMinAmount, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isNativeToken, _isTokenTransferredByEvm } from '@subwallet/extension-base/services/chain-service/utils'; import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; import { TokenHasBalanceInfo } from '@subwallet/extension-base/services/fee-service/interfaces'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; @@ -192,7 +192,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone return true; } - return !!chainInfo && !!assetInfo && destChainValue === chainValue && _isNativeToken(assetInfo) && (_isChainEvmCompatible(chainInfo) || _isChainCardanoCompatible(chainInfo)); + return !!chainInfo && !!assetInfo && destChainValue === chainValue && _isNativeToken(assetInfo) && (_isChainEvmCompatible(chainInfo) || _isChainCardanoCompatible(chainInfo) || _isChainBitcoinCompatible(chainInfo)); }, [chainInfoMap, chainValue, destChainValue, assetInfo]); const disabledToAddressInput = useMemo(() => { diff --git a/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/index.tsx b/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/index.tsx index b9830013835..e6bb5bd04b9 100644 --- a/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/index.tsx +++ b/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/index.tsx @@ -56,7 +56,7 @@ type Props = ThemeProps & { // todo: will update dynamic later const modalId = 'FeeEditorModalId'; -const FEE_TYPES_CAN_SHOW: Array = ['substrate', 'evm']; +const FEE_TYPES_CAN_SHOW: Array = ['substrate', 'evm', 'bitcoin']; const Component = ({ chainValue, className, currentTokenPayFee, destChainValue, estimateFee, feeOptionsInfo, feePercentageSpecialCase, feeType, isLoadingFee = false, isLoadingToken, listTokensCanPayFee, nativeTokenSlug, onSelect, onSetTokenPayFee, renderFieldNode, selectedFeeOption, tokenPayFeeSlug, tokenSlug }: Props): React.ReactElement => { const { t } = useTranslation(); From 3ccf65683da1205c86f821382bc7fd4f1532ba37 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 27 May 2025 11:44:32 +0700 Subject: [PATCH 090/178] [Issue-4316] fix some ui bugs --- .../src/Popup/Account/AccountDetail/index.tsx | 3 +- .../AccountProxy/AddressSelectorItem.tsx | 69 ++++++++++++------- .../Modal/Global/AddressQrModal.tsx | 12 +--- .../screen/home/useCoreReceiveModalHelper.tsx | 8 +++ .../src/utils/account/account.ts | 24 +++---- 5 files changed, 69 insertions(+), 47 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx b/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx index 69f5ee247ac..642a6022049 100644 --- a/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx @@ -516,7 +516,8 @@ const AccountDetail = styled(Wrapper)(({ theme: { token } }: Props) => { '.__item-chain-type-logos': { minHeight: 20, - marginRight: 12 + marginRight: 12, + marginLeft: 12 }, '.account-detail-form, .derivation-info-form': { diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx index 4f0c7696f39..692e15fe3a7 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx @@ -50,29 +50,46 @@ function Component (props: Props): React.ReactElement {
-
- { - !!name && ( -
- {name} -
- ) - } - {!!bitcoinAttributes && !!bitcoinAttributes.schema - ? ( - <> -
  -  
-
- {bitcoinAttributes.label} + {name + ? ( + <> +
+
+ {name}
- - ) - : null} -
- -
- {toShort(address, 9, 10)} -
+ {!!bitcoinAttributes && !!bitcoinAttributes.schema + ? ( + <> +
 - 
+
+ {bitcoinAttributes.label} +
+ + ) + : null} +
+
+ {toShort(address, 9, 10)} +
+ + ) + : ( +
+
+ {toShort(address, 9, 10)} +
+ {!!bitcoinAttributes && !!bitcoinAttributes.schema + ? ( + <> +
+ {bitcoinAttributes.label} +
+ + ) + : null} +
+ + )}
@@ -125,7 +142,13 @@ const AddressSelectorItem = styled(Component)(({ theme: { token } }: Prop '.__item-name-wrapper': { display: 'flex', - alignItems: 'center' + alignItems: 'baseline' + }, + + '.__item-address-wrapper': { + display: 'flex', + gap: 12, + alignItems: 'baseline' }, '.__item-right-part': { diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx index 29c5557d69f..9cb072a6ba1 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx @@ -177,7 +177,6 @@ const Component: React.FC = ({ accountTokenAddresses = [], address: initi size='md' />} onClick={handlePrevious} - tooltip={t('Previous address')} type='ghost' /> )} @@ -199,7 +198,6 @@ const Component: React.FC = ({ accountTokenAddresses = [], address: initi size='md' />} onClick={handleNext} - tooltip={t('Next address')} type='ghost' /> )} @@ -209,8 +207,7 @@ const Component: React.FC = ({ accountTokenAddresses = [], address: initi {!!bitcoinAttributes && !!bitcoinAttributes.label ? (
-
{bitcoinAttributes.label} BTC
-  address +
{bitcoinAttributes.label}
) : null} @@ -322,13 +319,6 @@ const AddressQrModal = styled(Component)(({ theme: { token } }: Props) => fontSize: token.fontSizeSM, lineHeight: token.lineHeightSM, color: token.colorTextLight2 - }, - - '.__label-address-suffix': { - fontWeight: 500, - fontSize: token.fontSizeSM, - lineHeight: token.lineHeightSM, - color: token.colorTextTertiary } }, diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx index 3ea8c10ed39..05f2dd41989 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx +++ b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx @@ -11,6 +11,7 @@ import { useGetChainSlugsByAccount, useHandleLedgerGenericAccountWarning, useHan import { useChainAssets } from '@subwallet/extension-koni-ui/hooks/assets'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, AccountTokenAddress, ReceiveModalProps } from '@subwallet/extension-koni-ui/types'; +import { getBitcoinAccountDetails } from '@subwallet/extension-koni-ui/utils'; import { BitcoinMainnetKeypairTypes, BitcoinTestnetKeypairTypes, KeypairType } from '@subwallet/keyring/types'; import { ModalContext } from '@subwallet/react-ui'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; @@ -178,6 +179,13 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo chainInfo ); + accountTokenAddressList.sort((a, b) => { + const aDetails = getBitcoinAccountDetails(a.accountInfo.type); + const bDetails = getBitcoinAccountDetails(b.accountInfo.type); + + return aDetails.order - bDetails.order; + }); + if (accountTokenAddressList.length > 1) { openAccountTokenAddressModal(accountTokenAddressList, () => { inactiveModal(tokenSelectorModalId); diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index 25e1b78d803..c8954522e59 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -170,39 +170,39 @@ export function getBitcoinAccountDetails (type: KeypairType): BitcoinAccountInfo }; switch (type) { - case 'bitcoin-44': + case 'bitcoin-84': result.logoKey = 'bitcoin'; - result.name = 'Legacy'; + result.name = 'Native SegWit'; result.order = 1; break; - case 'bitcoin-84': - result.logoKey = 'bitcoin'; + case 'bittest-84': + result.logoKey = 'bitcoinTestnet'; result.name = 'Native SegWit'; result.order = 2; break; case 'bitcoin-86': - result.logoKey = 'ordinal_rune'; + result.logoKey = 'bitcoin'; result.name = 'Taproot'; result.order = 3; break; - case 'bittest-44': + case 'bittest-86': result.logoKey = 'bitcoinTestnet'; - result.name = 'Legacy'; + result.name = 'Taproot'; result.order = 4; break; - case 'bittest-84': - result.logoKey = 'bitcoinTestnet'; - result.name = 'Native SegWit'; + case 'bitcoin-44': + result.logoKey = 'bitcoin'; + result.name = 'Legacy'; result.order = 5; break; - case 'bittest-86': + case 'bittest-44': result.logoKey = 'bitcoinTestnet'; - result.name = 'Taproot'; + result.name = 'Legacy'; result.order = 6; break; } From 0a111a82c9855ae7c66c76d33a94dae10f48f5f2 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 27 May 2025 19:02:58 +0700 Subject: [PATCH 091/178] [Issue-4316] Update UX for Bitcoin account details --- .../src/services/chain-service/utils/index.ts | 5 + .../AccountProxy/AccountChainAddressItem.tsx | 7 +- .../list/AccountChainAddressList.tsx | 94 ++++++++++++++++--- .../src/hooks/common/index.ts | 1 + .../src/hooks/common/useGetBitcoinAccount.tsx | 57 +++++++++++ .../screen/home/useCoreReceiveModalHelper.tsx | 48 ++-------- 6 files changed, 157 insertions(+), 55 deletions(-) create mode 100644 packages/extension-koni-ui/src/hooks/common/useGetBitcoinAccount.tsx diff --git a/packages/extension-base/src/services/chain-service/utils/index.ts b/packages/extension-base/src/services/chain-service/utils/index.ts index d42d0cb7b35..ecf633c5420 100644 --- a/packages/extension-base/src/services/chain-service/utils/index.ts +++ b/packages/extension-base/src/services/chain-service/utils/index.ts @@ -339,6 +339,11 @@ export function _getChainNativeTokenBasicInfo (chainInfo: _ChainInfo): BasicToke symbol: chainInfo.cardanoInfo.symbol, decimals: chainInfo.cardanoInfo.decimals }; + } else if (chainInfo.bitcoinInfo) { + return { + symbol: chainInfo.bitcoinInfo.symbol, + decimals: chainInfo.bitcoinInfo.decimals + }; } return defaultTokenInfo; diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx index 8c88f1de6f3..169b7af33d4 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx @@ -3,10 +3,11 @@ import { AccountChainAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { toShort } from '@subwallet/extension-koni-ui/utils'; +import { isBitcoinAddress } from '@subwallet/keyring'; import { Button, Icon, Logo } from '@subwallet/react-ui'; import CN from 'classnames'; import { Copy, Info, QrCode } from 'phosphor-react'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; type Props = ThemeProps & { @@ -37,6 +38,8 @@ function Component (props: Props): React.ReactElement { onClickInfoButton?.(); }, [onClickInfoButton]); + const isBitcoinChain = useMemo(() => isBitcoinAddress(item.address), [item.address]); + return ( <>
{ } onClick={_onClickInfoButton} size='xs' - tooltip={'This network has two address formats'} + tooltip={isBitcoinChain ? null : 'This network has two address formats'} tooltipPlacement={'topLeft'} type='ghost' /> diff --git a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx index ade66889377..ed4f20c1b55 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx @@ -1,15 +1,19 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; import { AccountProxy } from '@subwallet/extension-base/types'; import { AccountChainAddressItem, GeneralEmptyList } from '@subwallet/extension-koni-ui/components'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import { useGetAccountChainAddresses, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useNotification, useTranslation } from '@subwallet/extension-koni-ui/hooks'; -import { AccountChainAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { useGetAccountChainAddresses, useGetBitcoinAccount, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useNotification, useSelector, useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { AccountChainAddress, AccountTokenAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { copyToClipboard } from '@subwallet/extension-koni-ui/utils'; +import { isBitcoinAddress } from '@subwallet/keyring'; +import { BitcoinAddressType } from '@subwallet/keyring/types'; +import { getBitcoinAddressInfo } from '@subwallet/keyring/utils/address/validate'; import { SwList } from '@subwallet/react-ui'; -import React, { useCallback, useContext, useEffect } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo } from 'react'; import styled from 'styled-components'; type Props = ThemeProps & { @@ -23,11 +27,29 @@ type Props = ThemeProps & { function Component ({ accountProxy, className, isInModal, modalProps }: Props) { const { t } = useTranslation(); const items: AccountChainAddress[] = useGetAccountChainAddresses(accountProxy); + const getBitcoinAccount = useGetBitcoinAccount(); const notify = useNotification(); const onHandleTonAccountWarning = useHandleTonAccountWarning(); const onHandleLedgerGenericAccountWarning = useHandleLedgerGenericAccountWarning(); - const { addressQrModal, selectAddressFormatModal } = useContext(WalletModalContext); + const { accountTokenAddressModal, addressQrModal, selectAddressFormatModal } = useContext(WalletModalContext); const checkIsPolkadotUnifiedChain = useIsPolkadotUnifiedChain(); + const chainInfoMap = useSelector((state) => state.chainStore.chainInfoMap); + + const filteredItems = useMemo(() => { + if (!items) { + return []; + } + + return items.filter((item) => { + if (isBitcoinAddress(item.address)) { + const addressInfo = getBitcoinAddressInfo(item.address); + + return [BitcoinAddressType.p2wpkh, BitcoinAddressType.p2wsh].includes(addressInfo.type); + } + + return true; + }); + }, [items]); const openSelectAddressFormatModal = useCallback((item: AccountChainAddress) => { selectAddressFormatModal.open({ @@ -45,9 +67,25 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { }); }, [isInModal, modalProps, selectAddressFormatModal]); + const openAccountTokenAddressModal = useCallback((accounts: AccountTokenAddress[], closeCallback?: VoidCallback) => { + const processFunction = () => { + accountTokenAddressModal.open({ + items: accounts, + onBack: accountTokenAddressModal.close, + onCancel: () => { + accountTokenAddressModal.close(); + closeCallback?.(); + } + }); + }; + + processFunction(); + }, [accountTokenAddressModal]); + const onShowQr = useCallback((item: AccountChainAddress) => { return () => { const isPolkadotUnifiedChain = checkIsPolkadotUnifiedChain(item.slug); + const isBitcoinChain = isBitcoinAddress(item.address); const processFunction = () => { addressQrModal.open({ @@ -66,6 +104,14 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { if (isPolkadotUnifiedChain) { openSelectAddressFormatModal(item); + } else if (isBitcoinChain) { + const chainInfo = chainInfoMap[item.slug]; + + // TODO: Currently, only supports Bitcoin native token. + const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); + const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo); + + openAccountTokenAddressModal(accountTokenAddressList); } else { onHandleTonAccountWarning(item.accountType, () => { onHandleLedgerGenericAccountWarning({ @@ -75,11 +121,12 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { }); } }; - }, [accountProxy, addressQrModal, checkIsPolkadotUnifiedChain, isInModal, modalProps, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openSelectAddressFormatModal]); + }, [accountProxy, addressQrModal, chainInfoMap, checkIsPolkadotUnifiedChain, getBitcoinAccount, isInModal, modalProps, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openAccountTokenAddressModal, openSelectAddressFormatModal]); const onCopyAddress = useCallback((item: AccountChainAddress) => { return () => { const isPolkadotUnifiedChain = checkIsPolkadotUnifiedChain(item.slug); + const isBitcoinChain = isBitcoinAddress(item.address); const processFunction = () => { copyToClipboard(item.address || ''); @@ -90,6 +137,15 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { if (isPolkadotUnifiedChain) { openSelectAddressFormatModal(item); + } else if (isBitcoinChain) { + const chainInfo = chainInfoMap[item.slug]; + + // TODO: Currently, only supports Bitcoin native token. + const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); + + const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo); + + openAccountTokenAddressModal(accountTokenAddressList); } else { onHandleTonAccountWarning(item.accountType, () => { onHandleLedgerGenericAccountWarning({ @@ -99,22 +155,36 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { }); } }; - }, [accountProxy, checkIsPolkadotUnifiedChain, notify, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openSelectAddressFormatModal, t]); + }, [accountProxy, chainInfoMap, checkIsPolkadotUnifiedChain, getBitcoinAccount, notify, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openAccountTokenAddressModal, openSelectAddressFormatModal, t]); const onClickInfoButton = useCallback((item: AccountChainAddress) => { return () => { - openSelectAddressFormatModal(item); + const isBitcoinChain = isBitcoinAddress(item.address); + + if (isBitcoinChain) { + const chainInfo = chainInfoMap[item.slug]; + + // TODO: Currently, only supports Bitcoin native token. + const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); + + const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo); + + openAccountTokenAddressModal(accountTokenAddressList); + } else { + openSelectAddressFormatModal(item); + } }; - }, [openSelectAddressFormatModal]); + }, [chainInfoMap, getBitcoinAccount, openAccountTokenAddressModal, openSelectAddressFormatModal]); const renderItem = useCallback( (item: AccountChainAddress) => { const isPolkadotUnifiedChain = checkIsPolkadotUnifiedChain(item.slug); + const isBitcoinChain = isBitcoinAddress(item.address); return ( i.slug === prev.chainSlug)?.address; + const targetAddress = filteredItems.find((i) => i.slug === prev.chainSlug)?.address; if (!targetAddress) { return prev; @@ -157,13 +227,13 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { }; }); } - }, [addressQrModal, items]); + }, [addressQrModal, filteredItems]); return ( { + const isBitcoinTestnet = chainInfo.isTestnet; + const keypairTypes = isBitcoinTestnet ? BitcoinTestnetKeypairTypes : BitcoinMainnetKeypairTypes; + + return accounts + .filter( + (acc) => + acc.chainType === AccountChainType.BITCOIN && + keypairTypes.includes(acc.type) + ) + .map((item) => ({ + accountInfo: item, + tokenSlug, + chainSlug + })); +}; + +const useGetBitcoinAccount = () => { + const { currentAccountProxy } = useSelector((state: RootState) => state.accountState); + + return useCallback((chainSlug: string, tokenSlug: string, chainInfo: _ChainInfo): AccountTokenAddress[] => { + const accountTokenAddressList = transformBitcoinAccounts( + currentAccountProxy?.accounts || [], + chainSlug, + tokenSlug, + chainInfo + ); + + accountTokenAddressList.sort((a, b) => { + const aDetails = getBitcoinAccountDetails(a.accountInfo.type); + const bDetails = getBitcoinAccountDetails(b.accountInfo.type); + + return aDetails.order - bDetails.order; + }); + + return accountTokenAddressList; + }, [currentAccountProxy]); +}; + +export default useGetBitcoinAccount; diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx index 05f2dd41989..0bd154097f8 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx +++ b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx @@ -1,18 +1,17 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; +import { _ChainAsset } from '@subwallet/chain-list/types'; import { _getAssetOriginChain, _getMultiChainAsset, _isChainBitcoinCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; -import { AccountActions, AccountChainType, AccountJson, AccountProxyType } from '@subwallet/extension-base/types'; +import { AccountActions, AccountProxyType } from '@subwallet/extension-base/types'; import { RECEIVE_MODAL_ACCOUNT_SELECTOR, RECEIVE_MODAL_TOKEN_SELECTOR } from '@subwallet/extension-koni-ui/constants'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import { useGetChainSlugsByAccount, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useReformatAddress } from '@subwallet/extension-koni-ui/hooks'; +import { useGetBitcoinAccount, useGetChainSlugsByAccount, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useReformatAddress } from '@subwallet/extension-koni-ui/hooks'; import { useChainAssets } from '@subwallet/extension-koni-ui/hooks/assets'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, AccountTokenAddress, ReceiveModalProps } from '@subwallet/extension-koni-ui/types'; -import { getBitcoinAccountDetails } from '@subwallet/extension-koni-ui/utils'; -import { BitcoinMainnetKeypairTypes, BitcoinTestnetKeypairTypes, KeypairType } from '@subwallet/keyring/types'; +import { KeypairType } from '@subwallet/keyring/types'; import { ModalContext } from '@subwallet/react-ui'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; @@ -22,28 +21,6 @@ type HookType = { receiveModalProps: ReceiveModalProps; }; -const transformBitcoinAccounts = ( - accounts: AccountJson[] = [], - chainSlug: string, - tokenSlug: string, - chainInfo: _ChainInfo -): AccountTokenAddress[] => { - const isBitcoinTestnet = chainInfo.isTestnet; - const keypairTypes = isBitcoinTestnet ? BitcoinTestnetKeypairTypes : BitcoinMainnetKeypairTypes; - - return accounts - .filter( - (acc) => - acc.chainType === AccountChainType.BITCOIN && - keypairTypes.includes(acc.type) - ) - .map((item) => ({ - accountInfo: item, - tokenSlug, - chainSlug - })); -}; - const tokenSelectorModalId = RECEIVE_MODAL_TOKEN_SELECTOR; const accountSelectorModalId = RECEIVE_MODAL_ACCOUNT_SELECTOR; @@ -64,6 +41,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const onHandleLedgerGenericAccountWarning = useHandleLedgerGenericAccountWarning(); const getReformatAddress = useReformatAddress(); const checkIsPolkadotUnifiedChain = useIsPolkadotUnifiedChain(); + const getBitcoinAccount = useGetBitcoinAccount(); // chain related to tokenGroupSlug, if it is token slug const specificChain = useMemo(() => { @@ -172,19 +150,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const isBitcoinChain = _isChainBitcoinCompatible(chainInfo); if (isBitcoinChain) { - const accountTokenAddressList = transformBitcoinAccounts( - currentAccountProxy?.accounts || [], - chainSlug, - item.slug, - chainInfo - ); - - accountTokenAddressList.sort((a, b) => { - const aDetails = getBitcoinAccountDetails(a.accountInfo.type); - const bDetails = getBitcoinAccountDetails(b.accountInfo.type); - - return aDetails.order - bDetails.order; - }); + const accountTokenAddressList = getBitcoinAccount(chainSlug, item.slug, chainInfo); if (accountTokenAddressList.length > 1) { openAccountTokenAddressModal(accountTokenAddressList, () => { @@ -232,7 +198,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo break; } } - }, [currentAccountProxy, chainInfoMap, isAllAccount, checkIsPolkadotUnifiedChain, activeModal, openAccountTokenAddressModal, inactiveModal, getReformatAddress, openAddressFormatModal, openAddressQrModal]); + }, [currentAccountProxy, chainInfoMap, isAllAccount, checkIsPolkadotUnifiedChain, activeModal, getBitcoinAccount, openAccountTokenAddressModal, inactiveModal, openAddressQrModal, getReformatAddress, openAddressFormatModal]); /* token Selector --- */ From 7e5238382d1d26cb6de73af94c75050601eabe4a Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 28 May 2025 11:38:02 +0700 Subject: [PATCH 092/178] [Issue-4316] refactor ux account bitcoin details --- .../src/Popup/Account/AccountDetail/index.tsx | 4 ++-- .../AccountProxy/AccountChainAddressItem.tsx | 11 ++++----- .../AccountProxy/AddressSelectorItem.tsx | 5 +++- .../list/AccountChainAddressList.tsx | 23 +++++++++++++++---- .../src/hooks/common/useGetBitcoinAccount.tsx | 19 +++++---------- .../screen/home/useCoreReceiveModalHelper.tsx | 2 +- .../extension-koni-ui/src/types/account.ts | 9 ++++++-- 7 files changed, 44 insertions(+), 29 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx b/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx index 642a6022049..7219bf991c4 100644 --- a/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx @@ -418,7 +418,7 @@ const Component: React.FC = ({ accountProxy, onBack, requestView suffix={( )} /> @@ -514,7 +514,7 @@ const AccountDetail = styled(Wrapper)(({ theme: { token } }: Props) => { gap: token.sizeSM }, - '.__item-chain-type-logos': { + '.__account-item-chain-type-logos': { minHeight: 20, marginRight: 12, marginLeft: 12 diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx index 169b7af33d4..cc5ac44d587 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx @@ -3,11 +3,10 @@ import { AccountChainAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { toShort } from '@subwallet/extension-koni-ui/utils'; -import { isBitcoinAddress } from '@subwallet/keyring'; import { Button, Icon, Logo } from '@subwallet/react-ui'; import CN from 'classnames'; import { Copy, Info, QrCode } from 'phosphor-react'; -import React, { useMemo } from 'react'; +import React from 'react'; import styled from 'styled-components'; type Props = ThemeProps & { @@ -17,10 +16,12 @@ type Props = ThemeProps & { onClickQrButton?: VoidFunction; onClickInfoButton?: VoidFunction; isShowInfoButton?: boolean; + infoButtonTooltip?: string; } function Component (props: Props): React.ReactElement { - const { className, isShowInfoButton, + const { className, infoButtonTooltip, + isShowInfoButton, item, onClick, onClickCopyButton, onClickInfoButton, onClickQrButton } = props; const _onClickCopyButton: React.MouseEventHandler = React.useCallback((event) => { @@ -38,8 +39,6 @@ function Component (props: Props): React.ReactElement { onClickInfoButton?.(); }, [onClickInfoButton]); - const isBitcoinChain = useMemo(() => isBitcoinAddress(item.address), [item.address]); - return ( <>
{ } onClick={_onClickInfoButton} size='xs' - tooltip={isBitcoinChain ? null : 'This network has two address formats'} + tooltip={infoButtonTooltip} tooltipPlacement={'topLeft'} type='ghost' /> diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx index 692e15fe3a7..5376efa0c14 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx @@ -148,7 +148,10 @@ const AddressSelectorItem = styled(Component)(({ theme: { token } }: Prop '.__item-address-wrapper': { display: 'flex', gap: 12, - alignItems: 'baseline' + alignItems: 'baseline', + '.__address': { + fontSize: token.fontSize + } }, '.__item-right-part': { diff --git a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx index ed4f20c1b55..b6dab8b0bc1 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx @@ -7,7 +7,7 @@ import { AccountProxy } from '@subwallet/extension-base/types'; import { AccountChainAddressItem, GeneralEmptyList } from '@subwallet/extension-koni-ui/components'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; import { useGetAccountChainAddresses, useGetBitcoinAccount, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useNotification, useSelector, useTranslation } from '@subwallet/extension-koni-ui/hooks'; -import { AccountChainAddress, AccountTokenAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { AccountBitcoinInfoType, AccountChainAddress, AccountTokenAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { copyToClipboard } from '@subwallet/extension-koni-ui/utils'; import { isBitcoinAddress } from '@subwallet/keyring'; import { BitcoinAddressType } from '@subwallet/keyring/types'; @@ -35,6 +35,19 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { const checkIsPolkadotUnifiedChain = useIsPolkadotUnifiedChain(); const chainInfoMap = useSelector((state) => state.chainStore.chainInfoMap); + const bitcoinAccountList: AccountBitcoinInfoType[] = useMemo(() => { + if (!items) { + return []; + } + + return items + .filter((item) => isBitcoinAddress(item.address)) + .map((item) => ({ + address: item.address, + type: item.accountType + })); + }, [items]); + const filteredItems = useMemo(() => { if (!items) { return []; @@ -109,7 +122,7 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { // TODO: Currently, only supports Bitcoin native token. const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); - const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo); + const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo, bitcoinAccountList); openAccountTokenAddressModal(accountTokenAddressList); } else { @@ -143,7 +156,7 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { // TODO: Currently, only supports Bitcoin native token. const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); - const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo); + const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo, bitcoinAccountList); openAccountTokenAddressModal(accountTokenAddressList); } else { @@ -167,7 +180,7 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { // TODO: Currently, only supports Bitcoin native token. const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); - const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo); + const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo, bitcoinAccountList); openAccountTokenAddressModal(accountTokenAddressList); } else { @@ -180,10 +193,12 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { (item: AccountChainAddress) => { const isPolkadotUnifiedChain = checkIsPolkadotUnifiedChain(item.slug); const isBitcoinChain = isBitcoinAddress(item.address); + const tooltip = isPolkadotUnifiedChain ? 'This network has two address formats' : ''; return ( - acc.chainType === AccountChainType.BITCOIN && - keypairTypes.includes(acc.type) + (acc) => keypairTypes.includes(acc.type) ) .map((item) => ({ accountInfo: item, @@ -33,11 +28,9 @@ const transformBitcoinAccounts = ( }; const useGetBitcoinAccount = () => { - const { currentAccountProxy } = useSelector((state: RootState) => state.accountState); - - return useCallback((chainSlug: string, tokenSlug: string, chainInfo: _ChainInfo): AccountTokenAddress[] => { + return useCallback((chainSlug: string, tokenSlug: string, chainInfo: _ChainInfo, accountProxy: AccountBitcoinInfoType[]): AccountTokenAddress[] => { const accountTokenAddressList = transformBitcoinAccounts( - currentAccountProxy?.accounts || [], + accountProxy || [], chainSlug, tokenSlug, chainInfo @@ -51,7 +44,7 @@ const useGetBitcoinAccount = () => { }); return accountTokenAddressList; - }, [currentAccountProxy]); + }, []); }; export default useGetBitcoinAccount; diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx index 0bd154097f8..4b8d2dfa531 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx +++ b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx @@ -150,7 +150,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const isBitcoinChain = _isChainBitcoinCompatible(chainInfo); if (isBitcoinChain) { - const accountTokenAddressList = getBitcoinAccount(chainSlug, item.slug, chainInfo); + const accountTokenAddressList = getBitcoinAccount(chainSlug, item.slug, chainInfo, currentAccountProxy.accounts); if (accountTokenAddressList.length > 1) { openAccountTokenAddressModal(accountTokenAddressList, () => { diff --git a/packages/extension-koni-ui/src/types/account.ts b/packages/extension-koni-ui/src/types/account.ts index 3107d0a5691..38bb352d08c 100644 --- a/packages/extension-koni-ui/src/types/account.ts +++ b/packages/extension-koni-ui/src/types/account.ts @@ -3,7 +3,7 @@ import type { KeypairType } from '@subwallet/keyring/types'; -import { AccountActions, AccountJson, AccountProxyType } from '@subwallet/extension-base/types'; +import { AccountActions, AccountProxyType } from '@subwallet/extension-base/types'; export interface WordItem { index: number; @@ -36,8 +36,13 @@ export type AccountChainAddress = { logoKey?: string } +export type AccountBitcoinInfoType = { + address: string; + type: KeypairType; +} + export type AccountTokenAddress = { - accountInfo: AccountJson; + accountInfo: AccountBitcoinInfoType; tokenSlug: string; chainSlug: string; } From 2fb90eb24d5b88185b7aace3dcc7757b5fa1214a Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 28 May 2025 14:24:19 +0700 Subject: [PATCH 093/178] [Issue-4316] fix case account solo bitcoin on account details screen --- .../list/AccountChainAddressList.tsx | 139 +++++++++++++----- .../src/hooks/common/index.ts | 2 +- ...nAccount.tsx => useGetBitcoinAccounts.tsx} | 10 +- .../screen/home/useCoreReceiveModalHelper.tsx | 4 +- .../extension-koni-ui/src/types/account.ts | 4 +- 5 files changed, 111 insertions(+), 48 deletions(-) rename packages/extension-koni-ui/src/hooks/common/{useGetBitcoinAccount.tsx => useGetBitcoinAccounts.tsx} (81%) diff --git a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx index b6dab8b0bc1..6a9e3a62d51 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx @@ -6,8 +6,8 @@ import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/c import { AccountProxy } from '@subwallet/extension-base/types'; import { AccountChainAddressItem, GeneralEmptyList } from '@subwallet/extension-koni-ui/components'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import { useGetAccountChainAddresses, useGetBitcoinAccount, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useNotification, useSelector, useTranslation } from '@subwallet/extension-koni-ui/hooks'; -import { AccountBitcoinInfoType, AccountChainAddress, AccountTokenAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { useGetAccountChainAddresses, useGetBitcoinAccounts, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useNotification, useSelector, useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { AccountChainAddress, AccountInfoType, AccountTokenAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { copyToClipboard } from '@subwallet/extension-koni-ui/utils'; import { isBitcoinAddress } from '@subwallet/keyring'; import { BitcoinAddressType } from '@subwallet/keyring/types'; @@ -24,10 +24,15 @@ type Props = ThemeProps & { } }; +interface BitcoinAccountsByNetwork { + mainnet: AccountInfoType[]; + testnet: AccountInfoType[]; +} + function Component ({ accountProxy, className, isInModal, modalProps }: Props) { const { t } = useTranslation(); const items: AccountChainAddress[] = useGetAccountChainAddresses(accountProxy); - const getBitcoinAccount = useGetBitcoinAccount(); + const getBitcoinAccount = useGetBitcoinAccounts(); const notify = useNotification(); const onHandleTonAccountWarning = useHandleTonAccountWarning(); const onHandleLedgerGenericAccountWarning = useHandleLedgerGenericAccountWarning(); @@ -35,7 +40,7 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { const checkIsPolkadotUnifiedChain = useIsPolkadotUnifiedChain(); const chainInfoMap = useSelector((state) => state.chainStore.chainInfoMap); - const bitcoinAccountList: AccountBitcoinInfoType[] = useMemo(() => { + const bitcoinAccountList: AccountInfoType[] = useMemo(() => { if (!items) { return []; } @@ -48,6 +53,27 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { })); }, [items]); + const soloBitcoinAccount = useMemo((): BitcoinAccountsByNetwork => { + if (!bitcoinAccountList || bitcoinAccountList.length === 0) { + return { mainnet: [], testnet: [] }; + } + + const mainnet: AccountInfoType[] = []; + const testnet: AccountInfoType[] = []; + + bitcoinAccountList.forEach((account) => { + const bitcoinAddressInfo = getBitcoinAddressInfo(account.address); + + if (bitcoinAddressInfo.network === 'mainnet') { + mainnet.push(account); + } else { + testnet.push(account); + } + }); + + return { mainnet, testnet }; + }, [bitcoinAccountList]); + const filteredItems = useMemo(() => { if (!items) { return []; @@ -57,12 +83,33 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { if (isBitcoinAddress(item.address)) { const addressInfo = getBitcoinAddressInfo(item.address); - return [BitcoinAddressType.p2wpkh, BitcoinAddressType.p2wsh].includes(addressInfo.type); + if (addressInfo.network === 'mainnet' && soloBitcoinAccount.mainnet.length > 1) { + return [BitcoinAddressType.p2wpkh, BitcoinAddressType.p2wsh].includes(addressInfo.type); + } else if (addressInfo.network === 'testnet' && soloBitcoinAccount.testnet.length > 1) { + return [BitcoinAddressType.p2wpkh, BitcoinAddressType.p2wsh].includes(addressInfo.type); + } + + return true; } return true; }); - }, [items]); + }, [items, soloBitcoinAccount.mainnet.length, soloBitcoinAccount.testnet.length]); + + const getBitcoinTokenAddresses = useCallback( + (slug: string, bitcoinAccounts: AccountInfoType[]): AccountTokenAddress[] => { + const chainInfo = chainInfoMap[slug]; + + if (!chainInfo) { + return []; + } + + const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); + + return getBitcoinAccount(slug, nativeTokenSlug, chainInfo, bitcoinAccounts); + }, + [chainInfoMap, getBitcoinAccount] + ); const openSelectAddressFormatModal = useCallback((item: AccountChainAddress) => { selectAddressFormatModal.open({ @@ -117,22 +164,27 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { if (isPolkadotUnifiedChain) { openSelectAddressFormatModal(item); - } else if (isBitcoinChain) { - const chainInfo = chainInfoMap[item.slug]; + return; + } + + if (isBitcoinChain) { // TODO: Currently, only supports Bitcoin native token. - const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); - const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo, bitcoinAccountList); + const accountTokenAddressList = getBitcoinTokenAddresses(item.slug, bitcoinAccountList); - openAccountTokenAddressModal(accountTokenAddressList); - } else { - onHandleTonAccountWarning(item.accountType, () => { - onHandleLedgerGenericAccountWarning({ - accountProxy: accountProxy, - chainSlug: item.slug - }, processFunction); - }); + if (accountTokenAddressList.length > 1) { + openAccountTokenAddressModal(accountTokenAddressList); + + return; + } } + + onHandleTonAccountWarning(item.accountType, () => { + onHandleLedgerGenericAccountWarning({ + accountProxy: accountProxy, + chainSlug: item.slug + }, processFunction); + }); }; }, [accountProxy, addressQrModal, chainInfoMap, checkIsPolkadotUnifiedChain, getBitcoinAccount, isInModal, modalProps, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openAccountTokenAddressModal, openSelectAddressFormatModal]); @@ -150,23 +202,28 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { if (isPolkadotUnifiedChain) { openSelectAddressFormatModal(item); - } else if (isBitcoinChain) { - const chainInfo = chainInfoMap[item.slug]; + return; + } + + if (isBitcoinChain) { // TODO: Currently, only supports Bitcoin native token. - const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); - const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo, bitcoinAccountList); + const accountTokenAddressList = getBitcoinTokenAddresses(item.slug, bitcoinAccountList); - openAccountTokenAddressModal(accountTokenAddressList); - } else { - onHandleTonAccountWarning(item.accountType, () => { - onHandleLedgerGenericAccountWarning({ - accountProxy: accountProxy, - chainSlug: item.slug - }, processFunction); - }); + if (accountTokenAddressList.length > 1) { + openAccountTokenAddressModal(accountTokenAddressList); + + return; + } } + + onHandleTonAccountWarning(item.accountType, () => { + onHandleLedgerGenericAccountWarning({ + accountProxy: accountProxy, + chainSlug: item.slug + }, processFunction); + }); }; }, [accountProxy, chainInfoMap, checkIsPolkadotUnifiedChain, getBitcoinAccount, notify, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openAccountTokenAddressModal, openSelectAddressFormatModal, t]); @@ -175,17 +232,17 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { const isBitcoinChain = isBitcoinAddress(item.address); if (isBitcoinChain) { - const chainInfo = chainInfoMap[item.slug]; - // TODO: Currently, only supports Bitcoin native token. - const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); + const accountTokenAddressList = getBitcoinTokenAddresses(item.slug, bitcoinAccountList); - const accountTokenAddressList = getBitcoinAccount(item.slug, nativeTokenSlug, chainInfo, bitcoinAccountList); + if (accountTokenAddressList.length > 1) { + openAccountTokenAddressModal(accountTokenAddressList); - openAccountTokenAddressModal(accountTokenAddressList); - } else { - openSelectAddressFormatModal(item); + return; + } } + + openSelectAddressFormatModal(item); }; }, [chainInfoMap, getBitcoinAccount, openAccountTokenAddressModal, openSelectAddressFormatModal]); @@ -194,12 +251,18 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { const isPolkadotUnifiedChain = checkIsPolkadotUnifiedChain(item.slug); const isBitcoinChain = isBitcoinAddress(item.address); const tooltip = isPolkadotUnifiedChain ? 'This network has two address formats' : ''; + let isShowBitcoinInfoButton = false; + + if (isBitcoinChain) { + const accountTokenAddressList = getBitcoinTokenAddresses(item.slug, bitcoinAccountList); + isShowBitcoinInfoButton = accountTokenAddressList.length > 1; + } return ( { - return useCallback((chainSlug: string, tokenSlug: string, chainInfo: _ChainInfo, accountProxy: AccountBitcoinInfoType[]): AccountTokenAddress[] => { +const useGetBitcoinAccounts = () => { + return useCallback((chainSlug: string, tokenSlug: string, chainInfo: _ChainInfo, accountProxy: AccountInfoType[]): AccountTokenAddress[] => { const accountTokenAddressList = transformBitcoinAccounts( accountProxy || [], chainSlug, @@ -47,4 +47,4 @@ const useGetBitcoinAccount = () => { }, []); }; -export default useGetBitcoinAccount; +export default useGetBitcoinAccounts; diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx index 4b8d2dfa531..e038818affe 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx +++ b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx @@ -7,7 +7,7 @@ import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/c import { AccountActions, AccountProxyType } from '@subwallet/extension-base/types'; import { RECEIVE_MODAL_ACCOUNT_SELECTOR, RECEIVE_MODAL_TOKEN_SELECTOR } from '@subwallet/extension-koni-ui/constants'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import { useGetBitcoinAccount, useGetChainSlugsByAccount, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useReformatAddress } from '@subwallet/extension-koni-ui/hooks'; +import { useGetBitcoinAccounts, useGetChainSlugsByAccount, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useReformatAddress } from '@subwallet/extension-koni-ui/hooks'; import { useChainAssets } from '@subwallet/extension-koni-ui/hooks/assets'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, AccountTokenAddress, ReceiveModalProps } from '@subwallet/extension-koni-ui/types'; @@ -41,7 +41,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const onHandleLedgerGenericAccountWarning = useHandleLedgerGenericAccountWarning(); const getReformatAddress = useReformatAddress(); const checkIsPolkadotUnifiedChain = useIsPolkadotUnifiedChain(); - const getBitcoinAccount = useGetBitcoinAccount(); + const getBitcoinAccount = useGetBitcoinAccounts(); // chain related to tokenGroupSlug, if it is token slug const specificChain = useMemo(() => { diff --git a/packages/extension-koni-ui/src/types/account.ts b/packages/extension-koni-ui/src/types/account.ts index 38bb352d08c..0a4c10ce07e 100644 --- a/packages/extension-koni-ui/src/types/account.ts +++ b/packages/extension-koni-ui/src/types/account.ts @@ -36,13 +36,13 @@ export type AccountChainAddress = { logoKey?: string } -export type AccountBitcoinInfoType = { +export type AccountInfoType = { address: string; type: KeypairType; } export type AccountTokenAddress = { - accountInfo: AccountBitcoinInfoType; + accountInfo: AccountInfoType; tokenSlug: string; chainSlug: string; } From 691e4d6a301924c1dd4a99645c150ad0726d5059 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 28 May 2025 17:09:55 +0700 Subject: [PATCH 094/178] [Issue-4316] update content and sort account bitcoin --- .../src/Popup/Home/Tokens/DetailModal.tsx | 29 +++++++++++---- .../list/AccountChainAddressList.tsx | 24 ++++++++----- .../Modal/AddressBook/AddressBookModal.tsx | 17 ++++++++- .../Modal/Global/AccountTokenAddressModal.tsx | 36 ++++++++++++++----- .../Modal/Selector/AccountSelector.tsx | 23 +++++++++++- 5 files changed, 105 insertions(+), 24 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx index 2463b4c18eb..7489d3b6764 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx @@ -9,7 +9,7 @@ import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTransla import { RootState } from '@subwallet/extension-koni-ui/stores'; import { BalanceItemWithAddressType, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { TokenBalanceItemType } from '@subwallet/extension-koni-ui/types/balance'; -import { getBitcoinKeypairAttributes, isAccountAll } from '@subwallet/extension-koni-ui/utils'; +import { getBitcoinAccountDetails, getBitcoinKeypairAttributes, isAccountAll } from '@subwallet/extension-koni-ui/utils'; import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; import { Form, Icon, ModalContext, Number, SwModal } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; @@ -170,12 +170,29 @@ function Component ({ className = '', currentTokenInfo, id, onCancel, tokenBalan } // Sort by total balance in descending order - return result.sort((a, b) => { - const aTotal = new BigN(a.free).plus(BigN(a.locked)); - const bTotal = new BigN(b.free).plus(BigN(b.locked)); + return result + .sort((a, b) => { + const _isABitcoin = isBitcoinAddress(a.address); + const _isBBitcoin = isBitcoinAddress(b.address); - return bTotal.minus(aTotal).toNumber(); - }); + if (_isABitcoin && _isBBitcoin) { + const aKeyPairType = getKeypairTypeByAddress(a.address); + const bKeyPairType = getKeypairTypeByAddress(b.address); + + const aDetails = getBitcoinAccountDetails(aKeyPairType); + const bDetails = getBitcoinAccountDetails(bKeyPairType); + + return aDetails.order - bDetails.order; + } + + return 0; + }) + .sort((a, b) => { + const aTotal = new BigN(a.free).plus(BigN(a.locked)); + const bTotal = new BigN(b.free).plus(BigN(b.locked)); + + return bTotal.minus(aTotal).toNumber(); + }); }, [accounts, balanceMap, currentAccountProxy, currentTokenInfo?.slug, isAllAccount, isBitcoinChain]); const symbol = currentTokenInfo?.symbol || ''; diff --git a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx index 6a9e3a62d51..fa65021d303 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx @@ -32,7 +32,7 @@ interface BitcoinAccountsByNetwork { function Component ({ accountProxy, className, isInModal, modalProps }: Props) { const { t } = useTranslation(); const items: AccountChainAddress[] = useGetAccountChainAddresses(accountProxy); - const getBitcoinAccount = useGetBitcoinAccounts(); + const getBitcoinAccounts = useGetBitcoinAccounts(); const notify = useNotification(); const onHandleTonAccountWarning = useHandleTonAccountWarning(); const onHandleLedgerGenericAccountWarning = useHandleLedgerGenericAccountWarning(); @@ -106,9 +106,9 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); - return getBitcoinAccount(slug, nativeTokenSlug, chainInfo, bitcoinAccounts); + return getBitcoinAccounts(slug, nativeTokenSlug, chainInfo, bitcoinAccounts); }, - [chainInfoMap, getBitcoinAccount] + [chainInfoMap, getBitcoinAccounts] ); const openSelectAddressFormatModal = useCallback((item: AccountChainAddress) => { @@ -186,7 +186,7 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { }, processFunction); }); }; - }, [accountProxy, addressQrModal, chainInfoMap, checkIsPolkadotUnifiedChain, getBitcoinAccount, isInModal, modalProps, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openAccountTokenAddressModal, openSelectAddressFormatModal]); + }, [accountProxy, addressQrModal, bitcoinAccountList, checkIsPolkadotUnifiedChain, getBitcoinTokenAddresses, isInModal, modalProps, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openAccountTokenAddressModal, openSelectAddressFormatModal]); const onCopyAddress = useCallback((item: AccountChainAddress) => { return () => { @@ -225,7 +225,7 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { }, processFunction); }); }; - }, [accountProxy, chainInfoMap, checkIsPolkadotUnifiedChain, getBitcoinAccount, notify, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openAccountTokenAddressModal, openSelectAddressFormatModal, t]); + }, [accountProxy, bitcoinAccountList, checkIsPolkadotUnifiedChain, getBitcoinTokenAddresses, notify, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openAccountTokenAddressModal, openSelectAddressFormatModal, t]); const onClickInfoButton = useCallback((item: AccountChainAddress) => { return () => { @@ -244,17 +244,25 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { openSelectAddressFormatModal(item); }; - }, [chainInfoMap, getBitcoinAccount, openAccountTokenAddressModal, openSelectAddressFormatModal]); + }, [bitcoinAccountList, getBitcoinTokenAddresses, openAccountTokenAddressModal, openSelectAddressFormatModal]); const renderItem = useCallback( (item: AccountChainAddress) => { const isPolkadotUnifiedChain = checkIsPolkadotUnifiedChain(item.slug); const isBitcoinChain = isBitcoinAddress(item.address); - const tooltip = isPolkadotUnifiedChain ? 'This network has two address formats' : ''; + let tooltip = ''; + + if (isPolkadotUnifiedChain) { + tooltip = 'This network has two address formats'; + } else if (isBitcoinChain) { + tooltip = 'This network has three address types'; + } + let isShowBitcoinInfoButton = false; if (isBitcoinChain) { const accountTokenAddressList = getBitcoinTokenAddresses(item.slug, bitcoinAccountList); + isShowBitcoinInfoButton = accountTokenAddressList.length > 1; } @@ -272,7 +280,7 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { /> ); }, - [checkIsPolkadotUnifiedChain, onClickInfoButton, onCopyAddress, onShowQr] + [bitcoinAccountList, checkIsPolkadotUnifiedChain, getBitcoinTokenAddresses, onClickInfoButton, onCopyAddress, onShowQr] ); const emptyList = useCallback(() => { diff --git a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx index cd2a15aaad2..02f0d996b78 100644 --- a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx @@ -6,7 +6,8 @@ import { _reformatAddressWithChain, getAccountChainTypeForAddress } from '@subwa import { AddressSelectorItem, BackIcon } from '@subwallet/extension-koni-ui/components'; import { useChainInfo, useFilterModal, useReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { isAccountAll, isChainInfoAccordantAccountChainType } from '@subwallet/extension-koni-ui/utils'; +import { getBitcoinAccountDetails, isAccountAll, isChainInfoAccordantAccountChainType } from '@subwallet/extension-koni-ui/utils'; +import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; import { Badge, Icon, ModalContext, SwList, SwModal } from '@subwallet/react-ui'; import { SwListSectionRef } from '@subwallet/react-ui/es/sw-list'; import CN from 'classnames'; @@ -137,6 +138,20 @@ const Component: React.FC = (props: Props) => { return result .sort((a: AnalyzeAddress, b: AnalyzeAddress) => { + const _isABitcoin = isBitcoinAddress(a.address); + const _isBBitcoin = isBitcoinAddress(b.address); + const _isSameProxyId = a.proxyId === b.proxyId; + + if (_isABitcoin && _isBBitcoin && _isSameProxyId) { + const aKeyPairType = getKeypairTypeByAddress(a.address); + const bKeyPairType = getKeypairTypeByAddress(b.address); + + const aDetails = getBitcoinAccountDetails(aKeyPairType); + const bDetails = getBitcoinAccountDetails(bKeyPairType); + + return aDetails.order - bDetails.order; + } + return ((a?.displayName || '').toLowerCase() > (b?.displayName || '').toLowerCase()) ? 1 : -1; }) .sort((a, b) => getGroupPriority(b) - getGroupPriority(a)); diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx index 8f81becec54..294994cb791 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx @@ -25,6 +25,7 @@ type Props = ThemeProps & AccountTokenAddressModalProps & { }; const modalId = ADDRESS_GROUP_MODAL; +const LEARN_MORE_DOCS_URL = 'https://docs.subwallet.app/main/extension-user-guide/receive-and-transfer-assets/receive-tokens-and-nfts#select-your-preferred-bitcoin-address'; const Component: React.FC = ({ className, items, onBack, onCancel }: Props) => { const { t } = useTranslation(); @@ -108,14 +109,24 @@ const Component: React.FC = ({ className, items, onBack, onCancel }: Prop : undefined} title={t('Select address type')} > - } - className={'address-group-list'} - list={items} - renderItem={renderItem} - renderWhenEmpty={renderEmpty} - /> -
+
+
+ {t('SubWallet supports three Bitcoin address types for receiving and transferring assets. Make sure you choose the correct address type to avoid risks of fund loss. ')} + Learn more +
+ } + className={'address-group-list'} + list={items} + renderItem={renderItem} + renderWhenEmpty={renderEmpty} + /> +
); }; @@ -131,6 +142,15 @@ const AccountTokenAddressModal = styled(Component)(({ theme: { token } }: marginTop: 8 }, + '.sub-title': { + paddingBottom: token.padding, + fontSize: token.fontSizeSM, + fontWeight: token.bodyFontWeight, + lineHeight: token.lineHeightSM, + textAlign: 'center', + color: token.colorTextTertiary + }, + '.ant-sw-list-search-input': { paddingBottom: token.paddingXS }, diff --git a/packages/extension-koni-ui/src/components/Modal/Selector/AccountSelector.tsx b/packages/extension-koni-ui/src/components/Modal/Selector/AccountSelector.tsx index e49991d3b08..86e6a80f658 100644 --- a/packages/extension-koni-ui/src/components/Modal/Selector/AccountSelector.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Selector/AccountSelector.tsx @@ -7,6 +7,8 @@ import GeneralEmptyList from '@subwallet/extension-koni-ui/components/EmptyList/ import Search from '@subwallet/extension-koni-ui/components/Search'; import { useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { AccountAddressItemType, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { getBitcoinAccountDetails } from '@subwallet/extension-koni-ui/utils'; +import { isBitcoinAddress } from '@subwallet/keyring'; import { Icon, ModalContext, SwList, SwModal } from '@subwallet/react-ui'; import CN from 'classnames'; import { CaretLeft } from 'phosphor-react'; @@ -31,6 +33,10 @@ interface Props extends ThemeProps { const renderEmpty = () => ; +function isAccountAddressItem (item: ListItem): item is AccountAddressItemType { + return 'address' in item && 'accountProxyId' in item && 'accountName' in item && !('groupLabel' in item); +} + function Component ({ className = '', items, modalId, onBack, onCancel, onSelectItem, selectedValue }: Props): React.ReactElement { const { t } = useTranslation(); const { checkActive } = useContext(ModalContext); @@ -155,7 +161,22 @@ function Component ({ className = '', items, modalId, onBack, onCancel, onSelect result.push(...unknownAccounts); } - return result; + return result.sort((a: ListItem, b: ListItem) => { + if (isAccountAddressItem(a) && isAccountAddressItem(b)) { + const _isABitcoin = isBitcoinAddress(a.address); + const _isBBitcoin = isBitcoinAddress(b.address); + const _isSameProxyId = a.accountProxyId === b.accountProxyId; + + if (_isABitcoin && _isBBitcoin && _isSameProxyId) { + const aDetails = getBitcoinAccountDetails(a.accountType); + const bDetails = getBitcoinAccountDetails(b.accountType); + + return aDetails.order - bDetails.order; + } + } + + return 0; + }); }, [items, searchFunction, searchValue, t]); const handleSearch = useCallback((value: string) => { From 0daf06d8d8368ded01ee1e7c1e2fbb97f9c43ce5 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 29 May 2025 11:21:19 +0700 Subject: [PATCH 095/178] [Issue-4316] Fix ui bugs --- .../AccountProxy/AccountChainAddressItem.tsx | 2 +- .../AccountProxy/AccountChainTypeLogos.tsx | 2 +- .../AccountProxy/AccountProxySelectorItem.tsx | 8 ++++++-- .../src/components/Modal/Global/AddressQrModal.tsx | 13 ++++++++++++- .../hooks/account/useGetAccountChainAddresses.tsx | 2 +- packages/extension-koni-ui/src/types/account.ts | 1 + .../extension-koni-ui/src/utils/account/account.ts | 7 +++++++ 7 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx index cc5ac44d587..c515a93bbe9 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx @@ -131,7 +131,7 @@ const AccountChainAddressItem = styled(Component)(({ theme: { token } }: 'white-space': 'nowrap', gap: token.sizeXXS, flex: 1, - alignItems: 'flex-end' + alignItems: 'baseline' }, '.__item-chain-name': { diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx index c7631481a39..154dc322abe 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx @@ -57,7 +57,7 @@ const AccountChainTypeLogos = styled(Component)(({ theme: { token } }: Pr }, '.__chain-type-logo + .__chain-type-logo': { - marginLeft: -token.marginXXS + marginLeft: -6 } }; }); diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx index df59e97c7f6..d6d88f2a90c 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx @@ -347,13 +347,17 @@ const AccountProxySelectorItem = styled(Component)(({ theme }) => { '.__item-derived-path': { display: 'flex', - gap: token.sizeXS - 2, + gap: 4, alignItems: 'center', '.__derive-account-path': { fontSize: token.fontSizeSM, color: token.colorTextLight4, - lineHeight: token.lineHeightSM + lineHeight: token.lineHeightSM, + maxWidth: 103, + overflow: 'hidden', + 'white-space': 'nowrap', + textOverflow: 'ellipsis' } }, diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx index 9cb072a6ba1..535931b5c56 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx @@ -306,7 +306,18 @@ const AddressQrModal = styled(Component)(({ theme: { token } }: Props) => paddingTop: token.padding, paddingBottom: token.padding, display: 'flex', - alignItems: 'center' + alignItems: 'center', + justifyContent: 'center', + '.__prev-button': { + marginLeft: -20 + }, + '.__next-button': { + marginRight: -20 + }, + '.__qr-code ': { + marginLeft: 0, + marginRight: 0 + } }, '.__label-address-wrapper': { diff --git a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx index a79191829a1..0d069d0ee03 100644 --- a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx +++ b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx @@ -26,7 +26,7 @@ const createChainAddressItem = ( const bitcoinInfo = getBitcoinAccountDetails(accountType); return { - name: bitcoinInfo.name, + name: bitcoinInfo.network, logoKey: bitcoinInfo.logoKey, slug: chainInfo.slug, address, diff --git a/packages/extension-koni-ui/src/types/account.ts b/packages/extension-koni-ui/src/types/account.ts index 0a4c10ce07e..64600464e55 100644 --- a/packages/extension-koni-ui/src/types/account.ts +++ b/packages/extension-koni-ui/src/types/account.ts @@ -49,6 +49,7 @@ export type AccountTokenAddress = { export interface BitcoinAccountInfo { name: string; + network: string; logoKey?: string; order: number; } diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index c8954522e59..e289b647540 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -166,6 +166,7 @@ export const convertKeyTypes = (authTypes: AccountAuthType[]): KeypairType[] => export function getBitcoinAccountDetails (type: KeypairType): BitcoinAccountInfo { const result: BitcoinAccountInfo = { name: 'Unknown', + network: 'Unknown', order: 99 }; @@ -173,36 +174,42 @@ export function getBitcoinAccountDetails (type: KeypairType): BitcoinAccountInfo case 'bitcoin-84': result.logoKey = 'bitcoin'; result.name = 'Native SegWit'; + result.network = 'Bitcoin'; result.order = 1; break; case 'bittest-84': result.logoKey = 'bitcoinTestnet'; result.name = 'Native SegWit'; + result.network = 'Bitcoin Testnet'; result.order = 2; break; case 'bitcoin-86': result.logoKey = 'bitcoin'; result.name = 'Taproot'; + result.network = 'Bitcoin'; result.order = 3; break; case 'bittest-86': result.logoKey = 'bitcoinTestnet'; result.name = 'Taproot'; + result.network = 'Bitcoin Testnet'; result.order = 4; break; case 'bitcoin-44': result.logoKey = 'bitcoin'; result.name = 'Legacy'; + result.network = 'Bitcoin'; result.order = 5; break; case 'bittest-44': result.logoKey = 'bitcoinTestnet'; result.name = 'Legacy'; + result.network = 'Bitcoin Testnet'; result.order = 6; break; } From 727870bf354669cb19495961e42e45ce7103f135 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 29 May 2025 18:36:54 +0700 Subject: [PATCH 096/178] [Issue-4316] Fix issue where balance is not retrieved when manual network is turned off --- .../services/chain-service/handler/bitcoin/BitcoinApi.ts | 1 - packages/extension-base/src/services/chain-service/index.ts | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts index c5e367e6213..e605556e2a7 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts @@ -99,7 +99,6 @@ export class BitcoinApi implements _BitcoinApi { } async disconnect () { - this.api.stop(); this.onDisconnect(); this.updateConnectionStatus(_ChainConnectionStatus.DISCONNECTED); diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index 133e8f6cb0e..c52d39ab714 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -2058,7 +2058,8 @@ export class ChainService { this.substrateChainHandler.sleep(), this.evmChainHandler.sleep(), this.tonChainHandler.sleep(), - this.cardanoChainHandler.sleep() + this.cardanoChainHandler.sleep(), + this.bitcoinChainHandler.sleep() ]); this.stopCheckLatestChainData(); @@ -2069,7 +2070,8 @@ export class ChainService { this.substrateChainHandler.wakeUp(), this.evmChainHandler.wakeUp(), this.tonChainHandler.wakeUp(), - this.cardanoChainHandler.wakeUp() + this.cardanoChainHandler.wakeUp(), + this.bitcoinChainHandler.wakeUp() ]); this.checkLatestData(); From ae2b12e8dee3e45a5a4952525f0407de5eb2f7a4 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 30 May 2025 09:41:41 +0700 Subject: [PATCH 097/178] [Issue-4263] test: update Autobuild for Task dApp Bitcoin --- .github/workflows/push-koni-dev.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index 707b5a24fa4..323adda62e1 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -7,11 +7,13 @@ on: - subwallet-dev - koni/dev/issue-4200-v2 - koni/dev/issue-4094-v2 + - koni/dev/issue-4245 push: branches: - koni-dev - upgrade-ui - subwallet-dev + - koni/dev/issue-4245 jobs: master: From 30ffd0757cdbdba48c21b1126363685b15f46bfd Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 30 May 2025 09:47:25 +0700 Subject: [PATCH 098/178] [Issue-4263] test: remove Autobuild for Task dApp Bitcoin --- .github/workflows/push-koni-dev.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index 323adda62e1..707b5a24fa4 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -7,13 +7,11 @@ on: - subwallet-dev - koni/dev/issue-4200-v2 - koni/dev/issue-4094-v2 - - koni/dev/issue-4245 push: branches: - koni-dev - upgrade-ui - subwallet-dev - - koni/dev/issue-4245 jobs: master: From 5af434b342911ae44a17afebda49c694c97a4e0f Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 30 May 2025 10:11:28 +0700 Subject: [PATCH 099/178] [Issue-4200] lint: fix eslint issues after merging `subwallet-dev` --- .../src/contexts/WalletModalContextProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx b/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx index 706ce5c6b82..1c638aa134c 100644 --- a/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx +++ b/packages/extension-koni-ui/src/contexts/WalletModalContextProvider.tsx @@ -1,7 +1,7 @@ // Copyright 2019-2022 @polkadot/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AccountMigrationInProgressWarningModal,AccountTokenAddressModal, AddressQrModal, AlertModal, AttachAccountModal, ClaimDappStakingRewardsModal, CreateAccountModal, DeriveAccountActionModal, DeriveAccountListModal, ImportAccountModal, ImportSeedModal, NewSeedModal, RemindBackupSeedPhraseModal, RemindDuplicateAccountNameModal, RequestCameraAccessModal, RequestCreatePasswordModal, SelectAddressFormatModal, SwitchNetworkAuthorizeModal, TransactionProcessDetailModal, TransactionStepsModal } from '@subwallet/extension-koni-ui/components'; +import { AccountMigrationInProgressWarningModal, AccountTokenAddressModal, AddressQrModal, AlertModal, AttachAccountModal, ClaimDappStakingRewardsModal, CreateAccountModal, DeriveAccountActionModal, DeriveAccountListModal, ImportAccountModal, ImportSeedModal, NewSeedModal, RemindBackupSeedPhraseModal, RemindDuplicateAccountNameModal, RequestCameraAccessModal, RequestCreatePasswordModal, SelectAddressFormatModal, SwitchNetworkAuthorizeModal, TransactionProcessDetailModal, TransactionStepsModal } from '@subwallet/extension-koni-ui/components'; import { CustomizeModal } from '@subwallet/extension-koni-ui/components/Modal/Customize/CustomizeModal'; import { AccountDeriveActionProps } from '@subwallet/extension-koni-ui/components/Modal/DeriveAccountActionModal'; import { AccountTokenAddressModalProps } from '@subwallet/extension-koni-ui/components/Modal/Global/AccountTokenAddressModal'; @@ -9,7 +9,7 @@ import { SelectAddressFormatModalProps } from '@subwallet/extension-koni-ui/comp import SwapFeesModal, { SwapFeesModalProps } from '@subwallet/extension-koni-ui/components/Modal/Swap/SwapFeesModal'; import { SwitchNetworkAuthorizeModalProps } from '@subwallet/extension-koni-ui/components/Modal/SwitchNetworkAuthorizeModal'; import { TransactionStepsModalProps } from '@subwallet/extension-koni-ui/components/Modal/TransactionStepsModal'; -import { ACCOUNT_MIGRATION_IN_PROGRESS_WARNING_MODAL,ADDRESS_GROUP_MODAL, ADDRESS_QR_MODAL, DERIVE_ACCOUNT_ACTION_MODAL, EARNING_INSTRUCTION_MODAL, GLOBAL_ALERT_MODAL, SELECT_ADDRESS_FORMAT_MODAL, SWAP_FEES_MODAL, SWITCH_CURRENT_NETWORK_AUTHORIZE_MODAL, TRANSACTION_PROCESS_DETAIL_MODAL, TRANSACTION_STEPS_MODAL } from '@subwallet/extension-koni-ui/constants'; +import { ACCOUNT_MIGRATION_IN_PROGRESS_WARNING_MODAL, ADDRESS_GROUP_MODAL, ADDRESS_QR_MODAL, DERIVE_ACCOUNT_ACTION_MODAL, EARNING_INSTRUCTION_MODAL, GLOBAL_ALERT_MODAL, SELECT_ADDRESS_FORMAT_MODAL, SWAP_FEES_MODAL, SWITCH_CURRENT_NETWORK_AUTHORIZE_MODAL, TRANSACTION_PROCESS_DETAIL_MODAL, TRANSACTION_STEPS_MODAL } from '@subwallet/extension-koni-ui/constants'; import { useAlert, useExtensionDisplayModes, useGetConfig, useSetSessionLatest } from '@subwallet/extension-koni-ui/hooks'; import Confirmations from '@subwallet/extension-koni-ui/Popup/Confirmations'; import { RootState } from '@subwallet/extension-koni-ui/stores'; From 78d7874d0b7da5f408c3912db27f3e57fb4a3351 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 30 May 2025 10:26:38 +0700 Subject: [PATCH 100/178] [Issue-4200] lint: resolve conflict after merge --- .../src/koni/background/handlers/Extension.ts | 6 ++---- .../src/services/transaction-service/helpers/index.ts | 2 +- .../src/services/transaction-service/types.ts | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 605543eed0f..53c4ea2e2e3 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -7,8 +7,7 @@ import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _MultiChainAsset } from import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { createSubscription } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, HistoryTokenPriceJSON, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetHistoryTokenPriceData, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMigratePassword, RequestMigrateSoloAccount, RequestMigrateUnifiedAndFetchEligibleSoloAccounts, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestPingSession, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveMigrationAcknowledgedStatus, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSaveUnifiedAccountMigrationInProgress, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestSwitchCurrentNetworkAuthorization, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseMigrateSoloAccount, ResponseMigrateUnifiedAndFetchEligibleSoloAccounts, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeCurrentTokenPrice, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, ThemeNames, TokenPriorityDetails, TransactionHistoryItem, TransactionResponse, UiSettings, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; -import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, HistoryTokenPriceJSON, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetHistoryTokenPriceData, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMigratePassword, RequestMigrateSoloAccount, RequestMigrateUnifiedAndFetchEligibleSoloAccounts, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestPingSession, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveMigrationAcknowledgedStatus, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSaveUnifiedAccountMigrationInProgress, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseMigrateSoloAccount, ResponseMigrateUnifiedAndFetchEligibleSoloAccounts, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeCurrentTokenPrice, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, ThemeNames, TokenPriorityDetails, TransactionHistoryItem, TransactionResponse, UiSettings, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, HistoryTokenPriceJSON, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetHistoryTokenPriceData, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMigratePassword, RequestMigrateSoloAccount, RequestMigrateUnifiedAndFetchEligibleSoloAccounts, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestPingSession, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveMigrationAcknowledgedStatus, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSaveUnifiedAccountMigrationInProgress, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestSwitchCurrentNetworkAuthorization, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseMigrateSoloAccount, ResponseMigrateUnifiedAndFetchEligibleSoloAccounts, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeCurrentTokenPrice, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, ThemeNames, TokenPriorityDetails, TransactionHistoryItem, TransactionResponse, UiSettings, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountExport, RequestAuthorizeCancel, RequestAuthorizeReject, RequestCurrentAccountAddress, RequestMetadataApprove, RequestMetadataReject, RequestSigningApproveSignature, RequestSigningCancel, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseType, SigningRequest, WindowOpenParams } from '@subwallet/extension-base/background/types'; import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning'; import { _SUPPORT_TOKEN_PAY_FEE_GROUP, ALL_ACCOUNT_KEY, LATEST_SESSION } from '@subwallet/extension-base/constants'; @@ -44,8 +43,7 @@ import { _isPosChainBridge, getClaimPosBridge } from '@subwallet/extension-base/ import { estimateXcmFee } from '@subwallet/extension-base/services/balance-service/transfer/xcm/utils'; import { _DEFAULT_MANTA_ZK_CHAIN, _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX } from '@subwallet/extension-base/services/chain-service/constants'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _NetworkUpsertParams, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse, EnableChainParams, EnableMultiChainParams } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getEvmChainId, _isAssetSmartContractNft, _isChainEnabled, _isChainEvmCompatible, _isChainSubstrateCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isNativeTokenBySlug, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByCardano, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; -import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getEvmChainId, _isAssetSmartContractNft, _isChainEvmCompatible, _isChainSubstrateCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isNativeTokenBySlug, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByBitcoin, _isTokenTransferredByCardano, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getEvmChainId, _isAssetSmartContractNft, _isChainEnabled, _isChainEvmCompatible, _isChainSubstrateCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isNativeTokenBySlug, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByBitcoin, _isTokenTransferredByCardano, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; import { TokenHasBalanceInfo, TokenPayFeeInfo } from '@subwallet/extension-base/services/fee-service/interfaces'; import { calculateToAmountByReservePool } from '@subwallet/extension-base/services/fee-service/utils'; import { batchExtrinsicSetFeeHydration, getAssetHubTokensCanPayFee, getHydrationTokensCanPayFee } from '@subwallet/extension-base/services/fee-service/utils/tokenPayFee'; diff --git a/packages/extension-base/src/services/transaction-service/helpers/index.ts b/packages/extension-base/src/services/transaction-service/helpers/index.ts index 69d8092e992..b77a82abc17 100644 --- a/packages/extension-base/src/services/transaction-service/helpers/index.ts +++ b/packages/extension-base/src/services/transaction-service/helpers/index.ts @@ -5,7 +5,7 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { CardanoTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/cardano-transfer'; import { TonTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; -import { SWTransactionBase } from '@subwallet/extension-base/services/transaction-service/types'; +import { SWTransaction, SWTransactionBase } from '@subwallet/extension-base/services/transaction-service/types'; import { Psbt } from 'bitcoinjs-lib'; import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; diff --git a/packages/extension-base/src/services/transaction-service/types.ts b/packages/extension-base/src/services/transaction-service/types.ts index 4c4f7046f22..8099937034b 100644 --- a/packages/extension-base/src/services/transaction-service/types.ts +++ b/packages/extension-base/src/services/transaction-service/types.ts @@ -28,7 +28,7 @@ export interface SWTransactionBase extends ValidateTransactionResponse, Partial< updatedAt: number; estimateFee?: FeeData, xcmFeeDryRun?: string; - transaction: SubmittableExtrinsic | TransactionConfig | TonTransactionConfig | Psbt; + transaction: any; additionalValidator?: (inputTransaction: SWTransactionResponse) => Promise; eventsHandler?: (eventEmitter: TransactionEmitter) => void; isPassConfirmation?: boolean; @@ -38,7 +38,7 @@ export interface SWTransactionBase extends ValidateTransactionResponse, Partial< } export interface SWTransaction extends SWTransactionBase { - transaction: SubmittableExtrinsic | TransactionConfig | TonTransactionConfig; + transaction: SubmittableExtrinsic | TransactionConfig | TonTransactionConfig | Psbt; } export interface SWPermitTransaction extends SWTransactionBase { From afae0de7fd7cde8bdfd6c8162b8718cff1644d81 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 3 Jun 2025 11:26:06 +0700 Subject: [PATCH 101/178] [Issue-4263] feat: add support for bitcoin history. --- .../src/background/KoniTypes.ts | 2 +- .../history-service/bitcoin-history.ts | 56 +++++++++++++ .../helpers/recoverHistoryStatus.ts | 79 +++++++++++++++++-- .../src/services/history-service/index.ts | 32 +++++++- .../src/utils/account/common.ts | 5 +- .../Home/History/Detail/parts/Layout.tsx | 4 +- .../src/Popup/Home/History/index.tsx | 16 +++- 7 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 packages/extension-base/src/services/history-service/bitcoin-history.ts diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 5ee93d593d8..e9d82c20056 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -735,7 +735,7 @@ export type TransactionAdditionalInfo = { // ? Pick // : undefined; export interface TransactionHistoryItem { - origin?: 'app' | 'migration' | 'subsquid' | 'subscan', // 'app' or history source + origin?: 'app' | 'migration' | 'subsquid' | 'subscan' | 'blockstream', // 'app' or history source callhash?: string, signature?: string, chain: string, diff --git a/packages/extension-base/src/services/history-service/bitcoin-history.ts b/packages/extension-base/src/services/history-service/bitcoin-history.ts new file mode 100644 index 00000000000..8a5817e4b04 --- /dev/null +++ b/packages/extension-base/src/services/history-service/bitcoin-history.ts @@ -0,0 +1,56 @@ +// Copyright 2019-2022 @subwallet/extension-koni authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { _ChainInfo } from '@subwallet/chain-list/types'; +import { ChainType, ExtrinsicStatus, ExtrinsicType, TransactionDirection, TransactionHistoryItem } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinTx } from '@subwallet/extension-base/types'; + +function isSender (address: string, transferItem: BitcoinTx) { + return transferItem.vin.some((i) => i.prevout.scriptpubkey_address === address); +} + +export function parseBitcoinTransferData (address: string, transferItem: BitcoinTx, chainInfo: _ChainInfo): TransactionHistoryItem { + const chainType = ChainType.BITCOIN; + const nativeDecimals = chainInfo.bitcoinInfo?.decimals || 8; + const nativeSymbol = chainInfo.bitcoinInfo?.symbol || ''; + + const isCurrentAddressSender = isSender(address, transferItem); + + const sender = isCurrentAddressSender ? address : transferItem.vin[0]?.prevout?.scriptpubkey_address || ''; + const receiver = isCurrentAddressSender ? transferItem.vout[0]?.scriptpubkey_address || '' : address; + + const amountValue = (() => { + if (isCurrentAddressSender) { + return (transferItem.vout.find((i) => i.scriptpubkey_address === receiver))?.value || '0'; + } + + return (transferItem.vout.find((i) => i.scriptpubkey_address === address))?.value || '0'; + })(); + + return { + address, + origin: 'blockstream', + time: 0, // From api, cannot get time submit transaction + blockTime: transferItem.status.block_time ? transferItem.status.block_time * 1000 : undefined, + chainType, + type: ExtrinsicType.TRANSFER_BALANCE, + extrinsicHash: transferItem.txid, + chain: chainInfo.slug, + direction: address === sender ? TransactionDirection.SEND : TransactionDirection.RECEIVED, + fee: { + value: `${transferItem.fee}`, + decimals: nativeDecimals, + symbol: nativeSymbol + }, + from: sender, + to: receiver, + blockNumber: transferItem.status.block_height || 0, + blockHash: transferItem.status.block_hash || '', + amount: { + value: `${amountValue}`, + decimals: nativeDecimals, + symbol: nativeSymbol + }, + status: transferItem.status.confirmed ? ExtrinsicStatus.SUCCESS : ExtrinsicStatus.PROCESSING + }; +} diff --git a/packages/extension-base/src/services/history-service/helpers/recoverHistoryStatus.ts b/packages/extension-base/src/services/history-service/helpers/recoverHistoryStatus.ts index f255fd41e31..3350fe008d8 100644 --- a/packages/extension-base/src/services/history-service/helpers/recoverHistoryStatus.ts +++ b/packages/extension-base/src/services/history-service/helpers/recoverHistoryStatus.ts @@ -16,7 +16,8 @@ export enum HistoryRecoverStatus { API_INACTIVE = 'API_INACTIVE', LACK_INFO = 'LACK_INFO', FAIL_DETECT = 'FAIL_DETECT', - UNKNOWN = 'UNKNOWN' + UNKNOWN = 'UNKNOWN', + TX_PENDING = 'TX_PENDING', } export interface TransactionRecoverResult { @@ -24,6 +25,7 @@ export interface TransactionRecoverResult { extrinsicHash?: string; blockHash?: string; blockNumber?: number; + blockTime?: number; } const BLOCK_LIMIT = 6; @@ -213,17 +215,84 @@ const evmRecover = async (history: TransactionHistoryItem, chainService: ChainSe } }; +const bitcoinRecover = async (history: TransactionHistoryItem, chainService: ChainService): Promise => { + const { chain, extrinsicHash } = history; + const result: TransactionRecoverResult = { + status: HistoryRecoverStatus.UNKNOWN + }; + + try { + const bitcoinApi = chainService.getBitcoinApi(chain); + + if (bitcoinApi) { + const api = bitcoinApi.api; + + if (extrinsicHash) { + try { + const timeout = new Promise((resolve) => { + setTimeout(() => { + resolve(undefined); + }, 60000); + }); + const transactionDetail = await Promise.race([api.getTransactionDetail(extrinsicHash), timeout]); + + if (transactionDetail) { + result.blockHash = transactionDetail.status.block_hash || undefined; + result.blockNumber = transactionDetail.status.block_height || undefined; + result.blockTime = transactionDetail.status.block_time ? (transactionDetail.status.block_time * 1000) : undefined; + + return { ...result, status: transactionDetail ? HistoryRecoverStatus.SUCCESS : HistoryRecoverStatus.TX_PENDING }; + } else { + return { ...result, status: HistoryRecoverStatus.API_INACTIVE }; + } + } catch (e) { + // Fail when cannot find transaction + return { ...result, status: HistoryRecoverStatus.FAILED }; + } + } + + return { status: HistoryRecoverStatus.FAIL_DETECT }; + } else { + console.error(`Fail to update history ${chain}-${extrinsicHash}: Api not active`); + + return { status: HistoryRecoverStatus.API_INACTIVE }; + } + } catch (e) { + console.error(`Fail to update history ${chain}-${extrinsicHash}:`, (e as Error).message); + + return { status: HistoryRecoverStatus.UNKNOWN }; + } +}; + // undefined: Cannot check status // true: Transaction success // false: Transaction failed export const historyRecover = async (history: TransactionHistoryItem, chainService: ChainService): Promise => { const { chainType } = history; - if (chainType) { - const checkFunction = chainType === 'substrate' ? substrateRecover : evmRecover; + if (!chainType) { + return { status: HistoryRecoverStatus.LACK_INFO }; + } + const recoverFunctions: Record Promise> = { + substrate: substrateRecover, + evm: evmRecover, + bitcoin: bitcoinRecover + }; + + const checkFunction = recoverFunctions[chainType]; + + if (!checkFunction) { + console.warn(`Chain type ${chainType} is not supported for recoverHistory`); + + return { status: HistoryRecoverStatus.UNKNOWN }; + } + + try { return await checkFunction(history, chainService); - } else { - return { status: HistoryRecoverStatus.LACK_INFO }; + } catch (error) { + console.error(`Failed to recover history for chain type ${chainType}:`, error); + + return { status: HistoryRecoverStatus.FAILED }; } }; diff --git a/packages/extension-base/src/services/history-service/index.ts b/packages/extension-base/src/services/history-service/index.ts index 77732063176..6c316843aa4 100644 --- a/packages/extension-base/src/services/history-service/index.ts +++ b/packages/extension-base/src/services/history-service/index.ts @@ -5,8 +5,9 @@ import { ChainType, ExtrinsicStatus, ExtrinsicType, TransactionHistoryItem, XCMT import { CRON_RECOVER_HISTORY_INTERVAL } from '@subwallet/extension-base/constants'; import { PersistDataServiceInterface, ServiceStatus, StoppableServiceInterface } from '@subwallet/extension-base/services/base/types'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; -import { _isChainEvmCompatible, _isChainSubstrateCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { _isChainBitcoinCompatible, _isChainEvmCompatible, _isChainSubstrateCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { EventService } from '@subwallet/extension-base/services/event-service'; +import { parseBitcoinTransferData } from '@subwallet/extension-base/services/history-service/bitcoin-history'; import { historyRecover, HistoryRecoverStatus } from '@subwallet/extension-base/services/history-service/helpers/recoverHistoryStatus'; import { getExtrinsicParserKey } from '@subwallet/extension-base/services/history-service/helpers/subscan-extrinsic-parser-helper'; import { parseSubscanExtrinsicData, parseSubscanTransferData } from '@subwallet/extension-base/services/history-service/subscan-history'; @@ -179,23 +180,48 @@ export class HistoryService implements StoppableServiceInterface, PersistDataSer }); } + private async fetchBitcoinTransactionHistory (chain: string, addresses: string[]) { + const chainInfo = this.chainService.getChainInfoByKey(chain); + const chainState = this.chainService.getChainStateByKey(chain); + + if (!chainState.active) { + return; + } + + const bitcoinApi = this.chainService.getBitcoinApi(chain); + + for (const address of addresses) { + const transferItems = await bitcoinApi.api.getAddressTransaction(address); + + const parsedItems = Object.values(transferItems).map((i) => { + return parseBitcoinTransferData(address, i, chainInfo); + }); + + await this.addHistoryItems(parsedItems); + } + } + subscribeHistories (chain: string, proxyId: string, cb: (items: TransactionHistoryItem[]) => void) { const addresses = this.keyringService.context.getDecodedAddresses(proxyId, false); + const chainInfo = this.chainService.getChainInfoByKey(chain); const evmAddresses = getAddressesByChainType(addresses, [ChainType.EVM]); const substrateAddresses = getAddressesByChainType(addresses, [ChainType.SUBSTRATE]); + const bitcoinAddresses = getAddressesByChainType(addresses, [ChainType.BITCOIN], chainInfo); const subscription = this.historySubject.subscribe((items) => { cb(items.filter(filterHistoryItemByAddressAndChain(chain, addresses))); }); - const chainInfo = this.chainService.getChainInfoByKey(chain); - if (_isChainSubstrateCompatible(chainInfo)) { if (_isChainEvmCompatible(chainInfo)) { this.fetchSubscanTransactionHistory(chain, evmAddresses); } else { this.fetchSubscanTransactionHistory(chain, substrateAddresses); } + } else if (_isChainBitcoinCompatible(chainInfo)) { + this.fetchBitcoinTransactionHistory(chain, bitcoinAddresses).catch((e) => { + console.log('fetchBitcoinTransactionHistory Error', e); + }); } return { diff --git a/packages/extension-base/src/utils/account/common.ts b/packages/extension-base/src/utils/account/common.ts index d926ceb0f59..6772b48a706 100644 --- a/packages/extension-base/src/utils/account/common.ts +++ b/packages/extension-base/src/utils/account/common.ts @@ -79,8 +79,9 @@ interface AddressesByChainType { [ChainType.CARDANO]: string[] } -export function getAddressesByChainType (addresses: string[], chainTypes: ChainType[]): string[] { - const addressByChainTypeMap = getAddressesByChainTypeMap(addresses); +// TODO: Recheck the usage of this function for Bitcoin; it is currently applied to history. +export function getAddressesByChainType (addresses: string[], chainTypes: ChainType[], chainInfo?: _ChainInfo): string[] { + const addressByChainTypeMap = getAddressesByChainTypeMap(addresses, chainInfo); return chainTypes.map((chainType) => { return addressByChainTypeMap[chainType]; diff --git a/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx b/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx index 72feb50a115..6942e3e4fa1 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx @@ -56,8 +56,8 @@ const Component: React.FC = (props: Props) => { valueColorSchema={HistoryStatusMap[data.status].schema} /> {extrinsicHash} - {formatHistoryDate(data.time, language, 'detail')} - {data.blockTime && ({formatHistoryDate(data.blockTime, language, 'detail')})} + {!!data.time && ({formatHistoryDate(data.time, language, 'detail')})} + {!!data.blockTime && ({formatHistoryDate(data.blockTime, language, 'detail')})} { diff --git a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx index f039e513896..0a71ae709b7 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx @@ -70,7 +70,7 @@ function getIcon (item: TransactionHistoryItem): SwIconProps['phosphorIcon'] { function getDisplayData (item: TransactionHistoryItem, nameMap: Record, titleMap: Record): TransactionHistoryDisplayData { let displayData: TransactionHistoryDisplayData; - const time = customFormatDate(item.time, '#hhhh#:#mm#'); + const time = customFormatDate(item.time ? item.time : item.blockTime || 0, '#hhhh#:#mm#'); const displayStatus = item.status === ExtrinsicStatus.FAIL ? 'fail' : ''; @@ -384,7 +384,17 @@ function Component ({ className = '' }: Props): React.ReactElement { const [currentItemDisplayCount, setCurrentItemDisplayCount] = useState(DEFAULT_ITEMS_COUNT); const getHistoryItems = useCallback((count: number) => { - return Object.values(historyMap).filter(filterFunction).sort((a, b) => (b.time - a.time)).slice(0, count); + return Object.values(historyMap).filter(filterFunction).sort((a, b) => { + if (a.time !== 0 && b.time !== 0) { + return b.time - a.time; + } + + const blockTimeA = a.blockTime ?? 0; + const blockTimeB = b.blockTime ?? 0; + + return blockTimeB - blockTimeA; + }) + .slice(0, count); }, [filterFunction, historyMap]); const [historyItems, setHistoryItems] = useState(getHistoryItems(DEFAULT_ITEMS_COUNT)); @@ -483,7 +493,7 @@ function Component ({ className = '' }: Props): React.ReactElement { ); const groupBy = useCallback((item: TransactionHistoryItem) => { - return formatHistoryDate(item.time, language, 'list'); + return formatHistoryDate(item.time ? item.time : item.blockTime || 0, language, 'list'); }, [language]); const groupSeparator = useCallback((group: TransactionHistoryItem[], idx: number, groupLabel: string) => { From 69e360deb90d1f149962206e58730e1f3f25d1d1 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 3 Jun 2025 11:58:09 +0700 Subject: [PATCH 102/178] [Issue-4263] feat: format error message for dust limit --- .../transfer/bitcoin-transfer.ts | 4 +++- .../src/utils/bitcoin/utxo-management.ts | 24 ++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts index 9317daa1ec5..648a0bc7b94 100644 --- a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts +++ b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts @@ -104,7 +104,9 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P return [tx, transferAmount.toString()]; } catch (e) { - // const error = e as Error; + if (e instanceof TransactionError) { + throw e; + } throw new Error(`You don’t have enough BTC (${convertChainToSymbol(chain)}) for the transaction. Lower your BTC amount and try again`); } diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index 59772978f25..0fde0d228d1 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -1,9 +1,11 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 +import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { BTC_DUST_AMOUNT } from '@subwallet/extension-base/constants'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; -import { DetermineUtxosForSpendArgs, InsufficientFundsError, UtxoResponseItem } from '@subwallet/extension-base/types'; +import { DetermineUtxosForSpendArgs, InsufficientFundsError, TransferTxErrorType, UtxoResponseItem } from '@subwallet/extension-base/types'; +import { balanceFormatter, formatNumber } from '@subwallet/extension-base/utils'; import { BitcoinAddressType } from '@subwallet/keyring/types'; import { getBitcoinAddressInfo, validateBitcoinAddress } from '@subwallet/keyring/utils'; import BigN from 'bignumber.js'; @@ -33,22 +35,22 @@ export function filterUneconomicalUtxos ({ feeRate, return filteredAndSortUtxos.reduce((utxos, utxo, currentIndex) => { const utxosWithout = utxos.filter((u) => u.txid !== utxo.txid); - const { fee: feeWithout, spendableAmount: spendableAmountWithout } = getSpendableAmount({ + const { spendableAmount: spendableAmountWithout } = getSpendableAmount({ utxos: utxosWithout, feeRate, recipients, sender }); - const { fee, spendableAmount } = getSpendableAmount({ + const { spendableAmount } = getSpendableAmount({ utxos, feeRate, recipients, sender }); - console.log(utxosWithout, feeWithout, spendableAmountWithout.toString()); - console.log(utxos, fee, spendableAmount.toString()); + // console.log(utxosWithout, feeWithout, spendableAmountWithout.toString()); + // console.log(utxos, fee, spendableAmount.toString()); if (spendableAmount.lte(0)) { return utxosWithout; @@ -112,9 +114,9 @@ export function determineUtxosForSpend ({ amount, const recipientDustLimit = BTC_DUST_AMOUNT[recipientAddressInfo.type] || 546; if (amount < recipientDustLimit) { - throw new Error( - `Transfer amount ${amount} satoshis is below dust limit (${recipientDustLimit} satoshis for ${recipientAddressInfo.type})` - ); + const atLeastStr = formatNumber(recipientDustLimit, 8, balanceFormatter, { maxNumberFormat: 8, minNumberFormat: 8 }); + + throw new TransactionError(TransferTxErrorType.NOT_ENOUGH_VALUE, `You must transfer at least ${atLeastStr} BTC`); } const orderedUtxos = utxos.sort((a, b) => b.value - a.value); @@ -194,9 +196,9 @@ export function determineUtxosForSpend ({ amount, // fee: newFee // }; - throw new Error( - `The change output (${amountLeft.toString()} satoshis) is below the dust limit (${dustLimit} satoshis for ${senderAddressInfo.type})` - ); + const atLeastStr = formatNumber(dustLimit, 8, balanceFormatter, { maxNumberFormat: 8, minNumberFormat: 8 }); + + throw new TransactionError(TransferTxErrorType.NOT_ENOUGH_VALUE, `You must transfer at least ${atLeastStr} BTC`); } return { From c231f548e9245a53ba07f1f6ae386e797572f918 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 3 Jun 2025 12:00:05 +0700 Subject: [PATCH 103/178] [issue-4263] feat: hotfix transferMax value for bitcoin. --- .../extension-base/src/koni/background/handlers/Extension.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 53c4ea2e2e3..84378d04b9b 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -1519,6 +1519,11 @@ export default class KoniExtension { transferAll: !!transferAll, value: txVal, network: network }); + + // TODO: This is a hotfix until transferMax for Bitcoin is supported. + if (transferAll) { + inputData.value = transferAmount.value; + } } else { const substrateApi = this.#koniState.getSubstrateApi(chain); From 49c490fcfe132c1b40dd9e46c7be52bde59a874e Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 3 Jun 2025 12:08:55 +0700 Subject: [PATCH 104/178] [issue-4263] lint: fix eslint --- .../src/services/balance-service/transfer/bitcoin-transfer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts index 648a0bc7b94..248f95b3b4e 100644 --- a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts +++ b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts @@ -1,6 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 +import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { _BITCOIN_CHAIN_SLUG, _BITCOIN_NAME, _BITCOIN_TESTNET_NAME } from '@subwallet/extension-base/services/chain-service/constants'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; import { BitcoinFeeInfo, BitcoinFeeRate, FeeInfo, TransactionFee } from '@subwallet/extension-base/types'; From de181ce99706361326b0e213d8e6083fc6835a94 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 3 Jun 2025 17:19:35 +0700 Subject: [PATCH 105/178] [Issue-4263] feat: update balance by subtracting UTXOs immediately after transfer --- .../bitcoin/strategy/BlockStreamTestnet/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts index 4bf4369a98a..d3be59a1394 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts @@ -72,6 +72,10 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im } const rsRaw = await response.json() as BlockstreamAddressResponse; + const chainBalance = rsRaw.chain_stats.funded_txo_sum - rsRaw.chain_stats.spent_txo_sum; + const pendingLocked = rsRaw.mempool_stats.spent_txo_sum; // Only consider spent UTXOs in mempool + const availableBalance = Math.max(0, chainBalance - pendingLocked); // Ensure balance is non-negative + const rs: BitcoinAddressSummaryInfo = { address: rsRaw.address, chain_stats: { @@ -88,7 +92,7 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im spent_txo_sum: rsRaw.mempool_stats.spent_txo_sum, tx_count: rsRaw.mempool_stats.tx_count }, - balance: rsRaw.chain_stats.funded_txo_sum - rsRaw.chain_stats.spent_txo_sum, + balance: availableBalance, total_inscription: 0, balance_rune: '0', balance_inscription: '0' @@ -194,8 +198,6 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im const average = 4; const fast = 2; - console.log('getRecommendedFeeRate: estimates', estimates); - const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed(), 10); return { @@ -216,8 +218,6 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im const response = await getRequest(this.getUrl(`address/${address}/utxo`), undefined, {}); const rs = await response.json() as BlockStreamUtxo[]; - console.log('getUtxos: rs', rs); - if (!response.ok) { const errorText = await response.text(); From 0d63847dde7df54ff4c01507f491b54108ecd0ea Mon Sep 17 00:00:00 2001 From: lw Date: Tue, 3 Jun 2025 10:49:35 +0700 Subject: [PATCH 106/178] [Issue-4297] refactor: Rename useReformatAddress to useCoreReformatAddress --- packages/extension-koni-ui/src/Popup/BuyTokens.tsx | 4 ++-- .../Home/Earning/EarningPositionDetail/RewardInfoPart.tsx | 4 ++-- .../extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx | 4 ++-- .../src/Popup/Transaction/variants/SendFund.tsx | 4 ++-- .../src/Popup/Transaction/variants/Swap/index.tsx | 4 ++-- .../src/components/Modal/AddressBook/AddressBookModal.tsx | 4 ++-- .../src/hooks/account/useGetAccountChainAddresses.tsx | 4 ++-- packages/extension-koni-ui/src/hooks/common/index.ts | 2 +- .../{useReformatAddress.tsx => useCoreReformatAddress.tsx} | 4 ++-- .../src/hooks/history/useHistorySelection.tsx | 4 ++-- .../src/hooks/screen/home/useCoreReceiveModalHelper.tsx | 4 ++-- 11 files changed, 21 insertions(+), 21 deletions(-) rename packages/extension-koni-ui/src/hooks/common/{useReformatAddress.tsx => useCoreReformatAddress.tsx} (87%) diff --git a/packages/extension-koni-ui/src/Popup/BuyTokens.tsx b/packages/extension-koni-ui/src/Popup/BuyTokens.tsx index 81f3e198902..7e29a5122af 100644 --- a/packages/extension-koni-ui/src/Popup/BuyTokens.tsx +++ b/packages/extension-koni-ui/src/Popup/BuyTokens.tsx @@ -8,7 +8,7 @@ import { detectTranslate, isAccountAll } from '@subwallet/extension-base/utils'; import { AccountAddressSelector, baseServiceItems, Layout, PageWrapper, ServiceItem } from '@subwallet/extension-koni-ui/components'; import { ServiceSelector } from '@subwallet/extension-koni-ui/components/Field/BuyTokens/ServiceSelector'; import { TokenSelector } from '@subwallet/extension-koni-ui/components/Field/TokenSelector'; -import { useAssetChecker, useDefaultNavigate, useGetAccountTokenBalance, useGetChainSlugsByAccount, useNotification, useReformatAddress, useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { useAssetChecker, useCoreReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetChainSlugsByAccount, useNotification, useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, CreateBuyOrderFunction, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { TokenSelectorItemType } from '@subwallet/extension-koni-ui/types/field'; @@ -80,7 +80,7 @@ function Component ({ className, currentAccountProxy }: ComponentProps) { const checkAsset = useAssetChecker(); const allowedChains = useGetChainSlugsByAccount(); - const getReformatAddress = useReformatAddress(); + const getReformatAddress = useCoreReformatAddress(); const fixedTokenSlug = useMemo((): string | undefined => { if (currentSymbol) { diff --git a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx index ceb8f769282..de3968d508b 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx @@ -8,7 +8,7 @@ import { EarningRewardHistoryItem, YieldPoolType, YieldPositionInfo } from '@sub import { isSameAddress } from '@subwallet/extension-base/utils'; import { CollapsiblePanel, MetaInfo } from '@subwallet/extension-koni-ui/components'; import { ASTAR_PORTAL_URL, BN_ZERO, CLAIM_REWARD_TRANSACTION, DEFAULT_CLAIM_REWARD_PARAMS, EarningStatusUi } from '@subwallet/extension-koni-ui/constants'; -import { useReformatAddress, useSelector, useTranslation, useYieldRewardTotal } from '@subwallet/extension-koni-ui/hooks'; +import { useCoreReformatAddress, useSelector, useTranslation, useYieldRewardTotal } from '@subwallet/extension-koni-ui/hooks'; import { AlertDialogProps, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { customFormatDate, openInNewTab } from '@subwallet/extension-koni-ui/utils'; import { ActivityIndicator, Button, Icon, Number } from '@subwallet/react-ui'; @@ -43,7 +43,7 @@ function Component ({ className, closeAlert, compound, inputAsset, isShowBalance const [, setClaimRewardStorage] = useLocalStorage(CLAIM_REWARD_TRANSACTION, DEFAULT_CLAIM_REWARD_PARAMS); const total = useYieldRewardTotal(slug); - const getReformatAddress = useReformatAddress(); + const getReformatAddress = useCoreReformatAddress(); const isDAppStaking = useMemo(() => _STAKING_CHAIN_GROUP.astar.includes(compound.chain), [compound.chain]); const isMythosStaking = useMemo(() => _STAKING_CHAIN_GROUP.mythos.includes(compound.chain), [compound.chain]); diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx index 3a4c9590389..dbd18666766 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx @@ -18,7 +18,7 @@ import { EarningInstructionModal } from '@subwallet/extension-koni-ui/components import { SlippageModal } from '@subwallet/extension-koni-ui/components/Modal/Swap'; import { EARNING_INSTRUCTION_MODAL, EARNING_SLIPPAGE_MODAL, STAKE_ALERT_DATA } from '@subwallet/extension-koni-ui/constants'; import { MktCampaignModalContext } from '@subwallet/extension-koni-ui/contexts/MktCampaignModalContext'; -import { useChainConnection, useExtensionDisplayModes, useFetchChainState, useGetBalance, useGetNativeTokenSlug, useGetYieldPositionForSpecificAccount, useInitValidateTransaction, useNotification, useOneSignProcess, usePreCheckAction, useReformatAddress, useRestoreTransaction, useSelector, useSidePanelUtils, useTransactionContext, useWatchTransaction, useYieldPositionDetail } from '@subwallet/extension-koni-ui/hooks'; +import { useChainConnection, useCoreReformatAddress, useExtensionDisplayModes, useFetchChainState, useGetBalance, useGetNativeTokenSlug, useGetYieldPositionForSpecificAccount, useInitValidateTransaction, useNotification, useOneSignProcess, usePreCheckAction, useRestoreTransaction, useSelector, useSidePanelUtils, useTransactionContext, useWatchTransaction, useYieldPositionDetail } from '@subwallet/extension-koni-ui/hooks'; import useGetConfirmationByScreen from '@subwallet/extension-koni-ui/hooks/campaign/useGetConfirmationByScreen'; import { fetchPoolTarget, getEarningSlippage, getOptimalYieldPath, submitJoinYieldPool, submitProcess, validateYieldProcess, windowOpen } from '@subwallet/extension-koni-ui/messaging'; import { DEFAULT_YIELD_PROCESS, EarningActionType, earningReducer } from '@subwallet/extension-koni-ui/reducer'; @@ -80,7 +80,7 @@ const Component = () => { const oneSign = useOneSignProcess(fromValue); const nativeTokenSlug = useGetNativeTokenSlug(chainValue); - const getReformatAddress = useReformatAddress(); + const getReformatAddress = useCoreReformatAddress(); const isClickInfoButtonRef = useRef(false); 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 2b28bf4918d..d39549711c7 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -23,7 +23,7 @@ import { _reformatAddressWithChain, detectTranslate, isAccountAll } from '@subwa import { AccountAddressSelector, AddressInputNew, AddressInputRef, AlertBox, AlertBoxInstant, AlertModal, AmountInput, ChainSelector, FeeEditor, HiddenInput, TokenSelector } from '@subwallet/extension-koni-ui/components'; import { ADDRESS_INPUT_AUTO_FORMAT_VALUE } from '@subwallet/extension-koni-ui/constants'; import { MktCampaignModalContext } from '@subwallet/extension-koni-ui/contexts/MktCampaignModalContext'; -import { useAlert, useDefaultNavigate, useFetchChainAssetInfo, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useIsPolkadotUnifiedChain, useNotification, usePreCheckAction, useReformatAddress, useRestoreTransaction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; +import { useAlert, useCoreReformatAddress, useDefaultNavigate, useFetchChainAssetInfo, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useIsPolkadotUnifiedChain, useNotification, usePreCheckAction, useRestoreTransaction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; import useGetConfirmationByScreen from '@subwallet/extension-koni-ui/hooks/campaign/useGetConfirmationByScreen'; import { approveSpending, cancelSubscription, getOptimalTransferProcess, getTokensCanPayFee, isTonBounceableAddress, makeCrossChainTransfer, makeTransfer, subscribeMaxTransfer } from '@subwallet/extension-koni-ui/messaging'; import { CommonActionType, commonProcessReducer, DEFAULT_COMMON_PROCESS } from '@subwallet/extension-koni-ui/reducer'; @@ -158,7 +158,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone const { nativeTokenBalance } = useGetBalance(chainValue, fromValue); const assetInfo = useFetchChainAssetInfo(assetValue); - const getReformatAddress = useReformatAddress(); + const getReformatAddress = useCoreReformatAddress(); const { alertProps, closeAlert, openAlert } = useAlert(alertModalId); const { chainInfoMap, chainStateMap, chainStatusMap, ledgerGenericAllowNetworks, priorityTokens } = useSelector((root) => root.chainStore); diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx index 71fa59a46a6..aa14fe4b3bc 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx @@ -20,7 +20,7 @@ import { SwapFromField, SwapToField } from '@subwallet/extension-koni-ui/compone import { ChooseFeeTokenModal, SlippageModal, SwapIdleWarningModal, SwapQuotesSelectorModal, SwapTermsOfServiceModal } from '@subwallet/extension-koni-ui/components/Modal/Swap'; import { ADDRESS_INPUT_AUTO_FORMAT_VALUE, BN_TEN, BN_ZERO, CONFIRM_SWAP_TERM, SWAP_ALL_QUOTES_MODAL, SWAP_CHOOSE_FEE_TOKEN_MODAL, SWAP_IDLE_WARNING_MODAL, SWAP_SLIPPAGE_MODAL, SWAP_TERMS_OF_SERVICE_MODAL } from '@subwallet/extension-koni-ui/constants'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; -import { useChainConnection, useDefaultNavigate, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useNotification, useOneSignProcess, usePreCheckAction, useReformatAddress, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; +import { useChainConnection, useCoreReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useNotification, useOneSignProcess, usePreCheckAction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; import { submitProcess } from '@subwallet/extension-koni-ui/messaging'; import { handleSwapRequestV2, handleSwapStep, validateSwapProcess } from '@subwallet/extension-koni-ui/messaging/transaction/swap'; import { FreeBalance, TransactionContent, TransactionFooter } from '@subwallet/extension-koni-ui/Popup/Transaction/parts'; @@ -199,7 +199,7 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { const { checkChainConnected, turnOnChain } = useChainConnection(); const onPreCheck = usePreCheckAction(fromValue); const oneSign = useOneSignProcess(fromValue); - const getReformatAddress = useReformatAddress(); + const getReformatAddress = useCoreReformatAddress(); const [processState, dispatchProcessState] = useReducer(commonProcessReducer, DEFAULT_COMMON_PROCESS); const { onError, onSuccess } = useHandleSubmitMultiTransaction(dispatchProcessState); diff --git a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx index 02f0d996b78..b9a8929158c 100644 --- a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx @@ -4,7 +4,7 @@ import { AnalyzeAddress, AnalyzedGroup } from '@subwallet/extension-base/types'; import { _reformatAddressWithChain, getAccountChainTypeForAddress } from '@subwallet/extension-base/utils'; import { AddressSelectorItem, BackIcon } from '@subwallet/extension-koni-ui/components'; -import { useChainInfo, useFilterModal, useReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useChainInfo, useCoreReformatAddress, useFilterModal, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; import { getBitcoinAccountDetails, isAccountAll, isChainInfoAccordantAccountChainType } from '@subwallet/extension-koni-ui/utils'; import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; @@ -58,7 +58,7 @@ const Component: React.FC = (props: Props) => { const chainInfo = useChainInfo(chainSlug); - const getReformatAddress = useReformatAddress(); + const getReformatAddress = useCoreReformatAddress(); const filterModal = useMemo(() => `${id}-filter-modal`, [id]); diff --git a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx index 0d069d0ee03..fafe8c0c66b 100644 --- a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx +++ b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx @@ -6,7 +6,7 @@ import type { KeypairType } from '@subwallet/keyring/types'; import { _ChainInfo } from '@subwallet/chain-list/types'; import { _BITCOIN_CHAIN_SLUG, _BITCOIN_TESTNET_CHAIN_SLUG } from '@subwallet/extension-base/services/chain-service/constants'; import { AccountProxy } from '@subwallet/extension-base/types'; -import { useReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useCoreReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { AccountChainAddress } from '@subwallet/extension-koni-ui/types'; import { getBitcoinAccountDetails, getChainsByAccountType } from '@subwallet/extension-koni-ui/utils'; import { useMemo } from 'react'; @@ -44,7 +44,7 @@ const createChainAddressItem = ( const useGetAccountChainAddresses = (accountProxy: AccountProxy): AccountChainAddress[] => { const chainInfoMap = useSelector((state) => state.chainStore.chainInfoMap); - const getReformatAddress = useReformatAddress(); + const getReformatAddress = useCoreReformatAddress(); return useMemo(() => { const result: AccountChainAddress[] = []; diff --git a/packages/extension-koni-ui/src/hooks/common/index.ts b/packages/extension-koni-ui/src/hooks/common/index.ts index cddb54a6d78..4c0557c3318 100644 --- a/packages/extension-koni-ui/src/hooks/common/index.ts +++ b/packages/extension-koni-ui/src/hooks/common/index.ts @@ -16,7 +16,7 @@ export { default as useSetSessionLatest } from './useSetSessionLatest'; export { default as useDebouncedValue } from './useDebouncedValue'; export { default as useIsPolkadotUnifiedChain } from './useIsPolkadotUnifiedChain'; export { default as useGetBitcoinAccounts } from './useGetBitcoinAccounts'; -export { default as useReformatAddress } from './useReformatAddress'; +export { default as useCoreReformatAddress } from './useCoreReformatAddress'; export * from './useSelector'; export * from './useLazyList'; diff --git a/packages/extension-koni-ui/src/hooks/common/useReformatAddress.tsx b/packages/extension-koni-ui/src/hooks/common/useCoreReformatAddress.tsx similarity index 87% rename from packages/extension-koni-ui/src/hooks/common/useReformatAddress.tsx rename to packages/extension-koni-ui/src/hooks/common/useCoreReformatAddress.tsx index b9c0a0d967b..a19272206ca 100644 --- a/packages/extension-koni-ui/src/hooks/common/useReformatAddress.tsx +++ b/packages/extension-koni-ui/src/hooks/common/useCoreReformatAddress.tsx @@ -6,10 +6,10 @@ import { AccountJson } from '@subwallet/extension-base/types'; import { getReformatedAddressRelatedToChain } from '@subwallet/extension-koni-ui/utils'; import { useCallback } from 'react'; -const useReformatAddress = () => { +const useCoreReformatAddress = () => { return useCallback((accountJson: AccountJson, chainInfo: _ChainInfo): string | undefined => { return getReformatedAddressRelatedToChain(accountJson, chainInfo); }, []); }; -export default useReformatAddress; +export default useCoreReformatAddress; diff --git a/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx b/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx index f8e046f8847..cb934533734 100644 --- a/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx +++ b/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AccountProxy } from '@subwallet/extension-base/types'; -import { useChainInfoWithState, useGetChainSlugsByAccount, useReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useChainInfoWithState, useCoreReformatAddress, useGetChainSlugsByAccount, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { AccountAddressItemType, ChainItemType } from '@subwallet/extension-koni-ui/types'; import { isAccountAll } from '@subwallet/extension-koni-ui/utils'; import { useEffect, useMemo, useState } from 'react'; @@ -13,7 +13,7 @@ export default function useHistorySelection () { const { chainInfoMap } = useSelector((root) => root.chainStore); const chainInfoList = useChainInfoWithState(); const allowedChains = useGetChainSlugsByAccount(); - const getReformatAddress = useReformatAddress(); + const getReformatAddress = useCoreReformatAddress(); const { accountProxies, currentAccountProxy } = useSelector((root) => root.accountState); const [selectedAddress, setSelectedAddress] = useState(propAddress || ''); diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx index e038818affe..29f9ac82f12 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx +++ b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx @@ -7,7 +7,7 @@ import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/c import { AccountActions, AccountProxyType } from '@subwallet/extension-base/types'; import { RECEIVE_MODAL_ACCOUNT_SELECTOR, RECEIVE_MODAL_TOKEN_SELECTOR } from '@subwallet/extension-koni-ui/constants'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import { useGetBitcoinAccounts, useGetChainSlugsByAccount, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useReformatAddress } from '@subwallet/extension-koni-ui/hooks'; +import { useCoreReformatAddress, useGetBitcoinAccounts, useGetChainSlugsByAccount, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain } from '@subwallet/extension-koni-ui/hooks'; import { useChainAssets } from '@subwallet/extension-koni-ui/hooks/assets'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, AccountTokenAddress, ReceiveModalProps } from '@subwallet/extension-koni-ui/types'; @@ -39,7 +39,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const chainSupported = useGetChainSlugsByAccount(); const onHandleTonAccountWarning = useHandleTonAccountWarning(); const onHandleLedgerGenericAccountWarning = useHandleLedgerGenericAccountWarning(); - const getReformatAddress = useReformatAddress(); + const getReformatAddress = useCoreReformatAddress(); const checkIsPolkadotUnifiedChain = useIsPolkadotUnifiedChain(); const getBitcoinAccount = useGetBitcoinAccounts(); From 1a4f5849c5ff3e229469dc8525a4325011ca28e4 Mon Sep 17 00:00:00 2001 From: lw Date: Tue, 3 Jun 2025 11:15:12 +0700 Subject: [PATCH 107/178] [Issue-4297] feat: Init some functions and hooks --- packages/extension-koni-ui/src/hooks/chain/index.ts | 4 ++++ .../chain/useCoreGetChainSlugsByAccountProxy.tsx | 11 +++++++++++ .../useCoreIsChainInfoCompatibleWithAccountProxy.tsx | 10 ++++++++++ .../chain/useGetChainSlugsByCurrentAccountProxy.tsx | 8 ++++++++ packages/extension-koni-ui/src/utils/chain/chain.ts | 4 ++++ 5 files changed, 37 insertions(+) create mode 100644 packages/extension-koni-ui/src/hooks/chain/useCoreGetChainSlugsByAccountProxy.tsx create mode 100644 packages/extension-koni-ui/src/hooks/chain/useCoreIsChainInfoCompatibleWithAccountProxy.tsx create mode 100644 packages/extension-koni-ui/src/hooks/chain/useGetChainSlugsByCurrentAccountProxy.tsx diff --git a/packages/extension-koni-ui/src/hooks/chain/index.ts b/packages/extension-koni-ui/src/hooks/chain/index.ts index 6d4253fcdbc..753912245bf 100644 --- a/packages/extension-koni-ui/src/hooks/chain/index.ts +++ b/packages/extension-koni-ui/src/hooks/chain/index.ts @@ -12,3 +12,7 @@ export { default as useGetChainInfoByGenesisHash } from './useGetChainInfoByGene export { default as useGetChainPrefixBySlug } from './useGetChainPrefixBySlug'; export { default as useChainInfoData } from './useChainInfoData'; export { default as useChainConnection } from './useChainConnection'; + +export { default as useCoreGetChainSlugsByAccountProxy } from './useCoreGetChainSlugsByAccountProxy'; +export { default as useCoreIsChainInfoCompatibleWithAccountProxy } from './useCoreIsChainInfoCompatibleWithAccountProxy'; +export { default as useGetChainSlugsByCurrentAccountProxy } from './useGetChainSlugsByCurrentAccountProxy'; diff --git a/packages/extension-koni-ui/src/hooks/chain/useCoreGetChainSlugsByAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useCoreGetChainSlugsByAccountProxy.tsx new file mode 100644 index 00000000000..614d4a8bf0e --- /dev/null +++ b/packages/extension-koni-ui/src/hooks/chain/useCoreGetChainSlugsByAccountProxy.tsx @@ -0,0 +1,11 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { _ChainInfo } from '@subwallet/chain-list/types'; +import { AccountProxy } from '@subwallet/extension-base/types'; + +const useCoreIsChainInfoCompatibleWithAccountProxy = (chainInfo: _ChainInfo, accountProxies: AccountProxy): boolean => { + return false; +}; + +export default useCoreIsChainInfoCompatibleWithAccountProxy; diff --git a/packages/extension-koni-ui/src/hooks/chain/useCoreIsChainInfoCompatibleWithAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useCoreIsChainInfoCompatibleWithAccountProxy.tsx new file mode 100644 index 00000000000..d6f1b08d40b --- /dev/null +++ b/packages/extension-koni-ui/src/hooks/chain/useCoreIsChainInfoCompatibleWithAccountProxy.tsx @@ -0,0 +1,10 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { AccountProxy } from '@subwallet/extension-base/types'; + +const useCoreIsChainInfoCompatibleWithAccountProxy = (accountProxies: AccountProxy): string[] => { + return []; +}; + +export default useCoreIsChainInfoCompatibleWithAccountProxy; diff --git a/packages/extension-koni-ui/src/hooks/chain/useGetChainSlugsByCurrentAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useGetChainSlugsByCurrentAccountProxy.tsx new file mode 100644 index 00000000000..dbc0de079f1 --- /dev/null +++ b/packages/extension-koni-ui/src/hooks/chain/useGetChainSlugsByCurrentAccountProxy.tsx @@ -0,0 +1,8 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +const useGetChainSlugsByCurrentAccountProxy = (): string[] => { + return []; +}; + +export default useGetChainSlugsByCurrentAccountProxy; diff --git a/packages/extension-koni-ui/src/utils/chain/chain.ts b/packages/extension-koni-ui/src/utils/chain/chain.ts index 6b6e411265f..67578c356d6 100644 --- a/packages/extension-koni-ui/src/utils/chain/chain.ts +++ b/packages/extension-koni-ui/src/utils/chain/chain.ts @@ -59,6 +59,10 @@ export const isChainInfoAccordantAccountChainType = (chainInfo: _ChainInfo, chai return false; }; +export const isChainInfoCompatibleWithAccountInfo = (chainInfo: _ChainInfo, accountChainType: AccountChainType, AccountType: KeypairType): boolean => { + return false; +}; + export const isChainCompatibleWithAccountChainTypes = (chainInfo: _ChainInfo, chainTypes: AccountChainType[]): boolean => { return chainTypes.some((chainType) => isChainInfoAccordantAccountChainType(chainInfo, chainType)); }; From fd514a16c68bf2370c9e86ea724193d0aa82e0c8 Mon Sep 17 00:00:00 2001 From: lw Date: Tue, 3 Jun 2025 11:27:28 +0700 Subject: [PATCH 108/178] [Issue-4297] refactor: Implement useGetChainSlugsByCurrentAccountProxy --- packages/extension-koni-ui/src/Popup/BuyTokens.tsx | 4 ++-- .../extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx | 4 ++-- packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx | 4 ++-- packages/extension-koni-ui/src/Popup/Home/index.tsx | 4 ++-- .../src/Popup/Settings/Notifications/Notification.tsx | 5 ++--- .../hooks/earning/useGetYieldPositionForSpecificAccount.ts | 4 ++-- .../src/hooks/earning/useGroupYieldPosition.ts | 4 ++-- .../src/hooks/earning/useYieldGroupInfo.ts | 4 ++-- .../src/hooks/earning/useYieldPoolInfoByGroup.ts | 4 ++-- .../src/hooks/earning/useYieldPositionDetail.ts | 4 ++-- .../src/hooks/earning/useYieldRewardTotal.ts | 4 ++-- .../src/hooks/history/useHistorySelection.tsx | 4 ++-- .../src/hooks/screen/home/useCoreReceiveModalHelper.tsx | 4 ++-- .../src/hooks/screen/home/useGetTokensBySettings.ts | 6 +++--- 14 files changed, 29 insertions(+), 30 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/BuyTokens.tsx b/packages/extension-koni-ui/src/Popup/BuyTokens.tsx index 7e29a5122af..7063a0094d1 100644 --- a/packages/extension-koni-ui/src/Popup/BuyTokens.tsx +++ b/packages/extension-koni-ui/src/Popup/BuyTokens.tsx @@ -8,7 +8,7 @@ import { detectTranslate, isAccountAll } from '@subwallet/extension-base/utils'; import { AccountAddressSelector, baseServiceItems, Layout, PageWrapper, ServiceItem } from '@subwallet/extension-koni-ui/components'; import { ServiceSelector } from '@subwallet/extension-koni-ui/components/Field/BuyTokens/ServiceSelector'; import { TokenSelector } from '@subwallet/extension-koni-ui/components/Field/TokenSelector'; -import { useAssetChecker, useCoreReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetChainSlugsByAccount, useNotification, useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { useAssetChecker, useCoreReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetChainSlugsByCurrentAccountProxy, useNotification, useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, CreateBuyOrderFunction, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { TokenSelectorItemType } from '@subwallet/extension-koni-ui/types/field'; @@ -79,7 +79,7 @@ function Component ({ className, currentAccountProxy }: ComponentProps) { const getAccountTokenBalance = useGetAccountTokenBalance(); const checkAsset = useAssetChecker(); - const allowedChains = useGetChainSlugsByAccount(); + const allowedChains = useGetChainSlugsByCurrentAccountProxy(); const getReformatAddress = useCoreReformatAddress(); const fixedTokenSlug = useMemo((): string | undefined => { diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx index 5fd14d490b8..f9cf9b9b79e 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx @@ -12,7 +12,7 @@ import { TokenBalanceDetailItem } from '@subwallet/extension-koni-ui/components/ import { DEFAULT_SWAP_PARAMS, DEFAULT_TRANSFER_PARAMS, IS_SHOW_TON_CONTRACT_VERSION_WARNING, SWAP_TRANSACTION, TON_ACCOUNT_SELECTOR_MODAL, TON_WALLET_CONTRACT_SELECTOR_MODAL, TRANSFER_TRANSACTION } from '@subwallet/extension-koni-ui/constants'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; import { HomeContext } from '@subwallet/extension-koni-ui/contexts/screen/HomeContext'; -import { useCoreReceiveModalHelper, useDefaultNavigate, useGetBannerByScreen, useGetChainSlugsByAccount, useNavigateOnChangeAccount, useNotification, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useCoreReceiveModalHelper, useDefaultNavigate, useGetBannerByScreen, useGetChainSlugsByCurrentAccountProxy, useNavigateOnChangeAccount, useNotification, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { canShowChart } from '@subwallet/extension-koni-ui/messaging'; import { DetailModal } from '@subwallet/extension-koni-ui/Popup/Home/Tokens/DetailModal'; import { DetailUpperBlock } from '@subwallet/extension-koni-ui/Popup/Home/Tokens/DetailUpperBlock'; @@ -76,7 +76,7 @@ function Component (): React.ReactElement { const [, setStorage] = useLocalStorage(TRANSFER_TRANSACTION, DEFAULT_TRANSFER_PARAMS); const [, setSwapStorage] = useLocalStorage(SWAP_TRANSACTION, DEFAULT_SWAP_PARAMS); const { banners, dismissBanner, onClickBanner } = useGetBannerByScreen('token_detail', tokenGroupSlug); - const allowedChains = useGetChainSlugsByAccount(); + const allowedChains = useGetChainSlugsByCurrentAccountProxy(); const isTonWalletContactSelectorModalActive = checkActive(tonWalletContractSelectorModalId); const [isShowTonWarning, setIsShowTonWarning] = useLocalStorage(IS_SHOW_TON_CONTRACT_VERSION_WARNING, true); const tonAddress = useMemo(() => { diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx index 494a41c840a..ce4d080af7f 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx @@ -9,7 +9,7 @@ import { TokenGroupBalanceItem } from '@subwallet/extension-koni-ui/components/T import { DEFAULT_SWAP_PARAMS, DEFAULT_TRANSFER_PARAMS, IS_SHOW_TON_CONTRACT_VERSION_WARNING, SWAP_TRANSACTION, TON_ACCOUNT_SELECTOR_MODAL, TON_WALLET_CONTRACT_SELECTOR_MODAL, TRANSFER_TRANSACTION } from '@subwallet/extension-koni-ui/constants'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; import { HomeContext } from '@subwallet/extension-koni-ui/contexts/screen/HomeContext'; -import { useCoreReceiveModalHelper, useDebouncedValue, useGetBannerByScreen, useGetChainSlugsByAccount, useSetCurrentPage } from '@subwallet/extension-koni-ui/hooks'; +import { useCoreReceiveModalHelper, useDebouncedValue, useGetBannerByScreen, useGetChainSlugsByCurrentAccountProxy, useSetCurrentPage } from '@subwallet/extension-koni-ui/hooks'; import useNotification from '@subwallet/extension-koni-ui/hooks/common/useNotification'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; import { UpperBlock } from '@subwallet/extension-koni-ui/Popup/Home/Tokens/UpperBlock'; @@ -54,7 +54,7 @@ const Component = (): React.ReactElement => { const [, setStorage] = useLocalStorage(TRANSFER_TRANSACTION, DEFAULT_TRANSFER_PARAMS); const [, setSwapStorage] = useLocalStorage(SWAP_TRANSACTION, DEFAULT_SWAP_PARAMS); const { banners, dismissBanner, onClickBanner } = useGetBannerByScreen('token'); - const allowedChains = useGetChainSlugsByAccount(); + const allowedChains = useGetChainSlugsByCurrentAccountProxy(); const buyTokenInfos = useSelector((state: RootState) => state.buyService.tokens); const { activeModal, checkActive, inactiveModal } = useContext(ModalContext); const isTonWalletContactSelectorModalActive = checkActive(tonWalletContractSelectorModalId); diff --git a/packages/extension-koni-ui/src/Popup/Home/index.tsx b/packages/extension-koni-ui/src/Popup/Home/index.tsx index fb57c1d2cd8..11b03078d59 100644 --- a/packages/extension-koni-ui/src/Popup/Home/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/index.tsx @@ -8,7 +8,7 @@ import { GeneralTermModal } from '@subwallet/extension-koni-ui/components/Modal/ import { CONFIRM_GENERAL_TERM, DEFAULT_SESSION_VALUE, GENERAL_TERM_AND_CONDITION_MODAL, HOME_CAMPAIGN_BANNER_MODAL, LATEST_SESSION, REMIND_BACKUP_SEED_PHRASE_MODAL, REMIND_UPGRADE_FIREFOX_VERSION } from '@subwallet/extension-koni-ui/constants'; import { AppOnlineContentContext } from '@subwallet/extension-koni-ui/contexts/AppOnlineContentProvider'; import { HomeContext } from '@subwallet/extension-koni-ui/contexts/screen/HomeContext'; -import { useAccountBalance, useGetChainSlugsByAccount, useSetSessionLatest, useTokenGroup, useUpgradeFireFoxVersion } from '@subwallet/extension-koni-ui/hooks'; +import { useAccountBalance, useGetChainSlugsByCurrentAccountProxy, useSetSessionLatest, useTokenGroup, useUpgradeFireFoxVersion } from '@subwallet/extension-koni-ui/hooks'; import { RemindBackUpSeedPhraseParamState, SessionStorage, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { isFirefox } from '@subwallet/extension-koni-ui/utils'; import { ModalContext } from '@subwallet/react-ui'; @@ -26,7 +26,7 @@ const historyPageIgnoreBanner = 'ignoreBanner'; function Component ({ className = '' }: Props): React.ReactElement { const { activeModal, inactiveModal } = useContext(ModalContext); - const chainsByAccountType = useGetChainSlugsByAccount(); + const chainsByAccountType = useGetChainSlugsByCurrentAccountProxy(); const tokenGroupStructure = useTokenGroup(chainsByAccountType); const location = useLocation(); const accountBalance = useAccountBalance(tokenGroupStructure.tokenGroupMap); diff --git a/packages/extension-koni-ui/src/Popup/Settings/Notifications/Notification.tsx b/packages/extension-koni-ui/src/Popup/Settings/Notifications/Notification.tsx index e6d044eda4c..90d287a34e6 100644 --- a/packages/extension-koni-ui/src/Popup/Settings/Notifications/Notification.tsx +++ b/packages/extension-koni-ui/src/Popup/Settings/Notifications/Notification.tsx @@ -4,7 +4,6 @@ import { COMMON_CHAIN_SLUGS } from '@subwallet/chain-list'; import { NotificationType } from '@subwallet/extension-base/background/KoniTypes'; import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; -import { _POLYGON_BRIDGE_ABI } from '@subwallet/extension-base/koni/api/contract-handler/utils'; import { isClaimedPosBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/posBridge'; import { _NotificationInfo, BridgeTransactionStatus, ClaimAvailBridgeNotificationMetadata, ClaimPolygonBridgeNotificationMetadata, NotificationActionType, NotificationSetup, NotificationTab, ProcessNotificationMetadata, WithdrawClaimNotificationMetadata } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; import { GetNotificationParams, RequestSwitchStatusParams } from '@subwallet/extension-base/types/notification'; @@ -16,7 +15,7 @@ import Search from '@subwallet/extension-koni-ui/components/Search'; import { BN_ZERO, CLAIM_BRIDGE_TRANSACTION, CLAIM_REWARD_TRANSACTION, DEFAULT_CLAIM_AVAIL_BRIDGE_PARAMS, DEFAULT_CLAIM_REWARD_PARAMS, DEFAULT_UN_STAKE_PARAMS, DEFAULT_WITHDRAW_PARAMS, NOTIFICATION_DETAIL_MODAL, WITHDRAW_TRANSACTION } from '@subwallet/extension-koni-ui/constants'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import { useAlert, useDefaultNavigate, useGetChainSlugsByAccount, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useAlert, useDefaultNavigate, useGetChainSlugsByCurrentAccountProxy, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { useLocalStorage } from '@subwallet/extension-koni-ui/hooks/common/useLocalStorage'; import { enableChain, saveNotificationSetup } from '@subwallet/extension-koni-ui/messaging'; import { fetchInappNotifications, getIsClaimNotificationStatus, markAllReadNotification, switchReadNotificationStatus } from '@subwallet/extension-koni-ui/messaging/transaction/notification'; @@ -80,7 +79,7 @@ function Component ({ className = '' }: Props): React.ReactElement { const { goHome } = useDefaultNavigate(); const { token } = useTheme() as Theme; const { alertProps, closeAlert, openAlert, updateAlertProps } = useAlert(alertModalId); - const chainsByAccountType = useGetChainSlugsByAccount(); + const chainsByAccountType = useGetChainSlugsByCurrentAccountProxy(); const [, setClaimRewardStorage] = useLocalStorage(CLAIM_REWARD_TRANSACTION, DEFAULT_CLAIM_REWARD_PARAMS); const [, setWithdrawStorage] = useLocalStorage(WITHDRAW_TRANSACTION, DEFAULT_WITHDRAW_PARAMS); diff --git a/packages/extension-koni-ui/src/hooks/earning/useGetYieldPositionForSpecificAccount.ts b/packages/extension-koni-ui/src/hooks/earning/useGetYieldPositionForSpecificAccount.ts index dbfdadb510e..ad168a8be3e 100644 --- a/packages/extension-koni-ui/src/hooks/earning/useGetYieldPositionForSpecificAccount.ts +++ b/packages/extension-koni-ui/src/hooks/earning/useGetYieldPositionForSpecificAccount.ts @@ -3,7 +3,7 @@ import { YieldPositionInfo } from '@subwallet/extension-base/types'; import { isSameAddress } from '@subwallet/extension-base/utils'; -import { useGetChainSlugsByAccount, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useGetChainSlugsByCurrentAccountProxy, useSelector } from '@subwallet/extension-koni-ui/hooks'; import BigN from 'bignumber.js'; import { useMemo } from 'react'; @@ -12,7 +12,7 @@ const useGetYieldPositionForSpecificAccount = (address?: string): YieldPositionI const yieldPositions = useSelector((state) => state.earning.yieldPositions); const isAllAccount = useSelector((state) => state.accountState.isAllAccount); const currentAccountProxy = useSelector((state) => state.accountState.currentAccountProxy); - const chainsByAccountType = useGetChainSlugsByAccount(); + const chainsByAccountType = useGetChainSlugsByCurrentAccountProxy(); return useMemo(() => { const infoSpecificList: YieldPositionInfo[] = []; diff --git a/packages/extension-koni-ui/src/hooks/earning/useGroupYieldPosition.ts b/packages/extension-koni-ui/src/hooks/earning/useGroupYieldPosition.ts index 8e48e0ae5af..d63c5f3fb0a 100644 --- a/packages/extension-koni-ui/src/hooks/earning/useGroupYieldPosition.ts +++ b/packages/extension-koni-ui/src/hooks/earning/useGroupYieldPosition.ts @@ -4,7 +4,7 @@ import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; import { AbstractYieldPositionInfo, EarningStatus, LendingYieldPositionInfo, LiquidYieldPositionInfo, NativeYieldPositionInfo, NominationYieldPositionInfo, SubnetYieldPositionInfo, YieldPoolType, YieldPositionInfo } from '@subwallet/extension-base/types'; import { reformatAddress } from '@subwallet/extension-base/utils'; -import { useGetChainSlugsByAccount, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useGetChainSlugsByCurrentAccountProxy, useSelector } from '@subwallet/extension-koni-ui/hooks'; import BigN from 'bignumber.js'; import { useMemo } from 'react'; @@ -12,7 +12,7 @@ const useGroupYieldPosition = (): YieldPositionInfo[] => { const poolInfoMap = useSelector((state) => state.earning.poolInfoMap); const yieldPositions = useSelector((state) => state.earning.yieldPositions); const { currentAccountProxy, isAllAccount } = useSelector((state) => state.accountState); - const chainsByAccountType = useGetChainSlugsByAccount(); + const chainsByAccountType = useGetChainSlugsByCurrentAccountProxy(); return useMemo(() => { const result: YieldPositionInfo[] = []; diff --git a/packages/extension-koni-ui/src/hooks/earning/useYieldGroupInfo.ts b/packages/extension-koni-ui/src/hooks/earning/useYieldGroupInfo.ts index 20f352f8970..463412f2613 100644 --- a/packages/extension-koni-ui/src/hooks/earning/useYieldGroupInfo.ts +++ b/packages/extension-koni-ui/src/hooks/earning/useYieldGroupInfo.ts @@ -4,7 +4,7 @@ import { calculateReward } from '@subwallet/extension-base/services/earning-service/utils'; import { YieldPoolType } from '@subwallet/extension-base/types'; import { BN_ZERO } from '@subwallet/extension-koni-ui/constants'; -import { useAccountBalance, useGetChainSlugsByAccount, useSelector, useTokenGroup } from '@subwallet/extension-koni-ui/hooks'; +import { useAccountBalance, useGetChainSlugsByCurrentAccountProxy, useSelector, useTokenGroup } from '@subwallet/extension-koni-ui/hooks'; import { BalanceValueInfo, YieldGroupInfo } from '@subwallet/extension-koni-ui/types'; import { useMemo } from 'react'; @@ -12,7 +12,7 @@ const useYieldGroupInfo = (): YieldGroupInfo[] => { const poolInfoMap = useSelector((state) => state.earning.poolInfoMap); const { assetRegistry, multiChainAssetMap } = useSelector((state) => state.assetRegistry); const chainInfoMap = useSelector((state) => state.chainStore.chainInfoMap); - const chainsByAccountType = useGetChainSlugsByAccount(); + const chainsByAccountType = useGetChainSlugsByCurrentAccountProxy(); const { tokenGroupMap } = useTokenGroup(chainsByAccountType); const { tokenBalanceMap } = useAccountBalance(tokenGroupMap, true); diff --git a/packages/extension-koni-ui/src/hooks/earning/useYieldPoolInfoByGroup.ts b/packages/extension-koni-ui/src/hooks/earning/useYieldPoolInfoByGroup.ts index bb86d0516ef..af60a0c59b6 100644 --- a/packages/extension-koni-ui/src/hooks/earning/useYieldPoolInfoByGroup.ts +++ b/packages/extension-koni-ui/src/hooks/earning/useYieldPoolInfoByGroup.ts @@ -2,12 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { YieldPoolInfo } from '@subwallet/extension-base/types'; -import { useGetChainSlugsByAccount, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useGetChainSlugsByCurrentAccountProxy, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { useMemo } from 'react'; const useYieldPoolInfoByGroup = (group: string): YieldPoolInfo[] => { const { poolInfoMap } = useSelector((state) => state.earning); - const chainsByAccountType = useGetChainSlugsByAccount(); + const chainsByAccountType = useGetChainSlugsByCurrentAccountProxy(); return useMemo(() => { const result: YieldPoolInfo[] = []; diff --git a/packages/extension-koni-ui/src/hooks/earning/useYieldPositionDetail.ts b/packages/extension-koni-ui/src/hooks/earning/useYieldPositionDetail.ts index 5844e332edc..6cf2850f0d7 100644 --- a/packages/extension-koni-ui/src/hooks/earning/useYieldPositionDetail.ts +++ b/packages/extension-koni-ui/src/hooks/earning/useYieldPositionDetail.ts @@ -4,7 +4,7 @@ import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; import { AbstractYieldPositionInfo, EarningStatus, LendingYieldPositionInfo, LiquidYieldPositionInfo, NativeYieldPositionInfo, NominationYieldPositionInfo, SubnetYieldPositionInfo, YieldPoolType, YieldPositionInfo } from '@subwallet/extension-base/types'; import { isSameAddress } from '@subwallet/extension-base/utils'; -import { useGetChainSlugsByAccount, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useGetChainSlugsByCurrentAccountProxy, useSelector } from '@subwallet/extension-koni-ui/hooks'; import BigN from 'bignumber.js'; import { useMemo } from 'react'; @@ -17,7 +17,7 @@ interface Result { const useYieldPositionDetail = (slug: string, address?: string): Result => { const { poolInfoMap, yieldPositions } = useSelector((state) => state.earning); const { currentAccountProxy, isAllAccount } = useSelector((state) => state.accountState); - const chainsByAccountType = useGetChainSlugsByAccount(); + const chainsByAccountType = useGetChainSlugsByCurrentAccountProxy(); return useMemo(() => { const checkAddress = (item: YieldPositionInfo) => { diff --git a/packages/extension-koni-ui/src/hooks/earning/useYieldRewardTotal.ts b/packages/extension-koni-ui/src/hooks/earning/useYieldRewardTotal.ts index 66825407564..01292ae0e7f 100644 --- a/packages/extension-koni-ui/src/hooks/earning/useYieldRewardTotal.ts +++ b/packages/extension-koni-ui/src/hooks/earning/useYieldRewardTotal.ts @@ -5,14 +5,14 @@ import { _STAKING_CHAIN_GROUP } from '@subwallet/extension-base/services/earning import { EarningRewardItem, YieldPoolType } from '@subwallet/extension-base/types'; import { isSameAddress } from '@subwallet/extension-base/utils'; import { BN_ZERO } from '@subwallet/extension-koni-ui/constants'; -import { useGetChainSlugsByAccount, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useGetChainSlugsByCurrentAccountProxy, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { findAccountByAddress } from '@subwallet/extension-koni-ui/utils'; import { useMemo } from 'react'; const useYieldRewardTotal = (slug: string): string | undefined => { const { earningRewards, poolInfoMap } = useSelector((state) => state.earning); const { accounts, currentAccountProxy, isAllAccount } = useSelector((state) => state.accountState); - const chainsByAccountType = useGetChainSlugsByAccount(); + const chainsByAccountType = useGetChainSlugsByCurrentAccountProxy(); return useMemo(() => { const checkAddress = (item: EarningRewardItem) => { diff --git a/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx b/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx index cb934533734..14639b72cc1 100644 --- a/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx +++ b/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AccountProxy } from '@subwallet/extension-base/types'; -import { useChainInfoWithState, useCoreReformatAddress, useGetChainSlugsByAccount, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useChainInfoWithState, useCoreReformatAddress, useGetChainSlugsByCurrentAccountProxy, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { AccountAddressItemType, ChainItemType } from '@subwallet/extension-koni-ui/types'; import { isAccountAll } from '@subwallet/extension-koni-ui/utils'; import { useEffect, useMemo, useState } from 'react'; @@ -12,7 +12,7 @@ export default function useHistorySelection () { const { address: propAddress, chain: propChain } = useParams<{address: string, chain: string}>(); const { chainInfoMap } = useSelector((root) => root.chainStore); const chainInfoList = useChainInfoWithState(); - const allowedChains = useGetChainSlugsByAccount(); + const allowedChains = useGetChainSlugsByCurrentAccountProxy(); const getReformatAddress = useCoreReformatAddress(); const { accountProxies, currentAccountProxy } = useSelector((root) => root.accountState); diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx index 29f9ac82f12..87e627cb81b 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx +++ b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx @@ -7,7 +7,7 @@ import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/c import { AccountActions, AccountProxyType } from '@subwallet/extension-base/types'; import { RECEIVE_MODAL_ACCOUNT_SELECTOR, RECEIVE_MODAL_TOKEN_SELECTOR } from '@subwallet/extension-koni-ui/constants'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import { useCoreReformatAddress, useGetBitcoinAccounts, useGetChainSlugsByAccount, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain } from '@subwallet/extension-koni-ui/hooks'; +import { useCoreReformatAddress, useGetBitcoinAccounts, useGetChainSlugsByCurrentAccountProxy, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain } from '@subwallet/extension-koni-ui/hooks'; import { useChainAssets } from '@subwallet/extension-koni-ui/hooks/assets'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, AccountTokenAddress, ReceiveModalProps } from '@subwallet/extension-koni-ui/types'; @@ -36,7 +36,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const [selectedChain, setSelectedChain] = useState(); const [selectedAccountAddressItem, setSelectedAccountAddressItem] = useState(); const { accountTokenAddressModal, addressQrModal, selectAddressFormatModal } = useContext(WalletModalContext); - const chainSupported = useGetChainSlugsByAccount(); + const chainSupported = useGetChainSlugsByCurrentAccountProxy(); const onHandleTonAccountWarning = useHandleTonAccountWarning(); const onHandleLedgerGenericAccountWarning = useHandleLedgerGenericAccountWarning(); const getReformatAddress = useCoreReformatAddress(); diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useGetTokensBySettings.ts b/packages/extension-koni-ui/src/hooks/screen/home/useGetTokensBySettings.ts index 58a9332e4ff..f28843e71e2 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useGetTokensBySettings.ts +++ b/packages/extension-koni-ui/src/hooks/screen/home/useGetTokensBySettings.ts @@ -3,17 +3,17 @@ import { _ChainAsset } from '@subwallet/chain-list/types'; import { _isAssetFungibleToken } from '@subwallet/extension-base/services/chain-service/utils'; -import { useGetChainSlugsByAccount } from '@subwallet/extension-koni-ui/hooks/screen/home/useGetChainSlugsByAccount'; +import { useGetChainSlugsByCurrentAccountProxy } from '@subwallet/extension-koni-ui/hooks'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; // Get all fungible tokens by active chains, visible tokens and current account -export default function useGetTokensBySettings (address?: string): Record { +export default function useGetTokensBySettings (): Record { const chainStateMap = useSelector((state: RootState) => state.chainStore.chainStateMap); const chainAssetMap = useSelector((state: RootState) => state.assetRegistry.assetRegistry); const assetSettingMap = useSelector((state: RootState) => state.assetRegistry.assetSettingMap); - const filteredChainSlugs = useGetChainSlugsByAccount(address); + const filteredChainSlugs = useGetChainSlugsByCurrentAccountProxy(); return useMemo>(() => { const filteredChainAssetMap: Record = {}; From f96a89bf05e591053468d9ee878db0824d0c4caa Mon Sep 17 00:00:00 2001 From: lw Date: Tue, 3 Jun 2025 15:25:15 +0700 Subject: [PATCH 109/178] [Issue-4297] refactor: Implement useCoreGetChainSlugsByAccountProxy, useCoreIsChainInfoCompatibleWithAccountProxy, useGetChainSlugsByCurrentAccountProxy --- .../Popup/Transaction/variants/SendFund.tsx | 77 +++++++------------ .../Popup/Transaction/variants/Swap/index.tsx | 35 +++++---- .../account/useGetAccountChainAddresses.tsx | 9 ++- .../src/hooks/chain/index.ts | 4 +- ...eCoreCreateGetChainSlugsByAccountProxy.tsx | 13 ++++ ...eIsChainInfoCompatibleWithAccountProxy.tsx | 14 ++++ .../useCoreGetChainSlugsByAccountProxy.tsx | 11 --- ...eIsChainInfoCompatibleWithAccountProxy.tsx | 10 --- .../src/utils/chain/chainAndAsset.ts | 21 +---- 9 files changed, 84 insertions(+), 110 deletions(-) create mode 100644 packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx create mode 100644 packages/extension-koni-ui/src/hooks/chain/useCoreCreateIsChainInfoCompatibleWithAccountProxy.tsx delete mode 100644 packages/extension-koni-ui/src/hooks/chain/useCoreGetChainSlugsByAccountProxy.tsx delete mode 100644 packages/extension-koni-ui/src/hooks/chain/useCoreIsChainInfoCompatibleWithAccountProxy.tsx 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 d39549711c7..08477da30bf 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -1,7 +1,7 @@ // Copyright 2019-2022 @polkadot/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; +import { _AssetRef, _AssetType, _ChainInfo } from '@subwallet/chain-list/types'; import { ExtrinsicType, NotificationType } from '@subwallet/extension-base/background/KoniTypes'; import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning'; import { validateRecipientAddress } from '@subwallet/extension-base/core/logic-validation/recipientAddress'; @@ -30,7 +30,7 @@ import { CommonActionType, commonProcessReducer, DEFAULT_COMMON_PROCESS } from ' import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, ChainItemType, FormCallbacks, Theme, ThemeProps, TransferParams } from '@subwallet/extension-koni-ui/types'; import { TokenSelectorItemType } from '@subwallet/extension-koni-ui/types/field'; -import { findAccountByAddress, formatBalance, getChainsByAccountAll, getChainsByAccountType, noop, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; +import { findAccountByAddress, formatBalance, noop, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; import { Button, Form, Icon } from '@subwallet/react-ui'; import { Rule } from '@subwallet/react-ui/es/form'; import BigN from 'bignumber.js'; @@ -43,6 +43,7 @@ import { useIsFirstRender, useLocalStorage } from 'usehooks-ts'; import { BN, BN_ZERO } from '@polkadot/util'; +import useCoreCreateGetChainSlugsByAccountProxy from '../../../hooks/chain/useCoreCreateGetChainSlugsByAccountProxy'; import { FreeBalance, TransactionContent, TransactionFooter } from '../parts'; type WrapperProps = ThemeProps; @@ -60,44 +61,6 @@ interface TransferOptions { type SortableTokenSelectorItemType = TokenSelectorItemType & SortableTokenItem; -function getTokenItems ( - accountProxy: AccountProxy, - accountProxies: AccountProxy[], - chainInfoMap: Record, - assetRegistry: Record, - tokenGroupSlug?: string // is ether a token slug or a multiChainAsset slug -): TokenSelectorItemType[] { - let allowedChains: string[]; - - if (!isAccountAll(accountProxy.id)) { - // TODO: Review logic getChainsByAccountType after handle transfer for bitcoin - allowedChains = getChainsByAccountType(chainInfoMap, accountProxy.chainTypes, undefined, accountProxy.specialChain); - } else { - allowedChains = getChainsByAccountAll(accountProxy, accountProxies, chainInfoMap); - } - - const items: TokenSelectorItemType[] = []; - - Object.values(assetRegistry).forEach((chainAsset) => { - const originChain = _getAssetOriginChain(chainAsset); - - if (!allowedChains.includes(originChain)) { - return; - } - - if (!tokenGroupSlug || (chainAsset.slug === tokenGroupSlug || _getMultiChainAsset(chainAsset) === tokenGroupSlug)) { - items.push({ - slug: chainAsset.slug, - name: _getAssetName(chainAsset), - symbol: _getAssetSymbol(chainAsset), - originChain - }); - } - }); - - return items; -} - function getTokenAvailableDestinations (tokenSlug: string, xcmRefMap: Record, chainInfoMap: Record): ChainItemType[] { if (!tokenSlug) { return []; @@ -172,6 +135,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone const checkIsPolkadotUnifiedChain = useIsPolkadotUnifiedChain(); const isShowAddressFormatInfoBox = checkIsPolkadotUnifiedChain(chainValue); const getAccountTokenBalance = useGetAccountTokenBalance(); + const getChainSlugsByAccountProxy = useCoreCreateGetChainSlugsByAccountProxy(); const [selectedTransactionFee, setSelectedTransactionFee] = useState(); const { getCurrentConfirmation, renderConfirmationButtons } = useGetConfirmationByScreen('send-fund'); @@ -316,13 +280,30 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone }, [accountAddressItems, fromValue, targetAccountProxy.id]); const tokenItems = useMemo(() => { - const items = getTokenItems( - targetAccountProxy, - accountProxies, - chainInfoMap, - assetRegistry, - sendFundSlug - ); + const items = (() => { + const allowedChains = getChainSlugsByAccountProxy(targetAccountProxy); + + const result: TokenSelectorItemType[] = []; + + Object.values(assetRegistry).forEach((chainAsset) => { + const originChain = _getAssetOriginChain(chainAsset); + + if (!allowedChains.includes(originChain)) { + return; + } + + if (!sendFundSlug || (chainAsset.slug === sendFundSlug || _getMultiChainAsset(chainAsset) === sendFundSlug)) { + result.push({ + slug: chainAsset.slug, + name: _getAssetName(chainAsset), + symbol: _getAssetSymbol(chainAsset), + originChain + }); + } + }); + + return result; + })(); const tokenBalanceMap = getAccountTokenBalance( items.map((item) => item.slug), @@ -354,7 +335,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone sortTokensByBalanceInSelector(tokenItemsSorted, priorityTokens); return tokenItemsSorted; - }, [accountProxies, assetRegistry, chainInfoMap, chainStateMap, getAccountTokenBalance, priorityTokens, sendFundSlug, targetAccountProxy, targetAccountProxyIdForGetBalance]); + }, [assetRegistry, chainStateMap, getAccountTokenBalance, getChainSlugsByAccountProxy, priorityTokens, sendFundSlug, targetAccountProxy, targetAccountProxyIdForGetBalance]); const isNotShowAccountSelector = !isAllAccount && accountAddressItems.length < 2; diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx index aa14fe4b3bc..4abb83ef369 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx @@ -1,14 +1,14 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { _ChainAsset, _ChainStatus } from '@subwallet/chain-list/types'; +import { _ChainAsset, _ChainInfo, _ChainStatus } from '@subwallet/chain-list/types'; import { SwapError } from '@subwallet/extension-base/background/errors/SwapError'; import { ExtrinsicType, NotificationType } from '@subwallet/extension-base/background/KoniTypes'; import { validateRecipientAddress } from '@subwallet/extension-base/core/logic-validation/recipientAddress'; import { ActionType } from '@subwallet/extension-base/core/types'; import { AcrossErrorMsg } from '@subwallet/extension-base/services/balance-service/transfer/xcm/acrossBridge'; import { _ChainState } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetDecimals, _getAssetOriginChain, _getMultiChainAsset, _isAssetFungibleToken, _isChainEvmCompatible, _parseAssetRefKey } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getAssetOriginChain, _getMultiChainAsset, _getOriginChainOfAsset, _isAssetFungibleToken, _isChainEvmCompatible, _parseAssetRefKey } from '@subwallet/extension-base/services/chain-service/utils'; import { KyberSwapQuoteMetadata } from '@subwallet/extension-base/services/swap-service/handler/kyber-handler'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; import { AccountProxy, AccountProxyType, AnalyzedGroup, CommonOptimalSwapPath, ProcessType, SwapRequestResult, SwapRequestV2 } from '@subwallet/extension-base/types'; @@ -20,7 +20,7 @@ import { SwapFromField, SwapToField } from '@subwallet/extension-koni-ui/compone import { ChooseFeeTokenModal, SlippageModal, SwapIdleWarningModal, SwapQuotesSelectorModal, SwapTermsOfServiceModal } from '@subwallet/extension-koni-ui/components/Modal/Swap'; import { ADDRESS_INPUT_AUTO_FORMAT_VALUE, BN_TEN, BN_ZERO, CONFIRM_SWAP_TERM, SWAP_ALL_QUOTES_MODAL, SWAP_CHOOSE_FEE_TOKEN_MODAL, SWAP_IDLE_WARNING_MODAL, SWAP_SLIPPAGE_MODAL, SWAP_TERMS_OF_SERVICE_MODAL } from '@subwallet/extension-koni-ui/constants'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; -import { useChainConnection, useCoreReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useNotification, useOneSignProcess, usePreCheckAction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; +import { useChainConnection, useCoreCreateIsChainInfoCompatibleWithAccountProxy, useCoreReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useNotification, useOneSignProcess, usePreCheckAction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; import { submitProcess } from '@subwallet/extension-koni-ui/messaging'; import { handleSwapRequestV2, handleSwapStep, validateSwapProcess } from '@subwallet/extension-koni-ui/messaging/transaction/swap'; import { FreeBalance, TransactionContent, TransactionFooter } from '@subwallet/extension-koni-ui/Popup/Transaction/parts'; @@ -28,7 +28,7 @@ import { CommonActionType, commonProcessReducer, DEFAULT_COMMON_PROCESS } from ' import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, FormCallbacks, FormFieldData, SwapParams, ThemeProps, TokenBalanceItemType } from '@subwallet/extension-koni-ui/types'; import { TokenSelectorItemType } from '@subwallet/extension-koni-ui/types/field'; -import { convertFieldToObject, findAccountByAddress, getChainsByAccountAll, isAccountAll, isChainInfoAccordantAccountChainType, isTokenCompatibleWithAccountChainTypes, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; +import { convertFieldToObject, findAccountByAddress, isAccountAll, isChainInfoAccordantAccountChainType, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; import { Button, Form, Icon, ModalContext } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; import CN from 'classnames'; @@ -203,6 +203,7 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { const [processState, dispatchProcessState] = useReducer(commonProcessReducer, DEFAULT_COMMON_PROCESS); const { onError, onSuccess } = useHandleSubmitMultiTransaction(dispatchProcessState); + const isChainInfoCompatibleWithAccountProxy = useCoreCreateIsChainInfoCompatibleWithAccountProxy(); const accountAddressItems = useMemo(() => { const chainInfo = chainValue ? chainInfoMap[chainValue] : undefined; @@ -278,11 +279,17 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { return result; }, [assetItems, chainStateMap, getAccountTokenBalance, priorityTokens, targetAccountProxyIdForGetBalance]); - const fromTokenItems = useMemo(() => { - const allowChainSlugs = isAccountAll(targetAccountProxy.id) - ? getChainsByAccountAll(targetAccountProxy, accountProxies, chainInfoMap) - : undefined; + const isTokenCompatibleWithTargetAccountProxy = useCallback(( + tokenSlug: string, + chainInfoMap: Record + ): boolean => { + const chainSlug = _getOriginChainOfAsset(tokenSlug); + const chainInfo = chainInfoMap[chainSlug]; + + return !!chainInfo && isChainInfoCompatibleWithAccountProxy(chainInfo, targetAccountProxy); + }, [isChainInfoCompatibleWithAccountProxy, targetAccountProxy]); + const fromTokenItems = useMemo(() => { return tokenSelectorItems.filter((item) => { const slug = item.slug; const assetInfo = assetRegistryMap[slug]; @@ -291,11 +298,7 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { return false; } - if (allowChainSlugs && !allowChainSlugs.includes(assetInfo.originChain)) { - return false; - } - - if (!isTokenCompatibleWithAccountChainTypes(slug, targetAccountProxy.chainTypes, chainInfoMap)) { + if (!isTokenCompatibleWithTargetAccountProxy(slug, chainInfoMap)) { return false; } @@ -305,7 +308,7 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { return defaultSlug === slug || _getMultiChainAsset(assetInfo) === defaultSlug; }); - }, [accountProxies, assetRegistryMap, chainInfoMap, defaultSlug, targetAccountProxy, tokenSelectorItems]); + }, [assetRegistryMap, chainInfoMap, defaultSlug, isTokenCompatibleWithTargetAccountProxy, tokenSelectorItems]); const toTokenItems = useMemo(() => { return tokenSelectorItems.filter((item) => item.slug !== fromTokenSlugValue); @@ -322,8 +325,8 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { const destChainValue = _getAssetOriginChain(toAssetInfo); const isSwitchable = useMemo(() => { - return isTokenCompatibleWithAccountChainTypes(toTokenSlugValue, targetAccountProxy.chainTypes, chainInfoMap); - }, [chainInfoMap, targetAccountProxy.chainTypes, toTokenSlugValue]); + return isTokenCompatibleWithTargetAccountProxy(toTokenSlugValue || '', chainInfoMap); + }, [chainInfoMap, isTokenCompatibleWithTargetAccountProxy, toTokenSlugValue]); // Unable to use useEffect due to infinity loop caused by conflict setCurrentSlippage and currentQuote const slippage = useMemo(() => { diff --git a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx index fafe8c0c66b..ae3fb61b6d5 100644 --- a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx +++ b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx @@ -6,9 +6,9 @@ import type { KeypairType } from '@subwallet/keyring/types'; import { _ChainInfo } from '@subwallet/chain-list/types'; import { _BITCOIN_CHAIN_SLUG, _BITCOIN_TESTNET_CHAIN_SLUG } from '@subwallet/extension-base/services/chain-service/constants'; import { AccountProxy } from '@subwallet/extension-base/types'; -import { useCoreReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useCoreCreateGetChainSlugsByAccountProxy, useCoreReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { AccountChainAddress } from '@subwallet/extension-koni-ui/types'; -import { getBitcoinAccountDetails, getChainsByAccountType } from '@subwallet/extension-koni-ui/utils'; +import { getBitcoinAccountDetails } from '@subwallet/extension-koni-ui/utils'; import { useMemo } from 'react'; // todo: @@ -45,10 +45,11 @@ const createChainAddressItem = ( const useGetAccountChainAddresses = (accountProxy: AccountProxy): AccountChainAddress[] => { const chainInfoMap = useSelector((state) => state.chainStore.chainInfoMap); const getReformatAddress = useCoreReformatAddress(); + const getChainSlugsByAccountProxy = useCoreCreateGetChainSlugsByAccountProxy(); return useMemo(() => { const result: AccountChainAddress[] = []; - const chains: string[] = getChainsByAccountType(chainInfoMap, accountProxy.chainTypes, undefined, accountProxy.specialChain); + const chains: string[] = getChainSlugsByAccountProxy(accountProxy); accountProxy.accounts.forEach((a) => { for (const chain of chains) { @@ -68,7 +69,7 @@ const useGetAccountChainAddresses = (accountProxy: AccountProxy): AccountChainAd }); return result; - }, [accountProxy, chainInfoMap, getReformatAddress]); + }, [accountProxy, chainInfoMap, getChainSlugsByAccountProxy, getReformatAddress]); }; export default useGetAccountChainAddresses; diff --git a/packages/extension-koni-ui/src/hooks/chain/index.ts b/packages/extension-koni-ui/src/hooks/chain/index.ts index 753912245bf..d0813cca0ff 100644 --- a/packages/extension-koni-ui/src/hooks/chain/index.ts +++ b/packages/extension-koni-ui/src/hooks/chain/index.ts @@ -13,6 +13,6 @@ export { default as useGetChainPrefixBySlug } from './useGetChainPrefixBySlug'; export { default as useChainInfoData } from './useChainInfoData'; export { default as useChainConnection } from './useChainConnection'; -export { default as useCoreGetChainSlugsByAccountProxy } from './useCoreGetChainSlugsByAccountProxy'; -export { default as useCoreIsChainInfoCompatibleWithAccountProxy } from './useCoreIsChainInfoCompatibleWithAccountProxy'; +export { default as useCoreCreateGetChainSlugsByAccountProxy } from './useCoreCreateGetChainSlugsByAccountProxy'; +export { default as useCoreCreateIsChainInfoCompatibleWithAccountProxy } from './useCoreCreateIsChainInfoCompatibleWithAccountProxy'; export { default as useGetChainSlugsByCurrentAccountProxy } from './useGetChainSlugsByCurrentAccountProxy'; diff --git a/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx new file mode 100644 index 00000000000..474514664c9 --- /dev/null +++ b/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx @@ -0,0 +1,13 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { AccountProxy } from '@subwallet/extension-base/types'; +import { useCallback } from 'react'; + +const useCoreCreateGetChainSlugsByAccountProxy = () => { + return useCallback((accountProxies: AccountProxy): string[] => { + return []; + }, []); +}; + +export default useCoreCreateGetChainSlugsByAccountProxy; diff --git a/packages/extension-koni-ui/src/hooks/chain/useCoreCreateIsChainInfoCompatibleWithAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useCoreCreateIsChainInfoCompatibleWithAccountProxy.tsx new file mode 100644 index 00000000000..ddcb2ac0010 --- /dev/null +++ b/packages/extension-koni-ui/src/hooks/chain/useCoreCreateIsChainInfoCompatibleWithAccountProxy.tsx @@ -0,0 +1,14 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { _ChainInfo } from '@subwallet/chain-list/types'; +import { AccountProxy } from '@subwallet/extension-base/types'; +import { useCallback } from 'react'; + +const useCoreCreateIsChainInfoCompatibleWithAccountProxy = () => { + return useCallback((chainInfo: _ChainInfo, accountProxies: AccountProxy): boolean => { + return false; + }, []); +}; + +export default useCoreCreateIsChainInfoCompatibleWithAccountProxy; diff --git a/packages/extension-koni-ui/src/hooks/chain/useCoreGetChainSlugsByAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useCoreGetChainSlugsByAccountProxy.tsx deleted file mode 100644 index 614d4a8bf0e..00000000000 --- a/packages/extension-koni-ui/src/hooks/chain/useCoreGetChainSlugsByAccountProxy.tsx +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { _ChainInfo } from '@subwallet/chain-list/types'; -import { AccountProxy } from '@subwallet/extension-base/types'; - -const useCoreIsChainInfoCompatibleWithAccountProxy = (chainInfo: _ChainInfo, accountProxies: AccountProxy): boolean => { - return false; -}; - -export default useCoreIsChainInfoCompatibleWithAccountProxy; diff --git a/packages/extension-koni-ui/src/hooks/chain/useCoreIsChainInfoCompatibleWithAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useCoreIsChainInfoCompatibleWithAccountProxy.tsx deleted file mode 100644 index d6f1b08d40b..00000000000 --- a/packages/extension-koni-ui/src/hooks/chain/useCoreIsChainInfoCompatibleWithAccountProxy.tsx +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { AccountProxy } from '@subwallet/extension-base/types'; - -const useCoreIsChainInfoCompatibleWithAccountProxy = (accountProxies: AccountProxy): string[] => { - return []; -}; - -export default useCoreIsChainInfoCompatibleWithAccountProxy; diff --git a/packages/extension-koni-ui/src/utils/chain/chainAndAsset.ts b/packages/extension-koni-ui/src/utils/chain/chainAndAsset.ts index e5909e510f8..1da7776f9f5 100644 --- a/packages/extension-koni-ui/src/utils/chain/chainAndAsset.ts +++ b/packages/extension-koni-ui/src/utils/chain/chainAndAsset.ts @@ -1,12 +1,10 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; +import { _ChainAsset } from '@subwallet/chain-list/types'; import { AssetSetting } from '@subwallet/extension-base/background/KoniTypes'; import { _ChainState } from '@subwallet/extension-base/services/chain-service/types'; -import { _getOriginChainOfAsset, _isAssetFungibleToken } from '@subwallet/extension-base/services/chain-service/utils'; -import { AccountChainType } from '@subwallet/extension-base/types'; -import { isChainCompatibleWithAccountChainTypes } from '@subwallet/extension-koni-ui/utils'; +import { _isAssetFungibleToken } from '@subwallet/extension-base/services/chain-service/utils'; export function isTokenAvailable ( chainAsset: _ChainAsset, @@ -28,18 +26,3 @@ export function isTokenAvailable ( return isAssetVisible && isAssetFungible && isValidLedger; } - -export function getChainInfoFromToken (tokenSlug: string, chainInfoMap: Record): _ChainInfo | undefined { - const chainSlug = _getOriginChainOfAsset(tokenSlug); - - return chainInfoMap[chainSlug]; -} - -export function isTokenCompatibleWithAccountChainTypes ( - tokenSlug: string, - chainTypes: AccountChainType[], - chainInfoMap: Record): boolean { - const chainInfo = getChainInfoFromToken(tokenSlug, chainInfoMap); - - return !!chainInfo && isChainCompatibleWithAccountChainTypes(chainInfo, chainTypes); -} From 48dccf5889a22191b72bfc078bac759605bd4484 Mon Sep 17 00:00:00 2001 From: lw Date: Tue, 3 Jun 2025 15:30:33 +0700 Subject: [PATCH 110/178] [Issue-4297] refactor: Rename useCoreReformatAddress to useCoreCreateReformatAddress --- packages/extension-koni-ui/src/Popup/BuyTokens.tsx | 4 ++-- .../Home/Earning/EarningPositionDetail/RewardInfoPart.tsx | 4 ++-- .../src/Popup/Transaction/variants/Earn.tsx | 4 ++-- .../src/Popup/Transaction/variants/SendFund.tsx | 5 ++--- .../src/Popup/Transaction/variants/Swap/index.tsx | 4 ++-- .../src/components/Modal/AddressBook/AddressBookModal.tsx | 4 ++-- .../src/hooks/account/useGetAccountChainAddresses.tsx | 4 ++-- packages/extension-koni-ui/src/hooks/common/index.ts | 2 +- ...eReformatAddress.tsx => useCoreCreateReformatAddress.tsx} | 4 ++-- .../src/hooks/history/useHistorySelection.tsx | 4 ++-- .../src/hooks/screen/home/useCoreReceiveModalHelper.tsx | 4 ++-- 11 files changed, 21 insertions(+), 22 deletions(-) rename packages/extension-koni-ui/src/hooks/common/{useCoreReformatAddress.tsx => useCoreCreateReformatAddress.tsx} (85%) diff --git a/packages/extension-koni-ui/src/Popup/BuyTokens.tsx b/packages/extension-koni-ui/src/Popup/BuyTokens.tsx index 7063a0094d1..30e7793ecdb 100644 --- a/packages/extension-koni-ui/src/Popup/BuyTokens.tsx +++ b/packages/extension-koni-ui/src/Popup/BuyTokens.tsx @@ -8,7 +8,7 @@ import { detectTranslate, isAccountAll } from '@subwallet/extension-base/utils'; import { AccountAddressSelector, baseServiceItems, Layout, PageWrapper, ServiceItem } from '@subwallet/extension-koni-ui/components'; import { ServiceSelector } from '@subwallet/extension-koni-ui/components/Field/BuyTokens/ServiceSelector'; import { TokenSelector } from '@subwallet/extension-koni-ui/components/Field/TokenSelector'; -import { useAssetChecker, useCoreReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetChainSlugsByCurrentAccountProxy, useNotification, useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { useAssetChecker, useCoreCreateReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetChainSlugsByCurrentAccountProxy, useNotification, useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, CreateBuyOrderFunction, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { TokenSelectorItemType } from '@subwallet/extension-koni-ui/types/field'; @@ -80,7 +80,7 @@ function Component ({ className, currentAccountProxy }: ComponentProps) { const checkAsset = useAssetChecker(); const allowedChains = useGetChainSlugsByCurrentAccountProxy(); - const getReformatAddress = useCoreReformatAddress(); + const getReformatAddress = useCoreCreateReformatAddress(); const fixedTokenSlug = useMemo((): string | undefined => { if (currentSymbol) { diff --git a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx index de3968d508b..65fe4bfa9e9 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart.tsx @@ -8,7 +8,7 @@ import { EarningRewardHistoryItem, YieldPoolType, YieldPositionInfo } from '@sub import { isSameAddress } from '@subwallet/extension-base/utils'; import { CollapsiblePanel, MetaInfo } from '@subwallet/extension-koni-ui/components'; import { ASTAR_PORTAL_URL, BN_ZERO, CLAIM_REWARD_TRANSACTION, DEFAULT_CLAIM_REWARD_PARAMS, EarningStatusUi } from '@subwallet/extension-koni-ui/constants'; -import { useCoreReformatAddress, useSelector, useTranslation, useYieldRewardTotal } from '@subwallet/extension-koni-ui/hooks'; +import { useCoreCreateReformatAddress, useSelector, useTranslation, useYieldRewardTotal } from '@subwallet/extension-koni-ui/hooks'; import { AlertDialogProps, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { customFormatDate, openInNewTab } from '@subwallet/extension-koni-ui/utils'; import { ActivityIndicator, Button, Icon, Number } from '@subwallet/react-ui'; @@ -43,7 +43,7 @@ function Component ({ className, closeAlert, compound, inputAsset, isShowBalance const [, setClaimRewardStorage] = useLocalStorage(CLAIM_REWARD_TRANSACTION, DEFAULT_CLAIM_REWARD_PARAMS); const total = useYieldRewardTotal(slug); - const getReformatAddress = useCoreReformatAddress(); + const getReformatAddress = useCoreCreateReformatAddress(); const isDAppStaking = useMemo(() => _STAKING_CHAIN_GROUP.astar.includes(compound.chain), [compound.chain]); const isMythosStaking = useMemo(() => _STAKING_CHAIN_GROUP.mythos.includes(compound.chain), [compound.chain]); diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx index dbd18666766..e0f1d5f1fad 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Earn.tsx @@ -18,7 +18,7 @@ import { EarningInstructionModal } from '@subwallet/extension-koni-ui/components import { SlippageModal } from '@subwallet/extension-koni-ui/components/Modal/Swap'; import { EARNING_INSTRUCTION_MODAL, EARNING_SLIPPAGE_MODAL, STAKE_ALERT_DATA } from '@subwallet/extension-koni-ui/constants'; import { MktCampaignModalContext } from '@subwallet/extension-koni-ui/contexts/MktCampaignModalContext'; -import { useChainConnection, useCoreReformatAddress, useExtensionDisplayModes, useFetchChainState, useGetBalance, useGetNativeTokenSlug, useGetYieldPositionForSpecificAccount, useInitValidateTransaction, useNotification, useOneSignProcess, usePreCheckAction, useRestoreTransaction, useSelector, useSidePanelUtils, useTransactionContext, useWatchTransaction, useYieldPositionDetail } from '@subwallet/extension-koni-ui/hooks'; +import { useChainConnection, useCoreCreateReformatAddress, useExtensionDisplayModes, useFetchChainState, useGetBalance, useGetNativeTokenSlug, useGetYieldPositionForSpecificAccount, useInitValidateTransaction, useNotification, useOneSignProcess, usePreCheckAction, useRestoreTransaction, useSelector, useSidePanelUtils, useTransactionContext, useWatchTransaction, useYieldPositionDetail } from '@subwallet/extension-koni-ui/hooks'; import useGetConfirmationByScreen from '@subwallet/extension-koni-ui/hooks/campaign/useGetConfirmationByScreen'; import { fetchPoolTarget, getEarningSlippage, getOptimalYieldPath, submitJoinYieldPool, submitProcess, validateYieldProcess, windowOpen } from '@subwallet/extension-koni-ui/messaging'; import { DEFAULT_YIELD_PROCESS, EarningActionType, earningReducer } from '@subwallet/extension-koni-ui/reducer'; @@ -80,7 +80,7 @@ const Component = () => { const oneSign = useOneSignProcess(fromValue); const nativeTokenSlug = useGetNativeTokenSlug(chainValue); - const getReformatAddress = useCoreReformatAddress(); + const getReformatAddress = useCoreCreateReformatAddress(); const isClickInfoButtonRef = useRef(false); 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 08477da30bf..22f5505c4d7 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -23,7 +23,7 @@ import { _reformatAddressWithChain, detectTranslate, isAccountAll } from '@subwa import { AccountAddressSelector, AddressInputNew, AddressInputRef, AlertBox, AlertBoxInstant, AlertModal, AmountInput, ChainSelector, FeeEditor, HiddenInput, TokenSelector } from '@subwallet/extension-koni-ui/components'; import { ADDRESS_INPUT_AUTO_FORMAT_VALUE } from '@subwallet/extension-koni-ui/constants'; import { MktCampaignModalContext } from '@subwallet/extension-koni-ui/contexts/MktCampaignModalContext'; -import { useAlert, useCoreReformatAddress, useDefaultNavigate, useFetchChainAssetInfo, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useIsPolkadotUnifiedChain, useNotification, usePreCheckAction, useRestoreTransaction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; +import { useAlert, useCoreCreateGetChainSlugsByAccountProxy, useCoreCreateReformatAddress, useDefaultNavigate, useFetchChainAssetInfo, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useIsPolkadotUnifiedChain, useNotification, usePreCheckAction, useRestoreTransaction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; import useGetConfirmationByScreen from '@subwallet/extension-koni-ui/hooks/campaign/useGetConfirmationByScreen'; import { approveSpending, cancelSubscription, getOptimalTransferProcess, getTokensCanPayFee, isTonBounceableAddress, makeCrossChainTransfer, makeTransfer, subscribeMaxTransfer } from '@subwallet/extension-koni-ui/messaging'; import { CommonActionType, commonProcessReducer, DEFAULT_COMMON_PROCESS } from '@subwallet/extension-koni-ui/reducer'; @@ -43,7 +43,6 @@ import { useIsFirstRender, useLocalStorage } from 'usehooks-ts'; import { BN, BN_ZERO } from '@polkadot/util'; -import useCoreCreateGetChainSlugsByAccountProxy from '../../../hooks/chain/useCoreCreateGetChainSlugsByAccountProxy'; import { FreeBalance, TransactionContent, TransactionFooter } from '../parts'; type WrapperProps = ThemeProps; @@ -121,7 +120,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone const { nativeTokenBalance } = useGetBalance(chainValue, fromValue); const assetInfo = useFetchChainAssetInfo(assetValue); - const getReformatAddress = useCoreReformatAddress(); + const getReformatAddress = useCoreCreateReformatAddress(); const { alertProps, closeAlert, openAlert } = useAlert(alertModalId); const { chainInfoMap, chainStateMap, chainStatusMap, ledgerGenericAllowNetworks, priorityTokens } = useSelector((root) => root.chainStore); diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx index 4abb83ef369..1544e41a47d 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx @@ -20,7 +20,7 @@ import { SwapFromField, SwapToField } from '@subwallet/extension-koni-ui/compone import { ChooseFeeTokenModal, SlippageModal, SwapIdleWarningModal, SwapQuotesSelectorModal, SwapTermsOfServiceModal } from '@subwallet/extension-koni-ui/components/Modal/Swap'; import { ADDRESS_INPUT_AUTO_FORMAT_VALUE, BN_TEN, BN_ZERO, CONFIRM_SWAP_TERM, SWAP_ALL_QUOTES_MODAL, SWAP_CHOOSE_FEE_TOKEN_MODAL, SWAP_IDLE_WARNING_MODAL, SWAP_SLIPPAGE_MODAL, SWAP_TERMS_OF_SERVICE_MODAL } from '@subwallet/extension-koni-ui/constants'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; -import { useChainConnection, useCoreCreateIsChainInfoCompatibleWithAccountProxy, useCoreReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useNotification, useOneSignProcess, usePreCheckAction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; +import { useChainConnection, useCoreCreateIsChainInfoCompatibleWithAccountProxy, useCoreCreateReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useNotification, useOneSignProcess, usePreCheckAction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; import { submitProcess } from '@subwallet/extension-koni-ui/messaging'; import { handleSwapRequestV2, handleSwapStep, validateSwapProcess } from '@subwallet/extension-koni-ui/messaging/transaction/swap'; import { FreeBalance, TransactionContent, TransactionFooter } from '@subwallet/extension-koni-ui/Popup/Transaction/parts'; @@ -199,7 +199,7 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { const { checkChainConnected, turnOnChain } = useChainConnection(); const onPreCheck = usePreCheckAction(fromValue); const oneSign = useOneSignProcess(fromValue); - const getReformatAddress = useCoreReformatAddress(); + const getReformatAddress = useCoreCreateReformatAddress(); const [processState, dispatchProcessState] = useReducer(commonProcessReducer, DEFAULT_COMMON_PROCESS); const { onError, onSuccess } = useHandleSubmitMultiTransaction(dispatchProcessState); diff --git a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx index b9a8929158c..7fdc3dcb960 100644 --- a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx @@ -4,7 +4,7 @@ import { AnalyzeAddress, AnalyzedGroup } from '@subwallet/extension-base/types'; import { _reformatAddressWithChain, getAccountChainTypeForAddress } from '@subwallet/extension-base/utils'; import { AddressSelectorItem, BackIcon } from '@subwallet/extension-koni-ui/components'; -import { useChainInfo, useCoreReformatAddress, useFilterModal, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useChainInfo, useCoreCreateReformatAddress, useFilterModal, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; import { getBitcoinAccountDetails, isAccountAll, isChainInfoAccordantAccountChainType } from '@subwallet/extension-koni-ui/utils'; import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; @@ -58,7 +58,7 @@ const Component: React.FC = (props: Props) => { const chainInfo = useChainInfo(chainSlug); - const getReformatAddress = useCoreReformatAddress(); + const getReformatAddress = useCoreCreateReformatAddress(); const filterModal = useMemo(() => `${id}-filter-modal`, [id]); diff --git a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx index ae3fb61b6d5..7e6191582de 100644 --- a/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx +++ b/packages/extension-koni-ui/src/hooks/account/useGetAccountChainAddresses.tsx @@ -6,7 +6,7 @@ import type { KeypairType } from '@subwallet/keyring/types'; import { _ChainInfo } from '@subwallet/chain-list/types'; import { _BITCOIN_CHAIN_SLUG, _BITCOIN_TESTNET_CHAIN_SLUG } from '@subwallet/extension-base/services/chain-service/constants'; import { AccountProxy } from '@subwallet/extension-base/types'; -import { useCoreCreateGetChainSlugsByAccountProxy, useCoreReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useCoreCreateGetChainSlugsByAccountProxy, useCoreCreateReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { AccountChainAddress } from '@subwallet/extension-koni-ui/types'; import { getBitcoinAccountDetails } from '@subwallet/extension-koni-ui/utils'; import { useMemo } from 'react'; @@ -44,7 +44,7 @@ const createChainAddressItem = ( const useGetAccountChainAddresses = (accountProxy: AccountProxy): AccountChainAddress[] => { const chainInfoMap = useSelector((state) => state.chainStore.chainInfoMap); - const getReformatAddress = useCoreReformatAddress(); + const getReformatAddress = useCoreCreateReformatAddress(); const getChainSlugsByAccountProxy = useCoreCreateGetChainSlugsByAccountProxy(); return useMemo(() => { diff --git a/packages/extension-koni-ui/src/hooks/common/index.ts b/packages/extension-koni-ui/src/hooks/common/index.ts index 4c0557c3318..006be4c6c07 100644 --- a/packages/extension-koni-ui/src/hooks/common/index.ts +++ b/packages/extension-koni-ui/src/hooks/common/index.ts @@ -16,7 +16,7 @@ export { default as useSetSessionLatest } from './useSetSessionLatest'; export { default as useDebouncedValue } from './useDebouncedValue'; export { default as useIsPolkadotUnifiedChain } from './useIsPolkadotUnifiedChain'; export { default as useGetBitcoinAccounts } from './useGetBitcoinAccounts'; -export { default as useCoreReformatAddress } from './useCoreReformatAddress'; +export { default as useCoreCreateReformatAddress } from './useCoreCreateReformatAddress'; export * from './useSelector'; export * from './useLazyList'; diff --git a/packages/extension-koni-ui/src/hooks/common/useCoreReformatAddress.tsx b/packages/extension-koni-ui/src/hooks/common/useCoreCreateReformatAddress.tsx similarity index 85% rename from packages/extension-koni-ui/src/hooks/common/useCoreReformatAddress.tsx rename to packages/extension-koni-ui/src/hooks/common/useCoreCreateReformatAddress.tsx index a19272206ca..ac3f76d4a69 100644 --- a/packages/extension-koni-ui/src/hooks/common/useCoreReformatAddress.tsx +++ b/packages/extension-koni-ui/src/hooks/common/useCoreCreateReformatAddress.tsx @@ -6,10 +6,10 @@ import { AccountJson } from '@subwallet/extension-base/types'; import { getReformatedAddressRelatedToChain } from '@subwallet/extension-koni-ui/utils'; import { useCallback } from 'react'; -const useCoreReformatAddress = () => { +const useCoreCreateReformatAddress = () => { return useCallback((accountJson: AccountJson, chainInfo: _ChainInfo): string | undefined => { return getReformatedAddressRelatedToChain(accountJson, chainInfo); }, []); }; -export default useCoreReformatAddress; +export default useCoreCreateReformatAddress; diff --git a/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx b/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx index 14639b72cc1..e157a6ba999 100644 --- a/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx +++ b/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AccountProxy } from '@subwallet/extension-base/types'; -import { useChainInfoWithState, useCoreReformatAddress, useGetChainSlugsByCurrentAccountProxy, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useChainInfoWithState, useCoreCreateReformatAddress, useGetChainSlugsByCurrentAccountProxy, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { AccountAddressItemType, ChainItemType } from '@subwallet/extension-koni-ui/types'; import { isAccountAll } from '@subwallet/extension-koni-ui/utils'; import { useEffect, useMemo, useState } from 'react'; @@ -13,7 +13,7 @@ export default function useHistorySelection () { const { chainInfoMap } = useSelector((root) => root.chainStore); const chainInfoList = useChainInfoWithState(); const allowedChains = useGetChainSlugsByCurrentAccountProxy(); - const getReformatAddress = useCoreReformatAddress(); + const getReformatAddress = useCoreCreateReformatAddress(); const { accountProxies, currentAccountProxy } = useSelector((root) => root.accountState); const [selectedAddress, setSelectedAddress] = useState(propAddress || ''); diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx index 87e627cb81b..04b6d7eaf77 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx +++ b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx @@ -7,7 +7,7 @@ import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/c import { AccountActions, AccountProxyType } from '@subwallet/extension-base/types'; import { RECEIVE_MODAL_ACCOUNT_SELECTOR, RECEIVE_MODAL_TOKEN_SELECTOR } from '@subwallet/extension-koni-ui/constants'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import { useCoreReformatAddress, useGetBitcoinAccounts, useGetChainSlugsByCurrentAccountProxy, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain } from '@subwallet/extension-koni-ui/hooks'; +import { useCoreCreateReformatAddress, useGetBitcoinAccounts, useGetChainSlugsByCurrentAccountProxy, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain } from '@subwallet/extension-koni-ui/hooks'; import { useChainAssets } from '@subwallet/extension-koni-ui/hooks/assets'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, AccountTokenAddress, ReceiveModalProps } from '@subwallet/extension-koni-ui/types'; @@ -39,7 +39,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const chainSupported = useGetChainSlugsByCurrentAccountProxy(); const onHandleTonAccountWarning = useHandleTonAccountWarning(); const onHandleLedgerGenericAccountWarning = useHandleLedgerGenericAccountWarning(); - const getReformatAddress = useCoreReformatAddress(); + const getReformatAddress = useCoreCreateReformatAddress(); const checkIsPolkadotUnifiedChain = useIsPolkadotUnifiedChain(); const getBitcoinAccount = useGetBitcoinAccounts(); From e365ef23e96ca5b2028779e121bab454c8a8b3f1 Mon Sep 17 00:00:00 2001 From: lw Date: Tue, 3 Jun 2025 16:09:28 +0700 Subject: [PATCH 111/178] [Issue-4297] refactor: Add "deprecated" to some functions, hooks --- .../hooks/screen/home/useGetChainSlugsByAccount.ts | 3 +++ packages/extension-koni-ui/src/utils/chain/chain.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useGetChainSlugsByAccount.ts b/packages/extension-koni-ui/src/hooks/screen/home/useGetChainSlugsByAccount.ts index 4a5961aa3e5..42827ea4d32 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useGetChainSlugsByAccount.ts +++ b/packages/extension-koni-ui/src/hooks/screen/home/useGetChainSlugsByAccount.ts @@ -9,6 +9,9 @@ import { KeypairType } from '@subwallet/keyring/types'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; +/** + * @deprecated Use hook `useGetChainSlugsByCurrentAccountProxy` or 'useCoreCreateGetChainSlugsByAccountProxy' instead. + */ // TODO: Recheck the usages of the address in this hook. export const useGetChainSlugsByAccount = (address?: string): string[] => { const chainInfoMap = useSelector((state: RootState) => state.chainStore.chainInfoMap); diff --git a/packages/extension-koni-ui/src/utils/chain/chain.ts b/packages/extension-koni-ui/src/utils/chain/chain.ts index 67578c356d6..0fe82ba060c 100644 --- a/packages/extension-koni-ui/src/utils/chain/chain.ts +++ b/packages/extension-koni-ui/src/utils/chain/chain.ts @@ -35,6 +35,9 @@ export const findChainInfoByChainId = (chainMap: Record, cha return null; }; +/** + * @deprecated Use `isChainInfoCompatibleWithAccountInfo` instead. + */ export const isChainInfoAccordantAccountChainType = (chainInfo: _ChainInfo, chainType: AccountChainType): boolean => { if (chainType === AccountChainType.SUBSTRATE) { return _isPureSubstrateChain(chainInfo); @@ -63,10 +66,16 @@ export const isChainInfoCompatibleWithAccountInfo = (chainInfo: _ChainInfo, acco return false; }; +/** + * @deprecated Use hook `useCoreCreateIsChainInfoCompatibleWithAccountProxy` instead. + */ export const isChainCompatibleWithAccountChainTypes = (chainInfo: _ChainInfo, chainTypes: AccountChainType[]): boolean => { return chainTypes.some((chainType) => isChainInfoAccordantAccountChainType(chainInfo, chainType)); }; +/** + * @deprecated Use hook `useCoreCreateGetChainSlugsByAccountProxy` instead. + */ export const getChainsByAccountType = (_chainInfoMap: Record, chainTypes: AccountChainType[], accountTypes: KeypairType[] = [], specialChain?: string): string[] => { const chainInfoMap = Object.fromEntries(Object.entries(_chainInfoMap).filter(([, chainInfo]) => chainInfo.chainStatus === _ChainStatus.ACTIVE)); @@ -101,6 +110,9 @@ export const getChainsByAccountType = (_chainInfoMap: Record } }; +/** + * @deprecated Use hook `useCoreCreateGetChainSlugsByAccountProxy` instead. + */ // Note : The function filters the chain slug list by account All, where all accounts case may include only Ledger accounts. export const getChainsByAccountAll = (accountAllProxy: AccountProxy, accountProxies: AccountProxy[], _chainInfoMap: Record, accountTypes: KeypairType[] = []): string[] => { const specialChainRecord: Record = {} as Record; From 8b75bd1dc398d6b25fc0524e2dc4b29b9efb683c Mon Sep 17 00:00:00 2001 From: lw Date: Tue, 3 Jun 2025 17:47:56 +0700 Subject: [PATCH 112/178] [Issue-4297] refactor: Update logic for useCoreCreateGetChainSlugsByAccountProxy and useGetChainSlugsByCurrentAccountProxy --- ...eCoreCreateGetChainSlugsByAccountProxy.tsx | 53 +++++++++++++++++-- .../useGetChainSlugsByCurrentAccountProxy.tsx | 14 ++++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx index 474514664c9..9b5b686d2ea 100644 --- a/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx +++ b/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx @@ -1,13 +1,60 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { _ChainInfo, _ChainStatus } from '@subwallet/chain-list/types'; import { AccountProxy } from '@subwallet/extension-base/types'; +import { isAccountAll } from '@subwallet/extension-base/utils'; +import { useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-koni-ui/utils'; import { useCallback } from 'react'; +function getChainSlugsByAccountProxySingle (accountProxySingle: AccountProxy, chainInfoMap: Record): string[] { + if (accountProxySingle.specialChain) { + return accountProxySingle.specialChain in chainInfoMap ? [accountProxySingle.specialChain] : []; + } + + const result: string[] = []; + + for (const chainInfo of Object.values(chainInfoMap)) { + if (accountProxySingle.accounts.some((account) => isChainInfoCompatibleWithAccountInfo(chainInfo, account.chainType, account.type))) { + result.push(chainInfo.slug); + } + } + + return result; +} + +function getChainSlugsByAccountProxyAll (accountProxyAll: AccountProxy, accountProxies: AccountProxy[], chainInfoMap: Record): string[] { + const { specialChain } = accountProxyAll; + + if (specialChain) { + return specialChain in chainInfoMap ? [specialChain] : []; + } + + const slugSet = new Set(); + + for (const accountProxy of accountProxies) { + for (const slug of getChainSlugsByAccountProxySingle(accountProxy, chainInfoMap)) { + slugSet.add(slug); + } + } + + return [...slugSet]; +} + const useCoreCreateGetChainSlugsByAccountProxy = () => { - return useCallback((accountProxies: AccountProxy): string[] => { - return []; - }, []); + const accountProxies = useSelector((state) => state.accountState.accountProxies); + const chainInfoMap = useSelector((state) => state.chainStore.chainInfoMap); + + return useCallback((accountProxy: AccountProxy): string[] => { + const filteredChainInfoMap = Object.fromEntries(Object.entries(chainInfoMap).filter(([, chainInfo]) => chainInfo.chainStatus === _ChainStatus.ACTIVE)); + + if (isAccountAll(accountProxy.id)) { + return getChainSlugsByAccountProxyAll(accountProxy, accountProxies, filteredChainInfoMap); + } + + return getChainSlugsByAccountProxySingle(accountProxy, filteredChainInfoMap); + }, [accountProxies, chainInfoMap]); }; export default useCoreCreateGetChainSlugsByAccountProxy; diff --git a/packages/extension-koni-ui/src/hooks/chain/useGetChainSlugsByCurrentAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useGetChainSlugsByCurrentAccountProxy.tsx index dbc0de079f1..a0f8bc0a65f 100644 --- a/packages/extension-koni-ui/src/hooks/chain/useGetChainSlugsByCurrentAccountProxy.tsx +++ b/packages/extension-koni-ui/src/hooks/chain/useGetChainSlugsByCurrentAccountProxy.tsx @@ -1,8 +1,20 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { useCoreCreateGetChainSlugsByAccountProxy, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useMemo } from 'react'; + const useGetChainSlugsByCurrentAccountProxy = (): string[] => { - return []; + const currentAccountProxy = useSelector((state) => state.accountState.currentAccountProxy); + const getChainSlugsByAccountProxy = useCoreCreateGetChainSlugsByAccountProxy(); + + return useMemo(() => { + if (!currentAccountProxy) { + return []; + } + + return getChainSlugsByAccountProxy(currentAccountProxy); + }, [currentAccountProxy, getChainSlugsByAccountProxy]); }; export default useGetChainSlugsByCurrentAccountProxy; From 413047ebd3bbc693479ef5f167510a085894e85c Mon Sep 17 00:00:00 2001 From: lw Date: Tue, 3 Jun 2025 18:17:16 +0700 Subject: [PATCH 113/178] [Issue-4297] refactor: Remove useCoreCreateIsChainInfoCompatibleWithAccountProxy --- .../Popup/Transaction/variants/Swap/index.tsx | 34 +++++++++---------- .../src/hooks/chain/index.ts | 1 - ...eIsChainInfoCompatibleWithAccountProxy.tsx | 14 -------- .../src/utils/chain/chain.ts | 2 +- 4 files changed, 18 insertions(+), 33 deletions(-) delete mode 100644 packages/extension-koni-ui/src/hooks/chain/useCoreCreateIsChainInfoCompatibleWithAccountProxy.tsx diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx index 1544e41a47d..0405d73e423 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx @@ -20,7 +20,7 @@ import { SwapFromField, SwapToField } from '@subwallet/extension-koni-ui/compone import { ChooseFeeTokenModal, SlippageModal, SwapIdleWarningModal, SwapQuotesSelectorModal, SwapTermsOfServiceModal } from '@subwallet/extension-koni-ui/components/Modal/Swap'; import { ADDRESS_INPUT_AUTO_FORMAT_VALUE, BN_TEN, BN_ZERO, CONFIRM_SWAP_TERM, SWAP_ALL_QUOTES_MODAL, SWAP_CHOOSE_FEE_TOKEN_MODAL, SWAP_IDLE_WARNING_MODAL, SWAP_SLIPPAGE_MODAL, SWAP_TERMS_OF_SERVICE_MODAL } from '@subwallet/extension-koni-ui/constants'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; -import { useChainConnection, useCoreCreateIsChainInfoCompatibleWithAccountProxy, useCoreCreateReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useNotification, useOneSignProcess, usePreCheckAction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; +import { useChainConnection, useCoreCreateGetChainSlugsByAccountProxy, useCoreCreateReformatAddress, useDefaultNavigate, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useNotification, useOneSignProcess, usePreCheckAction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; import { submitProcess } from '@subwallet/extension-koni-ui/messaging'; import { handleSwapRequestV2, handleSwapStep, validateSwapProcess } from '@subwallet/extension-koni-ui/messaging/transaction/swap'; import { FreeBalance, TransactionContent, TransactionFooter } from '@subwallet/extension-koni-ui/Popup/Transaction/parts'; @@ -200,10 +200,10 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { const onPreCheck = usePreCheckAction(fromValue); const oneSign = useOneSignProcess(fromValue); const getReformatAddress = useCoreCreateReformatAddress(); + const getChainSlugsByAccountProxy = useCoreCreateGetChainSlugsByAccountProxy(); const [processState, dispatchProcessState] = useReducer(commonProcessReducer, DEFAULT_COMMON_PROCESS); const { onError, onSuccess } = useHandleSubmitMultiTransaction(dispatchProcessState); - const isChainInfoCompatibleWithAccountProxy = useCoreCreateIsChainInfoCompatibleWithAccountProxy(); const accountAddressItems = useMemo(() => { const chainInfo = chainValue ? chainInfoMap[chainValue] : undefined; @@ -279,26 +279,26 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { return result; }, [assetItems, chainStateMap, getAccountTokenBalance, priorityTokens, targetAccountProxyIdForGetBalance]); - const isTokenCompatibleWithTargetAccountProxy = useCallback(( - tokenSlug: string, - chainInfoMap: Record - ): boolean => { + const allowedChainSlugsForTargetAccountProxy = useMemo(() => { + return getChainSlugsByAccountProxy(targetAccountProxy); + }, [getChainSlugsByAccountProxy, targetAccountProxy]); + + const isTokenCompatibleWithTargetAccountProxy = useCallback((tokenSlug: string): boolean => { + if (!tokenSlug) { + return false; + } + const chainSlug = _getOriginChainOfAsset(tokenSlug); - const chainInfo = chainInfoMap[chainSlug]; - return !!chainInfo && isChainInfoCompatibleWithAccountProxy(chainInfo, targetAccountProxy); - }, [isChainInfoCompatibleWithAccountProxy, targetAccountProxy]); + return allowedChainSlugsForTargetAccountProxy.includes(chainSlug); + }, [allowedChainSlugsForTargetAccountProxy]); const fromTokenItems = useMemo(() => { return tokenSelectorItems.filter((item) => { const slug = item.slug; const assetInfo = assetRegistryMap[slug]; - if (!assetInfo) { - return false; - } - - if (!isTokenCompatibleWithTargetAccountProxy(slug, chainInfoMap)) { + if (!assetInfo || !isTokenCompatibleWithTargetAccountProxy(slug)) { return false; } @@ -308,7 +308,7 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { return defaultSlug === slug || _getMultiChainAsset(assetInfo) === defaultSlug; }); - }, [assetRegistryMap, chainInfoMap, defaultSlug, isTokenCompatibleWithTargetAccountProxy, tokenSelectorItems]); + }, [assetRegistryMap, defaultSlug, isTokenCompatibleWithTargetAccountProxy, tokenSelectorItems]); const toTokenItems = useMemo(() => { return tokenSelectorItems.filter((item) => item.slug !== fromTokenSlugValue); @@ -325,8 +325,8 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { const destChainValue = _getAssetOriginChain(toAssetInfo); const isSwitchable = useMemo(() => { - return isTokenCompatibleWithTargetAccountProxy(toTokenSlugValue || '', chainInfoMap); - }, [chainInfoMap, isTokenCompatibleWithTargetAccountProxy, toTokenSlugValue]); + return isTokenCompatibleWithTargetAccountProxy(toTokenSlugValue); + }, [isTokenCompatibleWithTargetAccountProxy, toTokenSlugValue]); // Unable to use useEffect due to infinity loop caused by conflict setCurrentSlippage and currentQuote const slippage = useMemo(() => { diff --git a/packages/extension-koni-ui/src/hooks/chain/index.ts b/packages/extension-koni-ui/src/hooks/chain/index.ts index d0813cca0ff..84c5c52ec64 100644 --- a/packages/extension-koni-ui/src/hooks/chain/index.ts +++ b/packages/extension-koni-ui/src/hooks/chain/index.ts @@ -14,5 +14,4 @@ export { default as useChainInfoData } from './useChainInfoData'; export { default as useChainConnection } from './useChainConnection'; export { default as useCoreCreateGetChainSlugsByAccountProxy } from './useCoreCreateGetChainSlugsByAccountProxy'; -export { default as useCoreCreateIsChainInfoCompatibleWithAccountProxy } from './useCoreCreateIsChainInfoCompatibleWithAccountProxy'; export { default as useGetChainSlugsByCurrentAccountProxy } from './useGetChainSlugsByCurrentAccountProxy'; diff --git a/packages/extension-koni-ui/src/hooks/chain/useCoreCreateIsChainInfoCompatibleWithAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useCoreCreateIsChainInfoCompatibleWithAccountProxy.tsx deleted file mode 100644 index ddcb2ac0010..00000000000 --- a/packages/extension-koni-ui/src/hooks/chain/useCoreCreateIsChainInfoCompatibleWithAccountProxy.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { _ChainInfo } from '@subwallet/chain-list/types'; -import { AccountProxy } from '@subwallet/extension-base/types'; -import { useCallback } from 'react'; - -const useCoreCreateIsChainInfoCompatibleWithAccountProxy = () => { - return useCallback((chainInfo: _ChainInfo, accountProxies: AccountProxy): boolean => { - return false; - }, []); -}; - -export default useCoreCreateIsChainInfoCompatibleWithAccountProxy; diff --git a/packages/extension-koni-ui/src/utils/chain/chain.ts b/packages/extension-koni-ui/src/utils/chain/chain.ts index 0fe82ba060c..34feef2cad3 100644 --- a/packages/extension-koni-ui/src/utils/chain/chain.ts +++ b/packages/extension-koni-ui/src/utils/chain/chain.ts @@ -67,7 +67,7 @@ export const isChainInfoCompatibleWithAccountInfo = (chainInfo: _ChainInfo, acco }; /** - * @deprecated Use hook `useCoreCreateIsChainInfoCompatibleWithAccountProxy` instead. + * @deprecated */ export const isChainCompatibleWithAccountChainTypes = (chainInfo: _ChainInfo, chainTypes: AccountChainType[]): boolean => { return chainTypes.some((chainType) => isChainInfoAccordantAccountChainType(chainInfo, chainType)); From 28914385cfa27bde7db526f4bca169ad70beb37e Mon Sep 17 00:00:00 2001 From: lw Date: Tue, 3 Jun 2025 18:32:00 +0700 Subject: [PATCH 114/178] [Issue-4297] refactor: Implement isChainInfoCompatibleWithAccountInfo --- .../Popup/Home/Earning/EarningPositionDetail/index.tsx | 6 +++--- .../src/Popup/Transaction/variants/Swap/index.tsx | 8 ++++---- .../src/components/Modal/AddressBook/AddressBookModal.tsx | 4 ++-- .../components/Modal/Earning/EarningInstructionModal.tsx | 6 +++--- packages/extension-koni-ui/src/utils/account/account.ts | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx index 005307b6569..fe93d34ae50 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx @@ -13,7 +13,7 @@ import { EarningInfoPart } from '@subwallet/extension-koni-ui/Popup/Home/Earning import { RewardInfoPart } from '@subwallet/extension-koni-ui/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart'; import { WithdrawInfoPart } from '@subwallet/extension-koni-ui/Popup/Home/Earning/EarningPositionDetail/WithdrawInfoPart'; import { EarningEntryParam, EarningEntryView, EarningPositionDetailParam, ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { getTransactionFromAccountProxyValue, isAccountAll, isChainInfoAccordantAccountChainType } from '@subwallet/extension-koni-ui/utils'; +import { getTransactionFromAccountProxyValue, isAccountAll, isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-koni-ui/utils'; import { Button, ButtonProps, Icon, Number } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; import CN from 'classnames'; @@ -52,11 +52,11 @@ function Component ({ compound, return ALL_ACCOUNT_KEY; } - const accountAddress = currentAccountProxy?.accounts.find(({ chainType }) => { + const accountAddress = currentAccountProxy?.accounts.find(({ chainType, type: accountType }) => { if (chainInfoMap[poolInfo.chain]) { const chainInfo = chainInfoMap[poolInfo.chain]; - return isChainInfoAccordantAccountChainType(chainInfo, chainType); + return isChainInfoCompatibleWithAccountInfo(chainInfo, chainType, accountType); } return false; diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx index 0405d73e423..1ae2802feb2 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { _ChainAsset, _ChainInfo, _ChainStatus } from '@subwallet/chain-list/types'; +import { _ChainAsset, _ChainStatus } from '@subwallet/chain-list/types'; import { SwapError } from '@subwallet/extension-base/background/errors/SwapError'; import { ExtrinsicType, NotificationType } from '@subwallet/extension-base/background/KoniTypes'; import { validateRecipientAddress } from '@subwallet/extension-base/core/logic-validation/recipientAddress'; @@ -28,7 +28,7 @@ import { CommonActionType, commonProcessReducer, DEFAULT_COMMON_PROCESS } from ' import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, FormCallbacks, FormFieldData, SwapParams, ThemeProps, TokenBalanceItemType } from '@subwallet/extension-koni-ui/types'; import { TokenSelectorItemType } from '@subwallet/extension-koni-ui/types/field'; -import { convertFieldToObject, findAccountByAddress, isAccountAll, isChainInfoAccordantAccountChainType, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; +import { convertFieldToObject, findAccountByAddress, isAccountAll, isChainInfoCompatibleWithAccountInfo, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; import { Button, Form, Icon, ModalContext } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; import CN from 'classnames'; @@ -373,7 +373,7 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { return false; } - return !isChainInfoAccordantAccountChainType(chainInfoMap[destChainValue], fromAccountJson.chainType); + return !isChainInfoCompatibleWithAccountInfo(chainInfoMap[destChainValue], fromAccountJson.chainType, fromAccountJson.type); }, [accounts, chainInfoMap, destChainValue, fromValue]); const recipientAddressValidator = useCallback((rule: Rule, _recipientAddress: string): Promise => { @@ -764,7 +764,7 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { return undefined; } - const accountJsonForRecipientAutoFilled = targetAccountProxy.accounts.find((a) => isChainInfoAccordantAccountChainType(destChainInfo, a.chainType)); + const accountJsonForRecipientAutoFilled = targetAccountProxy.accounts.find((a) => isChainInfoCompatibleWithAccountInfo(destChainInfo, a.chainType, a.type)); if (!accountJsonForRecipientAutoFilled) { return undefined; diff --git a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx index 7fdc3dcb960..9ee01c2f2df 100644 --- a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx @@ -6,7 +6,7 @@ import { _reformatAddressWithChain, getAccountChainTypeForAddress } from '@subwa import { AddressSelectorItem, BackIcon } from '@subwallet/extension-koni-ui/components'; import { useChainInfo, useCoreCreateReformatAddress, useFilterModal, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { getBitcoinAccountDetails, isAccountAll, isChainInfoAccordantAccountChainType } from '@subwallet/extension-koni-ui/utils'; +import { getBitcoinAccountDetails, isAccountAll, isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-koni-ui/utils'; import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; import { Badge, Icon, ModalContext, SwList, SwModal } from '@subwallet/react-ui'; import { SwListSectionRef } from '@subwallet/react-ui/es/sw-list'; @@ -102,7 +102,7 @@ const Component: React.FC = (props: Props) => { }); (!selectedFilters.length || selectedFilters.includes(AnalyzedGroup.CONTACT)) && contacts.forEach((acc) => { - if (isChainInfoAccordantAccountChainType(chainInfo, getAccountChainTypeForAddress(acc.address))) { + if (isChainInfoCompatibleWithAccountInfo(chainInfo, getAccountChainTypeForAddress(acc.address), getKeypairTypeByAddress(acc.address))) { result.push({ displayName: acc.name, formatedAddress: _reformatAddressWithChain(acc.address, chainInfo), diff --git a/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx b/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx index 1a5cc4a47e3..24b49612960 100644 --- a/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx @@ -14,7 +14,7 @@ import { EARNING_DATA_RAW, EARNING_INSTRUCTION_MODAL } from '@subwallet/extensio import { useSelector } from '@subwallet/extension-koni-ui/hooks'; import { earlyValidateJoin } from '@subwallet/extension-koni-ui/messaging'; import { AlertDialogProps, PhosphorIcon, ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { getBannerButtonIcon, getEarningTimeText, isAccountAll, isChainInfoAccordantAccountChainType } from '@subwallet/extension-koni-ui/utils'; +import { getBannerButtonIcon, getEarningTimeText, isAccountAll, isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-koni-ui/utils'; import { BackgroundIcon, Button, Icon, ModalContext, SwModal } from '@subwallet/react-ui'; import { getAlphaColor } from '@subwallet/react-ui/lib/theme/themes/default/colorAlgorithm'; import CN from 'classnames'; @@ -63,11 +63,11 @@ const Component: React.FC = (props: Props) => { return ALL_ACCOUNT_KEY; } - const accountAddress = currentAccountProxy?.accounts.find(({ chainType }) => { + const accountAddress = currentAccountProxy?.accounts.find(({ chainType, type: accountType }) => { if (chainInfoMap[poolInfo.chain]) { const chainInfo = chainInfoMap[poolInfo.chain]; - return isChainInfoAccordantAccountChainType(chainInfo, chainType); + return isChainInfoCompatibleWithAccountInfo(chainInfo, chainType, accountType); } return false; diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index e289b647540..c907bf91c45 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -19,7 +19,7 @@ import { Web3LogoMap } from '@subwallet/react-ui/es/config-provider/context'; import { decodeAddress, encodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; -import { isChainInfoAccordantAccountChainType } from '../chain'; +import { isChainInfoCompatibleWithAccountInfo } from '../chain'; import { getLogoByNetworkKey } from '../common'; export function getAccountType (address: string): AccountType { @@ -224,7 +224,7 @@ export function getReformatedAddressRelatedToChain (accountJson: AccountJson, ch return undefined; } - if (!isChainInfoAccordantAccountChainType(chainInfo, accountJson.chainType)) { + if (!isChainInfoCompatibleWithAccountInfo(chainInfo, accountJson.chainType, accountJson.type)) { return undefined; } From 93a7efa481b8428f42ba5d4853ea21bfdc11b05f Mon Sep 17 00:00:00 2001 From: lw Date: Tue, 3 Jun 2025 18:42:43 +0700 Subject: [PATCH 115/178] [Issue-4297] refactor: Update logic for isChainInfoCompatibleWithAccountInfo --- .../src/utils/chain/chain.ts | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/extension-koni-ui/src/utils/chain/chain.ts b/packages/extension-koni-ui/src/utils/chain/chain.ts index 34feef2cad3..89d344f9a0c 100644 --- a/packages/extension-koni-ui/src/utils/chain/chain.ts +++ b/packages/extension-koni-ui/src/utils/chain/chain.ts @@ -62,7 +62,41 @@ export const isChainInfoAccordantAccountChainType = (chainInfo: _ChainInfo, chai return false; }; -export const isChainInfoCompatibleWithAccountInfo = (chainInfo: _ChainInfo, accountChainType: AccountChainType, AccountType: KeypairType): boolean => { +export const isChainInfoCompatibleWithAccountInfo = (chainInfo: _ChainInfo, accountChainType: AccountChainType, accountType: KeypairType): boolean => { + if (accountChainType === AccountChainType.SUBSTRATE) { + return _isPureSubstrateChain(chainInfo); + } + + if (accountChainType === AccountChainType.ETHEREUM) { + return _isChainEvmCompatible(chainInfo); + } + + if (accountChainType === AccountChainType.TON) { + return _isChainTonCompatible(chainInfo); + } + + if (accountChainType === AccountChainType.BITCOIN) { + if (!_isChainBitcoinCompatible(chainInfo)) { + return false; + } + + const network = chainInfo.bitcoinInfo?.bitcoinNetwork; + + if (BitcoinMainnetKeypairTypes.includes(accountType)) { + return network === 'mainnet'; + } + + if (BitcoinTestnetKeypairTypes.includes(accountType)) { + return network === 'testnet'; + } + + return false; + } + + if (accountChainType === AccountChainType.CARDANO) { + return _isChainCardanoCompatible(chainInfo); + } + return false; }; From e343be2c55d6957ca4d7ae61862a71c1cf52b392 Mon Sep 17 00:00:00 2001 From: lw Date: Wed, 4 Jun 2025 11:25:11 +0700 Subject: [PATCH 116/178] [Issue-4297] refactor: Update logic for useCoreCreateGetChainSlugsByAccountProxy --- .../chain/useCoreCreateGetChainSlugsByAccountProxy.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx index 9b5b686d2ea..032eda5586f 100644 --- a/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx +++ b/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx @@ -13,15 +13,15 @@ function getChainSlugsByAccountProxySingle (accountProxySingle: AccountProxy, ch return accountProxySingle.specialChain in chainInfoMap ? [accountProxySingle.specialChain] : []; } - const result: string[] = []; + const slugSet = new Set(); for (const chainInfo of Object.values(chainInfoMap)) { if (accountProxySingle.accounts.some((account) => isChainInfoCompatibleWithAccountInfo(chainInfo, account.chainType, account.type))) { - result.push(chainInfo.slug); + slugSet.add(chainInfo.slug); } } - return result; + return [...slugSet]; } function getChainSlugsByAccountProxyAll (accountProxyAll: AccountProxy, accountProxies: AccountProxy[], chainInfoMap: Record): string[] { From e38c4a07285d5b2c97ebca5f3abea1111aa14ac5 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 4 Jun 2025 11:35:29 +0700 Subject: [PATCH 117/178] [Issue-4263] test: [ci/cd] add auto-build for task #4297; will remove once tests pass. --- .github/workflows/push-koni-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index 8c4491eda04..f3992dc90ca 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -7,6 +7,7 @@ on: - subwallet-dev - koni/dev/issue-4200-v2 - koni/dev/issue-4094-v2 + - koni/dev/issue-4297 push: branches: - koni-dev From 4881ec5108325d704d4fd61381f4c5fca9339d76 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 4 Jun 2025 11:42:03 +0700 Subject: [PATCH 118/178] [Issue-4263] test: [ci/cd] add auto-build for task #4297; will remove once tests pass (2). --- .github/workflows/push-koni-dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index f3992dc90ca..366060fc743 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -6,7 +6,6 @@ on: - upgrade-ui - subwallet-dev - koni/dev/issue-4200-v2 - - koni/dev/issue-4094-v2 - koni/dev/issue-4297 push: branches: From 5c1f98957115d2e4af85562e608800cdf1890875 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 4 Jun 2025 11:50:00 +0700 Subject: [PATCH 119/178] [Issue-4263] test: [ci/cd] add auto-build for task #4297; will remove once tests pass (3). --- .github/workflows/push-koni-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index 366060fc743..bdead2bbb70 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -7,6 +7,7 @@ on: - subwallet-dev - koni/dev/issue-4200-v2 - koni/dev/issue-4297 + - koni/dev/issue-4263 push: branches: - koni-dev From 576538639a864406ab4960d8cc52b210a252b0a2 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 4 Jun 2025 17:30:30 +0700 Subject: [PATCH 120/178] [Issue-4263] bug fix: corrected display of fee for bitcoin transfers --- .../src/core/logic-validation/transfer.ts | 2 +- .../bitcoin/strategy/BlockStreamTestnet/index.ts | 16 ++++++---------- .../src/services/transaction-service/index.ts | 1 - 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/extension-base/src/core/logic-validation/transfer.ts b/packages/extension-base/src/core/logic-validation/transfer.ts index aa3f03094cb..ef381a81aeb 100644 --- a/packages/extension-base/src/core/logic-validation/transfer.ts +++ b/packages/extension-base/src/core/logic-validation/transfer.ts @@ -384,7 +384,7 @@ export async function estimateFeeForTransaction (validationResponse: SWTransacti sender: address }); - estimateFee.value = (feeCombine.feeRate * sizeInfo.txVBytes).toString(); + estimateFee.value = Math.ceil(feeCombine.feeRate * sizeInfo.txVBytes).toString(); } else { const _transaction = transaction; const gasLimit = _transaction.gas || await evmApi.api.eth.estimateGas(_transaction); diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts index d3be59a1394..a4e9b64a287 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { SWError } from '@subwallet/extension-base/background/errors/SWError'; -import { BitcoinAddressSummaryInfo, BitcoinApiStrategy, BitcoinTransactionEventMap, BlockstreamAddressResponse, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, BlockStreamUtxo, Inscription, InscriptionFetchedData, RunesInfoByAddress, RunesInfoByAddressFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; +import { BitcoinAddressSummaryInfo, BitcoinApiStrategy, BitcoinTransactionEventMap, BlockstreamAddressResponse, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, BlockStreamUtxo, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { HiroService } from '@subwallet/extension-base/services/hiro-service'; import { RunesService } from '@subwallet/extension-base/services/rune-service'; import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; @@ -180,7 +180,7 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im getRecommendedFeeRate (): Promise { return this.addRequest(async (): Promise => { - const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); + const response = await getRequest(this.getUrl('v1/fees/recommended'), undefined, this.headers); if (!response.ok) { throw new SWError('BlockStreamTestnetRequestStrategy.getRecommendedFeeRate', `Failed to fetch fee estimates: ${response.statusText}`); @@ -192,11 +192,7 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im hourFee: 60 * 60000 }; - const estimates = await response.json() as BlockStreamFeeEstimates; - - const low = 6; - const average = 4; - const fast = 2; + const estimates = await response.json() as RecommendedFeeEstimates; const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed(), 10); @@ -204,9 +200,9 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im type: 'bitcoin', busyNetwork: false, options: { - slow: { feeRate: convertFee(estimates[low] || 10), time: convertTimeMilisec.hourFee }, - average: { feeRate: convertFee(estimates[average] || 12), time: convertTimeMilisec.halfHourFee }, - fast: { feeRate: convertFee(estimates[fast] || 15), time: convertTimeMilisec.fastestFee }, + slow: { feeRate: convertFee(estimates.hourFee || 1), time: convertTimeMilisec.hourFee }, + average: { feeRate: convertFee(estimates.halfHourFee || 1), time: convertTimeMilisec.halfHourFee }, + fast: { feeRate: convertFee(estimates.fastestFee || 1), time: convertTimeMilisec.fastestFee }, default: 'slow' } }; diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 513a855222a..3ed6fcf0ccd 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -1993,7 +1993,6 @@ export default class TransactionService { throw new Error('Bad signature'); } - console.log('Transaction Signed:', payload); // Emit signed event emitter.emit('signed', eventData); // Add start info From 6a238ecd7f18975da5bb9822e8ba59c2996e89cd Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 4 Jun 2025 17:54:06 +0700 Subject: [PATCH 121/178] [Issue-4263] bug fix: fix case displaying balance 'change output' of sender in immediate transfer --- .../strategy/BlockStreamTestnet/index.ts | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts index a4e9b64a287..1e1aa3b9fc0 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts @@ -72,9 +72,43 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im } const rsRaw = await response.json() as BlockstreamAddressResponse; + + // Fetch unconfirmed transactions from mempool + const mempoolResponse = await getRequest(this.getUrl(`address/${address}/txs/mempool`), undefined, this.headers); + + if (!mempoolResponse.ok) { + const errorText = await mempoolResponse.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.getAddressSummaryInfo', `Failed to fetch mempool transactions: ${mempoolResponse.status} - ${errorText}`); + } + + const mempoolTxs = await mempoolResponse.json() as BitcoinTx[]; + + // Calculate total value of UTXOs used in unconfirmed transactions (inputs) + let unconfirmedLocked = new BigN(0); + // Calculate total value of change outputs returning to the address + let unconfirmedChange = new BigN(0); + + for (const tx of mempoolTxs) { + // Process inputs + for (const vin of tx.vin) { + if (vin.prevout && vin.prevout.scriptpubkey_address === address) { + unconfirmedLocked = unconfirmedLocked.plus(vin.prevout.value); + } + } + + // Process outputs + for (const vout of tx.vout) { + if (vout.scriptpubkey_address === address) { + unconfirmedChange = unconfirmedChange.plus(vout.value); + } + } + } + const chainBalance = rsRaw.chain_stats.funded_txo_sum - rsRaw.chain_stats.spent_txo_sum; - const pendingLocked = rsRaw.mempool_stats.spent_txo_sum; // Only consider spent UTXOs in mempool - const availableBalance = Math.max(0, chainBalance - pendingLocked); // Ensure balance is non-negative + const unconfirmedLockedValue = parseInt(unconfirmedLocked.toString(), 10); + const unconfirmedChangeValue = parseInt(unconfirmedChange.toString(), 10); + const availableBalance = Math.max(0, chainBalance - unconfirmedLockedValue + unconfirmedChangeValue); // Ensure balance is non-negative const rs: BitcoinAddressSummaryInfo = { address: rsRaw.address, From c2a7484de487e16ec41fe9f26a4304b30f51275d Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 4 Jun 2025 17:56:45 +0700 Subject: [PATCH 122/178] [Issue-4263] add: update content --- .../src/Popup/Home/History/Detail/parts/Layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx b/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx index 6942e3e4fa1..ecb1f650b40 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/Detail/parts/Layout.tsx @@ -56,8 +56,8 @@ const Component: React.FC = (props: Props) => { valueColorSchema={HistoryStatusMap[data.status].schema} /> {extrinsicHash} - {!!data.time && ({formatHistoryDate(data.time, language, 'detail')})} - {!!data.blockTime && ({formatHistoryDate(data.blockTime, language, 'detail')})} + {!!data.time && ({formatHistoryDate(data.time, language, 'detail')})} + {!!data.blockTime && ({formatHistoryDate(data.blockTime, language, 'detail')})} { From 287f0362b1c8be0f321371eb04cb61b6f95f203d Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 4 Jun 2025 18:26:22 +0700 Subject: [PATCH 123/178] [Issue-4263] add: prioritize the value of 'block time' in history --- .../src/Popup/Home/History/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx index 0a71ae709b7..57c1903915e 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx @@ -70,7 +70,7 @@ function getIcon (item: TransactionHistoryItem): SwIconProps['phosphorIcon'] { function getDisplayData (item: TransactionHistoryItem, nameMap: Record, titleMap: Record): TransactionHistoryDisplayData { let displayData: TransactionHistoryDisplayData; - const time = customFormatDate(item.time ? item.time : item.blockTime || 0, '#hhhh#:#mm#'); + const time = customFormatDate(item.blockTime ? item.blockTime : item.time || 0, '#hhhh#:#mm#'); const displayStatus = item.status === ExtrinsicStatus.FAIL ? 'fail' : ''; @@ -385,14 +385,14 @@ function Component ({ className = '' }: Props): React.ReactElement { const getHistoryItems = useCallback((count: number) => { return Object.values(historyMap).filter(filterFunction).sort((a, b) => { - if (a.time !== 0 && b.time !== 0) { - return b.time - a.time; - } - const blockTimeA = a.blockTime ?? 0; const blockTimeB = b.blockTime ?? 0; - return blockTimeB - blockTimeA; + if (blockTimeA !== 0 && blockTimeB !== 0) { + return blockTimeB - blockTimeA; + } + + return b.time - a.time; }) .slice(0, count); }, [filterFunction, historyMap]); From 9e77659fc88fe6dc1b67702f3e1b8f58956fbded Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 5 Jun 2025 09:23:12 +0700 Subject: [PATCH 124/178] [Issue-4297] refactor: Refactor isChainInfoCompatibleWithAccountInfo to _isChainInfoCompatibleWithAccountInfo and move it to extension-base package --- .../src/services/chain-service/utils/index.ts | 39 ++++++++++++++++++ .../Earning/EarningPositionDetail/index.tsx | 5 ++- .../Popup/Transaction/variants/Swap/index.tsx | 8 ++-- .../Modal/AddressBook/AddressBookModal.tsx | 5 ++- .../Modal/Earning/EarningInstructionModal.tsx | 5 ++- ...eCoreCreateGetChainSlugsByAccountProxy.tsx | 4 +- .../src/utils/account/account.ts | 5 +-- .../src/utils/chain/chain.ts | 40 +------------------ 8 files changed, 57 insertions(+), 54 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/utils/index.ts b/packages/extension-base/src/services/chain-service/utils/index.ts index b575fe459ce..a0b82c063e7 100644 --- a/packages/extension-base/src/services/chain-service/utils/index.ts +++ b/packages/extension-base/src/services/chain-service/utils/index.ts @@ -7,6 +7,7 @@ import { _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX } from '@subwallet/extension-ba import { _ChainState, _CUSTOM_PREFIX, _DataMap, _SMART_CONTRACT_STANDARDS } from '@subwallet/extension-base/services/chain-service/types'; import { IChain } from '@subwallet/extension-base/services/storage-service/databases'; import { AccountChainType } from '@subwallet/extension-base/types'; +import { BitcoinMainnetKeypairTypes, BitcoinTestnetKeypairTypes, CardanoKeypairTypes, EthereumKeypairTypes, KeypairType, SubstrateKeypairTypes, TonKeypairTypes } from '@subwallet/keyring/types'; import { isEthereumAddress } from '@polkadot/util-crypto'; @@ -698,6 +699,44 @@ export const _chainInfoToChainType = (chainInfo: _ChainInfo): AccountChainType = return AccountChainType.SUBSTRATE; }; +export const _isChainInfoCompatibleWithAccountInfo = (chainInfo: _ChainInfo, accountChainType: AccountChainType, accountType: KeypairType): boolean => { + if (accountChainType === AccountChainType.SUBSTRATE) { + return _isPureSubstrateChain(chainInfo) && SubstrateKeypairTypes.includes(accountType); + } + + if (accountChainType === AccountChainType.ETHEREUM) { + return _isChainEvmCompatible(chainInfo) && EthereumKeypairTypes.includes(accountType); + } + + if (accountChainType === AccountChainType.TON) { + return _isChainTonCompatible(chainInfo) && TonKeypairTypes.includes(accountType); + } + + if (accountChainType === AccountChainType.CARDANO) { + return _isChainCardanoCompatible(chainInfo) && CardanoKeypairTypes.includes(accountType); + } + + if (accountChainType === AccountChainType.BITCOIN) { + if (!_isChainBitcoinCompatible(chainInfo) || ![...BitcoinMainnetKeypairTypes, ...BitcoinTestnetKeypairTypes].includes(accountType)) { + return false; + } + + const network = chainInfo.bitcoinInfo?.bitcoinNetwork; + + if (BitcoinMainnetKeypairTypes.includes(accountType)) { + return network === 'mainnet'; + } + + if (BitcoinTestnetKeypairTypes.includes(accountType)) { + return network === 'testnet'; + } + + return false; + } + + return false; +}; + export const _getAssetNetuid = (assetInfo: _ChainAsset): number => { // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unsafe-return diff --git a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx index fe93d34ae50..db91af04148 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Earning/EarningPositionDetail/index.tsx @@ -3,6 +3,7 @@ import { NotificationType } from '@subwallet/extension-base/background/KoniTypes'; import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; +import { _isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { EarningRewardHistoryItem, SpecialYieldPoolInfo, SpecialYieldPositionInfo, YieldPoolInfo, YieldPoolType, YieldPositionInfo } from '@subwallet/extension-base/types'; import { AlertModal, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; import { BN_TEN, BN_ZERO, DEFAULT_EARN_PARAMS, DEFAULT_UN_STAKE_PARAMS, EARN_TRANSACTION, UN_STAKE_TRANSACTION } from '@subwallet/extension-koni-ui/constants'; @@ -13,7 +14,7 @@ import { EarningInfoPart } from '@subwallet/extension-koni-ui/Popup/Home/Earning import { RewardInfoPart } from '@subwallet/extension-koni-ui/Popup/Home/Earning/EarningPositionDetail/RewardInfoPart'; import { WithdrawInfoPart } from '@subwallet/extension-koni-ui/Popup/Home/Earning/EarningPositionDetail/WithdrawInfoPart'; import { EarningEntryParam, EarningEntryView, EarningPositionDetailParam, ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { getTransactionFromAccountProxyValue, isAccountAll, isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-koni-ui/utils'; +import { getTransactionFromAccountProxyValue, isAccountAll } from '@subwallet/extension-koni-ui/utils'; import { Button, ButtonProps, Icon, Number } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; import CN from 'classnames'; @@ -56,7 +57,7 @@ function Component ({ compound, if (chainInfoMap[poolInfo.chain]) { const chainInfo = chainInfoMap[poolInfo.chain]; - return isChainInfoCompatibleWithAccountInfo(chainInfo, chainType, accountType); + return _isChainInfoCompatibleWithAccountInfo(chainInfo, chainType, accountType); } return false; diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx index 1ae2802feb2..3f193a5082a 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx @@ -8,7 +8,7 @@ import { validateRecipientAddress } from '@subwallet/extension-base/core/logic-v import { ActionType } from '@subwallet/extension-base/core/types'; import { AcrossErrorMsg } from '@subwallet/extension-base/services/balance-service/transfer/xcm/acrossBridge'; import { _ChainState } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetDecimals, _getAssetOriginChain, _getMultiChainAsset, _getOriginChainOfAsset, _isAssetFungibleToken, _isChainEvmCompatible, _parseAssetRefKey } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getAssetOriginChain, _getMultiChainAsset, _getOriginChainOfAsset, _isAssetFungibleToken, _isChainEvmCompatible, _isChainInfoCompatibleWithAccountInfo, _parseAssetRefKey } from '@subwallet/extension-base/services/chain-service/utils'; import { KyberSwapQuoteMetadata } from '@subwallet/extension-base/services/swap-service/handler/kyber-handler'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; import { AccountProxy, AccountProxyType, AnalyzedGroup, CommonOptimalSwapPath, ProcessType, SwapRequestResult, SwapRequestV2 } from '@subwallet/extension-base/types'; @@ -28,7 +28,7 @@ import { CommonActionType, commonProcessReducer, DEFAULT_COMMON_PROCESS } from ' import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, FormCallbacks, FormFieldData, SwapParams, ThemeProps, TokenBalanceItemType } from '@subwallet/extension-koni-ui/types'; import { TokenSelectorItemType } from '@subwallet/extension-koni-ui/types/field'; -import { convertFieldToObject, findAccountByAddress, isAccountAll, isChainInfoCompatibleWithAccountInfo, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; +import { convertFieldToObject, findAccountByAddress, isAccountAll, SortableTokenItem, sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; import { Button, Form, Icon, ModalContext } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; import CN from 'classnames'; @@ -373,7 +373,7 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { return false; } - return !isChainInfoCompatibleWithAccountInfo(chainInfoMap[destChainValue], fromAccountJson.chainType, fromAccountJson.type); + return !_isChainInfoCompatibleWithAccountInfo(chainInfoMap[destChainValue], fromAccountJson.chainType, fromAccountJson.type); }, [accounts, chainInfoMap, destChainValue, fromValue]); const recipientAddressValidator = useCallback((rule: Rule, _recipientAddress: string): Promise => { @@ -764,7 +764,7 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { return undefined; } - const accountJsonForRecipientAutoFilled = targetAccountProxy.accounts.find((a) => isChainInfoCompatibleWithAccountInfo(destChainInfo, a.chainType, a.type)); + const accountJsonForRecipientAutoFilled = targetAccountProxy.accounts.find((a) => _isChainInfoCompatibleWithAccountInfo(destChainInfo, a.chainType, a.type)); if (!accountJsonForRecipientAutoFilled) { return undefined; diff --git a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx index 9ee01c2f2df..9dc6494c2da 100644 --- a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx @@ -1,12 +1,13 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { _isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { AnalyzeAddress, AnalyzedGroup } from '@subwallet/extension-base/types'; import { _reformatAddressWithChain, getAccountChainTypeForAddress } from '@subwallet/extension-base/utils'; import { AddressSelectorItem, BackIcon } from '@subwallet/extension-koni-ui/components'; import { useChainInfo, useCoreCreateReformatAddress, useFilterModal, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { getBitcoinAccountDetails, isAccountAll, isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-koni-ui/utils'; +import { getBitcoinAccountDetails, isAccountAll } from '@subwallet/extension-koni-ui/utils'; import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; import { Badge, Icon, ModalContext, SwList, SwModal } from '@subwallet/react-ui'; import { SwListSectionRef } from '@subwallet/react-ui/es/sw-list'; @@ -102,7 +103,7 @@ const Component: React.FC = (props: Props) => { }); (!selectedFilters.length || selectedFilters.includes(AnalyzedGroup.CONTACT)) && contacts.forEach((acc) => { - if (isChainInfoCompatibleWithAccountInfo(chainInfo, getAccountChainTypeForAddress(acc.address), getKeypairTypeByAddress(acc.address))) { + if (_isChainInfoCompatibleWithAccountInfo(chainInfo, getAccountChainTypeForAddress(acc.address), getKeypairTypeByAddress(acc.address))) { result.push({ displayName: acc.name, formatedAddress: _reformatAddressWithChain(acc.address, chainInfo), diff --git a/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx b/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx index 24b49612960..2c10bb3c9df 100644 --- a/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Earning/EarningInstructionModal.tsx @@ -4,6 +4,7 @@ import { NotificationType } from '@subwallet/extension-base/background/KoniTypes'; import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; import { getValidatorLabel } from '@subwallet/extension-base/koni/api/staking/bonding/utils'; +import { _isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { _STAKING_CHAIN_GROUP } from '@subwallet/extension-base/services/earning-service/constants'; import { calculateReward } from '@subwallet/extension-base/services/earning-service/utils'; import { YieldPoolType } from '@subwallet/extension-base/types'; @@ -14,7 +15,7 @@ import { EARNING_DATA_RAW, EARNING_INSTRUCTION_MODAL } from '@subwallet/extensio import { useSelector } from '@subwallet/extension-koni-ui/hooks'; import { earlyValidateJoin } from '@subwallet/extension-koni-ui/messaging'; import { AlertDialogProps, PhosphorIcon, ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { getBannerButtonIcon, getEarningTimeText, isAccountAll, isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-koni-ui/utils'; +import { getBannerButtonIcon, getEarningTimeText, isAccountAll } from '@subwallet/extension-koni-ui/utils'; import { BackgroundIcon, Button, Icon, ModalContext, SwModal } from '@subwallet/react-ui'; import { getAlphaColor } from '@subwallet/react-ui/lib/theme/themes/default/colorAlgorithm'; import CN from 'classnames'; @@ -67,7 +68,7 @@ const Component: React.FC = (props: Props) => { if (chainInfoMap[poolInfo.chain]) { const chainInfo = chainInfoMap[poolInfo.chain]; - return isChainInfoCompatibleWithAccountInfo(chainInfo, chainType, accountType); + return _isChainInfoCompatibleWithAccountInfo(chainInfo, chainType, accountType); } return false; diff --git a/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx b/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx index 032eda5586f..292279ecec1 100644 --- a/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx +++ b/packages/extension-koni-ui/src/hooks/chain/useCoreCreateGetChainSlugsByAccountProxy.tsx @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { _ChainInfo, _ChainStatus } from '@subwallet/chain-list/types'; +import { _isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { AccountProxy } from '@subwallet/extension-base/types'; import { isAccountAll } from '@subwallet/extension-base/utils'; import { useSelector } from '@subwallet/extension-koni-ui/hooks'; -import { isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-koni-ui/utils'; import { useCallback } from 'react'; function getChainSlugsByAccountProxySingle (accountProxySingle: AccountProxy, chainInfoMap: Record): string[] { @@ -16,7 +16,7 @@ function getChainSlugsByAccountProxySingle (accountProxySingle: AccountProxy, ch const slugSet = new Set(); for (const chainInfo of Object.values(chainInfoMap)) { - if (accountProxySingle.accounts.some((account) => isChainInfoCompatibleWithAccountInfo(chainInfo, account.chainType, account.type))) { + if (accountProxySingle.accounts.some((account) => _isChainInfoCompatibleWithAccountInfo(chainInfo, account.chainType, account.type))) { slugSet.add(chainInfo.slug); } } diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index c907bf91c45..62958c45c26 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -5,7 +5,7 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { NetworkJson } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType } from '@subwallet/extension-base/background/types'; import { ALL_ACCOUNT_KEY } from '@subwallet/extension-base/constants'; -import { _getChainSubstrateAddressPrefix, _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getChainSubstrateAddressPrefix, _isChainEvmCompatible, _isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { AbstractAddressJson, AccountChainType, AccountJson, AccountProxy, AccountProxyType } from '@subwallet/extension-base/types'; import { isAccountAll, reformatAddress, uniqueStringArray } from '@subwallet/extension-base/utils'; import { DEFAULT_ACCOUNT_TYPES, EVM_ACCOUNT_TYPE, SUBSTRATE_ACCOUNT_TYPE, TON_ACCOUNT_TYPE } from '@subwallet/extension-koni-ui/constants'; @@ -19,7 +19,6 @@ import { Web3LogoMap } from '@subwallet/react-ui/es/config-provider/context'; import { decodeAddress, encodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; -import { isChainInfoCompatibleWithAccountInfo } from '../chain'; import { getLogoByNetworkKey } from '../common'; export function getAccountType (address: string): AccountType { @@ -224,7 +223,7 @@ export function getReformatedAddressRelatedToChain (accountJson: AccountJson, ch return undefined; } - if (!isChainInfoCompatibleWithAccountInfo(chainInfo, accountJson.chainType, accountJson.type)) { + if (!_isChainInfoCompatibleWithAccountInfo(chainInfo, accountJson.chainType, accountJson.type)) { return undefined; } diff --git a/packages/extension-koni-ui/src/utils/chain/chain.ts b/packages/extension-koni-ui/src/utils/chain/chain.ts index 89d344f9a0c..f269b8cd0bc 100644 --- a/packages/extension-koni-ui/src/utils/chain/chain.ts +++ b/packages/extension-koni-ui/src/utils/chain/chain.ts @@ -36,7 +36,7 @@ export const findChainInfoByChainId = (chainMap: Record, cha }; /** - * @deprecated Use `isChainInfoCompatibleWithAccountInfo` instead. + * @deprecated Use `_isChainInfoCompatibleWithAccountInfo` instead. */ export const isChainInfoAccordantAccountChainType = (chainInfo: _ChainInfo, chainType: AccountChainType): boolean => { if (chainType === AccountChainType.SUBSTRATE) { @@ -62,44 +62,6 @@ export const isChainInfoAccordantAccountChainType = (chainInfo: _ChainInfo, chai return false; }; -export const isChainInfoCompatibleWithAccountInfo = (chainInfo: _ChainInfo, accountChainType: AccountChainType, accountType: KeypairType): boolean => { - if (accountChainType === AccountChainType.SUBSTRATE) { - return _isPureSubstrateChain(chainInfo); - } - - if (accountChainType === AccountChainType.ETHEREUM) { - return _isChainEvmCompatible(chainInfo); - } - - if (accountChainType === AccountChainType.TON) { - return _isChainTonCompatible(chainInfo); - } - - if (accountChainType === AccountChainType.BITCOIN) { - if (!_isChainBitcoinCompatible(chainInfo)) { - return false; - } - - const network = chainInfo.bitcoinInfo?.bitcoinNetwork; - - if (BitcoinMainnetKeypairTypes.includes(accountType)) { - return network === 'mainnet'; - } - - if (BitcoinTestnetKeypairTypes.includes(accountType)) { - return network === 'testnet'; - } - - return false; - } - - if (accountChainType === AccountChainType.CARDANO) { - return _isChainCardanoCompatible(chainInfo); - } - - return false; -}; - /** * @deprecated */ From f0a33eeafb99c71a2b82dd670503257560f283b7 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 5 Jun 2025 10:21:34 +0700 Subject: [PATCH 125/178] [Issue-4263] feat: improve bitcoin recover status --- .../helpers/recoverHistoryStatus.ts | 26 ++++++++++++++----- .../src/services/history-service/index.ts | 11 +++++++- .../src/Popup/Home/History/index.tsx | 6 +++++ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/extension-base/src/services/history-service/helpers/recoverHistoryStatus.ts b/packages/extension-base/src/services/history-service/helpers/recoverHistoryStatus.ts index 3350fe008d8..d94900db829 100644 --- a/packages/extension-base/src/services/history-service/helpers/recoverHistoryStatus.ts +++ b/packages/extension-base/src/services/history-service/helpers/recoverHistoryStatus.ts @@ -221,6 +221,8 @@ const bitcoinRecover = async (history: TransactionHistoryItem, chainService: Cha status: HistoryRecoverStatus.UNKNOWN }; + // TODO: 1. Consider rebroadcasting transaction if stuck in mempool + try { const bitcoinApi = chainService.getBitcoinApi(chain); @@ -234,16 +236,26 @@ const bitcoinRecover = async (history: TransactionHistoryItem, chainService: Cha resolve(undefined); }, 60000); }); - const transactionDetail = await Promise.race([api.getTransactionDetail(extrinsicHash), timeout]); + const txStatus = await Promise.race([api.getTransactionStatus(extrinsicHash), timeout]); - if (transactionDetail) { - result.blockHash = transactionDetail.status.block_hash || undefined; - result.blockNumber = transactionDetail.status.block_height || undefined; - result.blockTime = transactionDetail.status.block_time ? (transactionDetail.status.block_time * 1000) : undefined; + if (!txStatus) { + return { ...result, status: HistoryRecoverStatus.API_INACTIVE }; + } + + if (txStatus.confirmed) { + const transactionDetail = await Promise.race([api.getTransactionDetail(extrinsicHash), timeout]); + + if (transactionDetail) { + result.blockHash = transactionDetail.status.block_hash || undefined; + result.blockNumber = transactionDetail.status.block_height || undefined; + result.blockTime = transactionDetail.status.block_time ? (transactionDetail.status.block_time * 1000) : undefined; + + return { ...result, status: HistoryRecoverStatus.SUCCESS }; + } - return { ...result, status: transactionDetail ? HistoryRecoverStatus.SUCCESS : HistoryRecoverStatus.TX_PENDING }; - } else { return { ...result, status: HistoryRecoverStatus.API_INACTIVE }; + } else { + return { ...result, status: HistoryRecoverStatus.TX_PENDING }; } } catch (e) { // Fail when cannot find transaction diff --git a/packages/extension-base/src/services/history-service/index.ts b/packages/extension-base/src/services/history-service/index.ts index 6c316843aa4..4de3a8c7dd4 100644 --- a/packages/extension-base/src/services/history-service/index.ts +++ b/packages/extension-base/src/services/history-service/index.ts @@ -334,6 +334,9 @@ export class HistoryService implements StoppableServiceInterface, PersistDataSer switch (recoverResult.status) { case HistoryRecoverStatus.API_INACTIVE: break; + case HistoryRecoverStatus.TX_PENDING: + delete this.#needRecoveryHistories[currentExtrinsicHash]; + break; case HistoryRecoverStatus.FAILED: case HistoryRecoverStatus.SUCCESS: updateData.status = recoverResult.status === HistoryRecoverStatus.SUCCESS ? ExtrinsicStatus.SUCCESS : ExtrinsicStatus.FAIL; @@ -380,7 +383,13 @@ export class HistoryService implements StoppableServiceInterface, PersistDataSer histories .filter((history) => { - return [ExtrinsicStatus.PROCESSING, ExtrinsicStatus.SUBMITTING].includes(history.status); + if ([ExtrinsicStatus.PROCESSING, ExtrinsicStatus.SUBMITTING].includes(history.status)) { + return true; + } else if (history.status === ExtrinsicStatus.SUCCESS && history.chainType === 'bitcoin') { + return !history.blockTime; + } + + return false; }) .filter((history) => { if (history.type === ExtrinsicType.TRANSFER_XCM) { diff --git a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx index 57c1903915e..1cf96288380 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx @@ -600,6 +600,12 @@ function Component ({ className = '' }: Props): React.ReactElement { let id: string; let isSubscribed = true; + if (!selectedChain) { + setLoading(false); + + return; + } + setLoading(true); setCurrentItemDisplayCount(DEFAULT_ITEMS_COUNT); From 8d00de07019675b47dc455e09fe9f0d2d152cbda Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 5 Jun 2025 10:22:24 +0700 Subject: [PATCH 126/178] [Issue-4412] refactor: Update logic for _analyzeAddress --- packages/extension-base/src/utils/account/analyze.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/extension-base/src/utils/account/analyze.ts b/packages/extension-base/src/utils/account/analyze.ts index a9f18ea6937..3d456318e10 100644 --- a/packages/extension-base/src/utils/account/analyze.ts +++ b/packages/extension-base/src/utils/account/analyze.ts @@ -4,8 +4,9 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { resolveAzeroAddressToDomain, resolveAzeroDomainToAddress } from '@subwallet/extension-base/koni/api/dotsama/domain'; import { _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _chainInfoToChainType, _getChainSubstrateAddressPrefix } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getChainSubstrateAddressPrefix, _isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { AbstractAddressJson, AccountChainType, AccountProxy, AddressJson, AnalyzeAddress, AnalyzedGroup, ResponseInputAccountSubscribe } from '@subwallet/extension-base/types'; +import { getKeypairTypeByAddress } from '@subwallet/keyring'; import { isAddress } from '@polkadot/util-crypto'; @@ -67,14 +68,13 @@ export const _analyzeAddress = async (data: string, accountProxies: AccountProxy const chain = chainInfo.slug; const _data = data.trim().toLowerCase(); const options: AnalyzeAddress[] = []; - const currentChainType = _chainInfoToChainType(chainInfo); let current: AnalyzeAddress | undefined; // Filter account proxies for (const accountProxy of accountProxies) { const _name = accountProxy.name.trim().toLowerCase(); const nameCondition = isNameValid(_data, _name); - const filterAccounts = accountProxy.accounts.filter((account) => account.chainType === currentChainType); + const filterAccounts = accountProxy.accounts.filter((account) => _isChainInfoCompatibleWithAccountInfo(chainInfo, account.chainType, account.type)); for (const account of filterAccounts) { const addressCondition = isStrValidWithAddress(_data, account, chainInfo); @@ -108,7 +108,7 @@ export const _analyzeAddress = async (data: string, accountProxies: AccountProxy } } - const filterContacts = contacts.filter((contact) => contact.chainType === currentChainType); + const filterContacts = contacts.filter((contact) => _isChainInfoCompatibleWithAccountInfo(chainInfo, contact.chainType, getKeypairTypeByAddress(contact.address))); // Filter address book addresses for (const contact of filterContacts) { From c3d37bb58833e3e938a0fbc8bb40cf782637d34b Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 5 Jun 2025 11:14:38 +0700 Subject: [PATCH 127/178] [Issue-4412] refactor: Update UI logic for address suggestion --- .../src/components/Field/AddressInputNew.tsx | 32 +++++++++++-------- .../Modal/AddressBook/AddressBookModal.tsx | 22 ++----------- .../src/utils/sort/address.ts | 24 ++++++++++++++ .../extension-koni-ui/src/utils/sort/index.ts | 1 + 4 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 packages/extension-koni-ui/src/utils/sort/address.ts diff --git a/packages/extension-koni-ui/src/components/Field/AddressInputNew.tsx b/packages/extension-koni-ui/src/components/Field/AddressInputNew.tsx index 844d979cb21..87448d88e48 100644 --- a/packages/extension-koni-ui/src/components/Field/AddressInputNew.tsx +++ b/packages/extension-koni-ui/src/components/Field/AddressInputNew.tsx @@ -15,7 +15,7 @@ import useGetChainInfo from '@subwallet/extension-koni-ui/hooks/screen/common/us import { cancelSubscription, saveRecentAccount, subscribeAccountsInputAddress } from '@subwallet/extension-koni-ui/messaging'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { ScannerResult, ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { toShort } from '@subwallet/extension-koni-ui/utils'; +import { sortFuncAnalyzeAddress, toShort } from '@subwallet/extension-koni-ui/utils'; import { isAddress } from '@subwallet/keyring'; import { AutoComplete, Button, Icon, Input, ModalContext, Switch, SwQrScanner } from '@subwallet/react-ui'; import CN from 'classnames'; @@ -199,10 +199,10 @@ function Component (props: Props, ref: ForwardedRef): React.Rea } const result: AutoCompleteGroupItem[] = []; - const walletItems: AutoCompleteItem[] = []; - const contactItems: AutoCompleteItem[] = []; - const domainItems: AutoCompleteItem[] = []; - const recentItems: AutoCompleteItem[] = []; + const walletItems: AnalyzeAddress[] = []; + const contactItems: AnalyzeAddress[] = []; + const domainItems: AnalyzeAddress[] = []; + const recentItems: AnalyzeAddress[] = []; const genAutoCompleteItem = (responseOption: AnalyzeAddress): AutoCompleteItem => { return { @@ -210,7 +210,7 @@ function Component (props: Props, ref: ForwardedRef): React.Rea label: ( ), @@ -227,30 +227,34 @@ function Component (props: Props, ref: ForwardedRef): React.Rea responseOptions.forEach((ro) => { if (ro.analyzedGroup === AnalyzedGroup.WALLET) { - walletItems.push(genAutoCompleteItem(ro)); + walletItems.push(ro); } else if (ro.analyzedGroup === AnalyzedGroup.CONTACT) { - contactItems.push(genAutoCompleteItem(ro)); + contactItems.push(ro); } else if (ro.analyzedGroup === AnalyzedGroup.DOMAIN) { - domainItems.push(genAutoCompleteItem(ro)); + domainItems.push(ro); } else if (ro.analyzedGroup === AnalyzedGroup.RECENT) { - recentItems.push(genAutoCompleteItem(ro)); + recentItems.push(ro); } }); if (walletItems.length) { - result.push(genAutoCompleteGroupItem(t('My wallet'), walletItems)); + walletItems.sort(sortFuncAnalyzeAddress); + result.push(genAutoCompleteGroupItem(t('My wallet'), walletItems.map((i) => genAutoCompleteItem(i)))); } if (contactItems.length) { - result.push(genAutoCompleteGroupItem(t('My contact'), contactItems)); + contactItems.sort(sortFuncAnalyzeAddress); + result.push(genAutoCompleteGroupItem(t('My contact'), contactItems.map((i) => genAutoCompleteItem(i)))); } if (domainItems.length) { - result.push(genAutoCompleteGroupItem(t('Domain name'), domainItems)); + domainItems.sort(sortFuncAnalyzeAddress); + result.push(genAutoCompleteGroupItem(t('Domain name'), domainItems.map((i) => genAutoCompleteItem(i)))); } if (recentItems.length) { - result.push(genAutoCompleteGroupItem(t('Recent'), recentItems)); + recentItems.sort(sortFuncAnalyzeAddress); + result.push(genAutoCompleteGroupItem(t('Recent'), recentItems.map((i) => genAutoCompleteItem(i)))); } return result; diff --git a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx index 9dc6494c2da..d5f36feef1b 100644 --- a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx @@ -7,8 +7,8 @@ import { _reformatAddressWithChain, getAccountChainTypeForAddress } from '@subwa import { AddressSelectorItem, BackIcon } from '@subwallet/extension-koni-ui/components'; import { useChainInfo, useCoreCreateReformatAddress, useFilterModal, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { getBitcoinAccountDetails, isAccountAll } from '@subwallet/extension-koni-ui/utils'; -import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; +import { isAccountAll, sortFuncAnalyzeAddress } from '@subwallet/extension-koni-ui/utils'; +import { getKeypairTypeByAddress } from '@subwallet/keyring'; import { Badge, Icon, ModalContext, SwList, SwModal } from '@subwallet/react-ui'; import { SwListSectionRef } from '@subwallet/react-ui/es/sw-list'; import CN from 'classnames'; @@ -138,23 +138,7 @@ const Component: React.FC = (props: Props) => { // todo: may need better solution for this sorting below return result - .sort((a: AnalyzeAddress, b: AnalyzeAddress) => { - const _isABitcoin = isBitcoinAddress(a.address); - const _isBBitcoin = isBitcoinAddress(b.address); - const _isSameProxyId = a.proxyId === b.proxyId; - - if (_isABitcoin && _isBBitcoin && _isSameProxyId) { - const aKeyPairType = getKeypairTypeByAddress(a.address); - const bKeyPairType = getKeypairTypeByAddress(b.address); - - const aDetails = getBitcoinAccountDetails(aKeyPairType); - const bDetails = getBitcoinAccountDetails(bKeyPairType); - - return aDetails.order - bDetails.order; - } - - return ((a?.displayName || '').toLowerCase() > (b?.displayName || '').toLowerCase()) ? 1 : -1; - }) + .sort(sortFuncAnalyzeAddress) .sort((a, b) => getGroupPriority(b) - getGroupPriority(a)); }, [accountProxies, chainInfo, chainSlug, contacts, getReformatAddress, recent, selectedFilters]); diff --git a/packages/extension-koni-ui/src/utils/sort/address.ts b/packages/extension-koni-ui/src/utils/sort/address.ts new file mode 100644 index 00000000000..f097b659b9d --- /dev/null +++ b/packages/extension-koni-ui/src/utils/sort/address.ts @@ -0,0 +1,24 @@ +// Copyright 2019-2022 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { AnalyzeAddress } from '@subwallet/extension-base/types'; +import { getBitcoinAccountDetails } from '@subwallet/extension-koni-ui/utils'; +import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; + +export const sortFuncAnalyzeAddress = (a: AnalyzeAddress, b: AnalyzeAddress) => { + const _isABitcoin = isBitcoinAddress(a.address); + const _isBBitcoin = isBitcoinAddress(b.address); + const _isSameProxyId = a.proxyId === b.proxyId; + + if (_isABitcoin && _isBBitcoin && _isSameProxyId) { + const aKeyPairType = getKeypairTypeByAddress(a.address); + const bKeyPairType = getKeypairTypeByAddress(b.address); + + const aDetails = getBitcoinAccountDetails(aKeyPairType); + const bDetails = getBitcoinAccountDetails(bKeyPairType); + + return aDetails.order - bDetails.order; + } + + return ((a?.displayName || '').toLowerCase() > (b?.displayName || '').toLowerCase()) ? 1 : -1; +}; diff --git a/packages/extension-koni-ui/src/utils/sort/index.ts b/packages/extension-koni-ui/src/utils/sort/index.ts index 14a673715ab..90b3bc265f2 100644 --- a/packages/extension-koni-ui/src/utils/sort/index.ts +++ b/packages/extension-koni-ui/src/utils/sort/index.ts @@ -4,3 +4,4 @@ export * from './crowdloan'; export * from './token'; export * from './staking'; +export * from './address'; From 69a650f67152fbedb5f459b359ceb67dafd87113 Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 5 Jun 2025 11:29:57 +0700 Subject: [PATCH 128/178] [Issue-4412] refactor: Update UI logic for Token Selector --- .../ReceiveModalNew/parts/TokenSelector.tsx | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/TokenSelector.tsx b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/TokenSelector.tsx index 7639a0a1476..e8c00a19bc2 100644 --- a/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/TokenSelector.tsx +++ b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/TokenSelector.tsx @@ -41,25 +41,7 @@ function Component ({ className = '', items, onCancel, onSelectItem }: Props): R return item.symbol.toLowerCase().includes(currentSearchText.toLowerCase()) || chainName.toLowerCase().includes(currentSearchText.toLowerCase()); }); - if (!currentSearchText) { - sortTokensByStandard(filteredList, priorityTokens); - - return filteredList; - } - - if (currentSearchText.toLowerCase() === 'ton') { - const tonItemIndex = filteredList.findIndex((item) => item.slug === 'ton-NATIVE-TON'); - - if (tonItemIndex !== -1) { - const [tonItem] = filteredList.splice(tonItemIndex, 1); - - if (tonItem) { - filteredList.unshift(tonItem); - } - } - - return filteredList; - } + sortTokensByStandard(filteredList, priorityTokens); return filteredList; }, [chainInfoMap, currentSearchText, items, priorityTokens]); From 3907eaf9307405311eb416eb76efb0e33d5f6644 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 5 Jun 2025 12:04:55 +0700 Subject: [PATCH 129/178] [Issue-4263] bug fix: correctly display balance on testnet after transfer --- .../strategy/BlockStreamTestnet/index.ts | 39 ++----------------- 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts index 1e1aa3b9fc0..47839e17456 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts @@ -72,43 +72,10 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im } const rsRaw = await response.json() as BlockstreamAddressResponse; - - // Fetch unconfirmed transactions from mempool - const mempoolResponse = await getRequest(this.getUrl(`address/${address}/txs/mempool`), undefined, this.headers); - - if (!mempoolResponse.ok) { - const errorText = await mempoolResponse.text(); - - throw new SWError('BlockStreamTestnetRequestStrategy.getAddressSummaryInfo', `Failed to fetch mempool transactions: ${mempoolResponse.status} - ${errorText}`); - } - - const mempoolTxs = await mempoolResponse.json() as BitcoinTx[]; - - // Calculate total value of UTXOs used in unconfirmed transactions (inputs) - let unconfirmedLocked = new BigN(0); - // Calculate total value of change outputs returning to the address - let unconfirmedChange = new BigN(0); - - for (const tx of mempoolTxs) { - // Process inputs - for (const vin of tx.vin) { - if (vin.prevout && vin.prevout.scriptpubkey_address === address) { - unconfirmedLocked = unconfirmedLocked.plus(vin.prevout.value); - } - } - - // Process outputs - for (const vout of tx.vout) { - if (vout.scriptpubkey_address === address) { - unconfirmedChange = unconfirmedChange.plus(vout.value); - } - } - } - const chainBalance = rsRaw.chain_stats.funded_txo_sum - rsRaw.chain_stats.spent_txo_sum; - const unconfirmedLockedValue = parseInt(unconfirmedLocked.toString(), 10); - const unconfirmedChangeValue = parseInt(unconfirmedChange.toString(), 10); - const availableBalance = Math.max(0, chainBalance - unconfirmedLockedValue + unconfirmedChangeValue); // Ensure balance is non-negative + const pendingLocked = rsRaw.mempool_stats.spent_txo_sum; // Only consider spent UTXOs in mempool + const mempoolReceived = rsRaw.mempool_stats.funded_txo_sum; // Funds received in mempool (e.g., change) + const availableBalance = Math.max(0, chainBalance - pendingLocked + mempoolReceived); // Ensure balance is non-negative const rs: BitcoinAddressSummaryInfo = { address: rsRaw.address, From 0d899bf79b559a5edbaa74c90bdbd806d42c3cae Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 5 Jun 2025 12:33:46 +0700 Subject: [PATCH 130/178] [Issue-4263] chore: add a note about fallback for the "estimate fee" route. --- .../handler/bitcoin/strategy/BlockStreamTestnet/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts index 47839e17456..99b3b79f664 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts @@ -179,6 +179,7 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im }, 0); } + // TODO: Handle fallback for this route as it is not stable. getRecommendedFeeRate (): Promise { return this.addRequest(async (): Promise => { const response = await getRequest(this.getUrl('v1/fees/recommended'), undefined, this.headers); From e5b9e25e7efe4502bf68baad8ff053bb3d2868d2 Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 5 Jun 2025 14:14:43 +0700 Subject: [PATCH 131/178] [Issue-4412] refactor: Update UI for AddressQrModal --- .../Modal/Global/AddressQrModal.tsx | 84 ++++++++++--------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx index 535931b5c56..2d3d52bad0d 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx @@ -144,6 +144,40 @@ const Component: React.FC = ({ accountTokenAddresses = [], address: initi : undefined } destroyOnClose={true} + footer={isNewFormat === undefined || isNewFormat + ? ( + + ) + : ( + + )} id={modalId} onCancel={onBack || onCancel} rightIconProps={onBack @@ -185,7 +219,7 @@ const Component: React.FC = ({ accountTokenAddresses = [], address: initi color='#000' errorLevel='H' icon={''} - size={264} + size={232} value={currentAddress} /> @@ -250,41 +284,6 @@ const Component: React.FC = ({ accountTokenAddresses = [], address: initi
- - {isNewFormat === undefined || isNewFormat - ? ( - - ) - : ( - - )} {isRelatedToTon && isTonWalletContactSelectorModalActive && @@ -302,6 +301,14 @@ const Component: React.FC = ({ accountTokenAddresses = [], address: initi const AddressQrModal = styled(Component)(({ theme: { token } }: Props) => { return { + '.ant-sw-modal-footer': { + borderTop: 0 + }, + + '.ant-sw-modal-body': { + paddingBottom: 0 + }, + '.__qr-code-wrapper': { paddingTop: token.padding, paddingBottom: token.padding, @@ -324,6 +331,7 @@ const AddressQrModal = styled(Component)(({ theme: { token } }: Props) => display: 'flex', alignItems: 'center', justifyContent: 'center', + marginBottom: 2, '.__label-address-prefix': { fontWeight: 700, @@ -356,7 +364,7 @@ const AddressQrModal = styled(Component)(({ theme: { token } }: Props) => }, '.__address-box-wrapper': { - marginBottom: token.margin + }, '.__address-box': { @@ -366,7 +374,7 @@ const AddressQrModal = styled(Component)(({ theme: { token } }: Props) => justifyContent: 'center', paddingLeft: token.paddingSM, paddingRight: token.paddingXXS, - minHeight: 48 + minHeight: 52 }, '.__address': { From 27ece23993b9249eab3dee2fcdebdc3e410fd84a Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 5 Jun 2025 15:27:21 +0700 Subject: [PATCH 132/178] [Issue-4412] refactor: Update sorting logic for GlobalSearchTokenModal --- .../src/Popup/Home/Tokens/index.tsx | 6 +- .../src/Popup/Home/index.tsx | 2 +- .../Modal/GlobalSearchTokenModal.tsx | 38 +++++-------- .../src/contexts/screen/HomeContext.tsx | 4 +- .../src/hooks/screen/home/useTokenGroup.ts | 55 ++----------------- packages/extension-koni-ui/src/types/hook.ts | 4 +- 6 files changed, 28 insertions(+), 81 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx index ce4d080af7f..c83044efe6f 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx @@ -44,7 +44,7 @@ const Component = (): React.ReactElement => { const currentAccountProxy = useSelector((state: RootState) => state.accountState.currentAccountProxy); const isAllAccount = useSelector((state: RootState) => state.accountState.isAllAccount); const { accountBalance: { tokenGroupBalanceMap, - totalBalanceInfo }, tokenGroupStructure: { sortedTokenGroups } } = useContext(HomeContext); + totalBalanceInfo }, tokenGroupStructure: { tokenGroups } } = useContext(HomeContext); const notify = useNotification(); const { onOpenReceive, receiveModalProps } = useCoreReceiveModalHelper(); const priorityTokens = useSelector((state: RootState) => state.chainStore.priorityTokens); @@ -277,7 +277,7 @@ const Component = (): React.ReactElement => { const tokenGroupBalanceItems = useMemo((): TokenBalanceItemType[] => { const result: TokenBalanceItemType[] = []; - sortedTokenGroups.forEach((tokenGroupSlug) => { + tokenGroups.forEach((tokenGroupSlug) => { if (debouncedTokenGroupBalanceMap[tokenGroupSlug]) { result.push(debouncedTokenGroupBalanceMap[tokenGroupSlug]); } @@ -286,7 +286,7 @@ const Component = (): React.ReactElement => { sortTokensByStandard(result, priorityTokens, true); return result; - }, [sortedTokenGroups, debouncedTokenGroupBalanceMap, priorityTokens]); + }, [tokenGroups, debouncedTokenGroupBalanceMap, priorityTokens]); useEffect(() => { window.addEventListener('resize', handleResize); diff --git a/packages/extension-koni-ui/src/Popup/Home/index.tsx b/packages/extension-koni-ui/src/Popup/Home/index.tsx index 11b03078d59..baf10ded8b3 100644 --- a/packages/extension-koni-ui/src/Popup/Home/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/index.tsx @@ -127,8 +127,8 @@ function Component ({ className = '' }: Props): React.ReactElement { ); diff --git a/packages/extension-koni-ui/src/components/Modal/GlobalSearchTokenModal.tsx b/packages/extension-koni-ui/src/components/Modal/GlobalSearchTokenModal.tsx index d6a52ee268c..84987d7d9f1 100644 --- a/packages/extension-koni-ui/src/components/Modal/GlobalSearchTokenModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/GlobalSearchTokenModal.tsx @@ -5,8 +5,9 @@ import { TokenBalanceSelectionItem, TokenEmptyList } from '@subwallet/extension- import Search from '@subwallet/extension-koni-ui/components/Search'; import { useSelector, useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { useChainAssets } from '@subwallet/extension-koni-ui/hooks/assets'; +import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountBalanceHookType, ThemeProps, TokenBalanceItemType, TokenGroupHookType } from '@subwallet/extension-koni-ui/types'; -import { sortTokenByValue } from '@subwallet/extension-koni-ui/utils'; +import { sortTokensByBalanceInSelector } from '@subwallet/extension-koni-ui/utils'; import { SwList, SwModal } from '@subwallet/react-ui'; import React, { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -16,15 +17,15 @@ type Props = ThemeProps & { id: string, onCancel: () => void, tokenBalanceMap: AccountBalanceHookType['tokenBalanceMap'], - sortedTokenSlugs: TokenGroupHookType['sortedTokenSlugs'], + tokenSlugs: TokenGroupHookType['tokenSlugs'], } function getTokenBalances ( tokenBalanceMap: AccountBalanceHookType['tokenBalanceMap'], - sortedTokenSlugs: TokenGroupHookType['sortedTokenSlugs']): TokenBalanceItemType[] { + tokenSlugs: TokenGroupHookType['tokenSlugs']): TokenBalanceItemType[] { const result: TokenBalanceItemType[] = []; - sortedTokenSlugs.forEach((tokenSlug) => { + tokenSlugs.forEach((tokenSlug) => { if (tokenBalanceMap[tokenSlug]) { result.push(tokenBalanceMap[tokenSlug]); } @@ -33,18 +34,23 @@ function getTokenBalances ( return result; } -function Component ({ className = '', id, onCancel, sortedTokenSlugs, tokenBalanceMap }: Props): React.ReactElement { +function Component ({ className = '', id, onCancel, tokenBalanceMap, tokenSlugs }: Props): React.ReactElement { const { t } = useTranslation(); const navigate = useNavigate(); const { chainInfoMap } = useSelector((state) => state.chainStore); const { multiChainAssetMap } = useSelector((state) => state.assetRegistry); const assetRegistry = useChainAssets({ isActive: true }).chainAssetRegistry; + const priorityTokens = useSelector((state: RootState) => state.chainStore.priorityTokens); const [currentSearchText, setCurrentSearchText] = useState(''); const tokenBalances = useMemo(() => { - return getTokenBalances(tokenBalanceMap, sortedTokenSlugs).sort(sortTokenByValue); - }, [tokenBalanceMap, sortedTokenSlugs]); + const result = getTokenBalances(tokenBalanceMap, tokenSlugs); + + sortTokensByBalanceInSelector(result, priorityTokens); + + return result; + }, [tokenBalanceMap, tokenSlugs, priorityTokens]); const onClickItem = useCallback((item: TokenBalanceItemType) => { return () => { @@ -76,7 +82,7 @@ function Component ({ className = '', id, onCancel, sortedTokenSlugs, tokenBalan ); const filteredItems = useMemo(() => { - const filteredTokenBalances = tokenBalances.filter((item) => { + return tokenBalances.filter((item) => { const searchTextLowerCase = currentSearchText.toLowerCase(); const chainName = chainInfoMap[item.chain || '']?.name?.toLowerCase(); const symbol = item.symbol.toLowerCase(); @@ -86,22 +92,6 @@ function Component ({ className = '', id, onCancel, sortedTokenSlugs, tokenBalan chainName.includes(searchTextLowerCase) ); }); - - if (currentSearchText.toLowerCase() === 'ton') { - const tonItemIndex = filteredTokenBalances.findIndex((item) => item.slug === 'ton-NATIVE-TON'); - - if (tonItemIndex !== -1) { - const [tonItem] = filteredTokenBalances.splice(tonItemIndex, 1); - - if (tonItem) { - filteredTokenBalances.unshift(tonItem); - } - } - - return filteredTokenBalances; - } else { - return filteredTokenBalances; - } }, [chainInfoMap, currentSearchText, tokenBalances]); const renderEmpty = useCallback(() => { diff --git a/packages/extension-koni-ui/src/contexts/screen/HomeContext.tsx b/packages/extension-koni-ui/src/contexts/screen/HomeContext.tsx index 66586ed6144..dadbbf88a15 100644 --- a/packages/extension-koni-ui/src/contexts/screen/HomeContext.tsx +++ b/packages/extension-koni-ui/src/contexts/screen/HomeContext.tsx @@ -20,7 +20,7 @@ export const HomeContext = React.createContext({ }, tokenGroupStructure: { tokenGroupMap: {}, - sortedTokenGroups: [], - sortedTokenSlugs: [] + tokenGroups: [], + tokenSlugs: [] } }); diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useTokenGroup.ts b/packages/extension-koni-ui/src/hooks/screen/home/useTokenGroup.ts index 1ef2a435bc8..108d77f9037 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useTokenGroup.ts +++ b/packages/extension-koni-ui/src/hooks/screen/home/useTokenGroup.ts @@ -3,7 +3,7 @@ import { _ChainAsset } from '@subwallet/chain-list/types'; import { _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX } from '@subwallet/extension-base/services/chain-service/constants'; -import { _getMultiChainAsset, _isNativeTokenBySlug } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getMultiChainAsset } from '@subwallet/extension-base/services/chain-service/utils'; import { useIsMantaPayEnabled } from '@subwallet/extension-koni-ui/hooks/account/useIsMantaPayEnabled'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AssetRegistryStore } from '@subwallet/extension-koni-ui/stores/types'; @@ -12,51 +12,11 @@ import { isTokenAvailable } from '@subwallet/extension-koni-ui/utils/chain/chain import { useMemo } from 'react'; import { useSelector } from 'react-redux'; -function sortTokenSlugs (tokenSlugs: string[]) { - tokenSlugs.sort((a, b) => { - const hasNativeA = _isNativeTokenBySlug(a); - const hasNativeB = _isNativeTokenBySlug(b); - - if (hasNativeA && !hasNativeB) { - return -1; // if only element a has "NATIVE", a comes before b - } else if (!hasNativeA && hasNativeB) { - return 1; // if only element b has "NATIVE", a comes after b - } else { - return a.localeCompare(b); // if both elements have "native" or neither does, sort alphabetically - } - }); -} - -function sortTokenGroupMap (tokenGroupMap: TokenGroupHookType['tokenGroupMap']) { - Object.keys(tokenGroupMap).forEach((tokenGroup) => { - sortTokenSlugs(tokenGroupMap[tokenGroup]); - }); -} - -const prioritizedTokenGroups = ['DOT-Polkadot', 'KSM-Kusama']; - -function sortTokenGroups (tokenGroups: string[]) { - tokenGroups.sort((a, b) => { - const indexA = prioritizedTokenGroups.indexOf(a); - const indexB = prioritizedTokenGroups.indexOf(b); - - if (indexA === -1 && indexB === -1) { - return a.localeCompare(b); // if both elements are not in the prioritizedTokenGroups array, sort alphabetically - } else if (indexA === -1) { - return 1; // if only element b is in the prioritizedTokenGroups array, a comes after b - } else if (indexB === -1) { - return -1; // if only element a is in the prioritizedTokenGroups array, a comes before b - } else { - return indexA - indexB; // if both elements are in the prioritizedTokenGroups array, sort by their position in the array - } - }); -} - function getTokenGroup (assetRegistryMap: AssetRegistryStore['assetRegistry'], filteredChains?: string[]): TokenGroupHookType { const result: TokenGroupHookType = { tokenGroupMap: {}, - sortedTokenGroups: [], - sortedTokenSlugs: [] + tokenGroups: [], + tokenSlugs: [] }; Object.values(assetRegistryMap).forEach((chainAsset) => { @@ -73,15 +33,12 @@ function getTokenGroup (assetRegistryMap: AssetRegistryStore['assetRegistry'], f result.tokenGroupMap[tokenGroupKey].push(chainAsset.slug); } else { result.tokenGroupMap[tokenGroupKey] = [chainAsset.slug]; - result.sortedTokenGroups.push(tokenGroupKey); + result.tokenGroups.push(tokenGroupKey); } }); - sortTokenGroupMap(result.tokenGroupMap); - sortTokenGroups(result.sortedTokenGroups); - - result.sortedTokenGroups.forEach((tokenGroup) => { - result.sortedTokenSlugs.push(...result.tokenGroupMap[tokenGroup]); + result.tokenGroups.forEach((tokenGroup) => { + result.tokenSlugs.push(...result.tokenGroupMap[tokenGroup]); }); return result; diff --git a/packages/extension-koni-ui/src/types/hook.ts b/packages/extension-koni-ui/src/types/hook.ts index 3b4c98e0a96..d58b2ff33e5 100644 --- a/packages/extension-koni-ui/src/types/hook.ts +++ b/packages/extension-koni-ui/src/types/hook.ts @@ -6,8 +6,8 @@ import BigN from 'bignumber.js'; export type TokenGroupHookType = { tokenGroupMap: Record, - sortedTokenGroups: string[], - sortedTokenSlugs: string[], + tokenGroups: string[], + tokenSlugs: string[], } export type AccountBalanceHookType = { From 5e6e594bdbdeb8eb0ba907665ae65acc36069e5d Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 5 Jun 2025 15:28:40 +0700 Subject: [PATCH 133/178] [Issue-4263] feat: handle fallback for the "estimate fee" route. --- .../strategy/BlockStreamTestnet/index.ts | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts index 99b3b79f664..e5ce64da86e 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts @@ -182,32 +182,49 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im // TODO: Handle fallback for this route as it is not stable. getRecommendedFeeRate (): Promise { return this.addRequest(async (): Promise => { - const response = await getRequest(this.getUrl('v1/fees/recommended'), undefined, this.headers); - - if (!response.ok) { - throw new SWError('BlockStreamTestnetRequestStrategy.getRecommendedFeeRate', `Failed to fetch fee estimates: ${response.statusText}`); - } - const convertTimeMilisec = { fastestFee: 10 * 60000, halfHourFee: 30 * 60000, hourFee: 60 * 60000 }; - const estimates = await response.json() as RecommendedFeeEstimates; - - const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed(), 10); - - return { + const defaultFeeInfo: BitcoinFeeInfo = { type: 'bitcoin', busyNetwork: false, options: { - slow: { feeRate: convertFee(estimates.hourFee || 1), time: convertTimeMilisec.hourFee }, - average: { feeRate: convertFee(estimates.halfHourFee || 1), time: convertTimeMilisec.halfHourFee }, - fast: { feeRate: convertFee(estimates.fastestFee || 1), time: convertTimeMilisec.fastestFee }, + slow: { feeRate: 1, time: convertTimeMilisec.hourFee }, + average: { feeRate: 1, time: convertTimeMilisec.halfHourFee }, + fast: { feeRate: 1, time: convertTimeMilisec.fastestFee }, default: 'slow' } }; + + try { + const response = await getRequest(this.getUrl('v1/fees/recommended'), undefined, this.headers); + + if (!response.ok) { + console.warn(`Failed to fetch fee estimates: ${response.statusText}`); + + return defaultFeeInfo; + } + + const estimates = await response.json() as RecommendedFeeEstimates; + + const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed(), 10); + + return { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: convertFee(estimates.hourFee || 1), time: convertTimeMilisec.hourFee }, + average: { feeRate: convertFee(estimates.halfHourFee || 1), time: convertTimeMilisec.halfHourFee }, + fast: { feeRate: convertFee(estimates.fastestFee || 1), time: convertTimeMilisec.fastestFee }, + default: 'slow' + } + }; + } catch { + return defaultFeeInfo; + } }, 0); } From 6a0af62483596033ec6871ac64fb6f609fc3e6fe Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 5 Jun 2025 18:43:21 +0700 Subject: [PATCH 134/178] [Issue-4263] feat: change api to mempool testnet v4 --- .../chain-service/handler/bitcoin/BitcoinApi.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts index e605556e2a7..19305144f8d 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts @@ -1,9 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import '@polkadot/types-augment'; - -import { BlockStreamTestnetRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet'; +import { BlockStreamTestnetRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockstreamTestnet'; import { SubWalletMainnetRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/SubWalletMainnet'; import { BitcoinApiStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { createPromiseHandler, PromiseHandler } from '@subwallet/extension-base/utils/promise'; @@ -13,6 +11,8 @@ import { _ApiOptions } from '../../handler/types'; import { _BitcoinApi, _ChainConnectionStatus } from '../../types'; // const isBlockStreamProvider = (apiUrl: string): boolean => apiUrl === 'https://blockstream-testnet.openbit.app' || apiUrl === 'https://electrs.openbit.app'; +// const BLOCKSTREAM_TESTNET_API_URL = 'https://blockstream.info/testnet/api/'; +const MEMPOOL_TESTNET_V4_API_URL = 'https://mempool.space/testnet4/api/'; export class BitcoinApi implements _BitcoinApi { chainSlug: string; @@ -37,7 +37,7 @@ export class BitcoinApi implements _BitcoinApi { const isTestnet = apiUrl.includes('testnet'); if (isTestnet) { - this.api = new BlockStreamTestnetRequestStrategy(apiUrl); + this.api = new BlockStreamTestnetRequestStrategy(MEMPOOL_TESTNET_V4_API_URL); } else { this.api = new SubWalletMainnetRequestStrategy(apiUrl); } @@ -80,7 +80,7 @@ export class BitcoinApi implements _BitcoinApi { const isTestnet = apiUrl.includes('testnet'); if (isTestnet) { - this.api = new BlockStreamTestnetRequestStrategy(apiUrl); + this.api = new BlockStreamTestnetRequestStrategy(MEMPOOL_TESTNET_V4_API_URL); } else { this.api = new SubWalletMainnetRequestStrategy(apiUrl); } From 246550f5daf77c37579c18fb098abcea697d83bd Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 5 Jun 2025 18:53:54 +0700 Subject: [PATCH 135/178] [Issue-4263] chores: fix eslint --- .../src/services/chain-service/handler/bitcoin/BitcoinApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts index 19305144f8d..7681b6e3009 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { BlockStreamTestnetRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockstreamTestnet'; +import { BlockStreamTestnetRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet'; import { SubWalletMainnetRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/SubWalletMainnet'; import { BitcoinApiStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { createPromiseHandler, PromiseHandler } from '@subwallet/extension-base/utils/promise'; From 337d53f76ef166af3fd91a9b6371bd45d045b7c1 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 5 Jun 2025 19:48:51 +0700 Subject: [PATCH 136/178] [Issue-4263] feat: trigger balance after receiving extrinsichash when transferring btc --- .../src/services/balance-service/index.ts | 49 +++++++++++++++++++ .../src/services/transaction-service/index.ts | 9 ++++ 2 files changed, 58 insertions(+) diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 243bf085212..5eb1acddc17 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -442,6 +442,55 @@ export class BalanceService implements StoppableServiceInterface { }; } + async runSubscribeBalanceForAddress (address: string, chain: string, asset: string, extrinsicType?: ExtrinsicType) { + await Promise.all([this.state.eventService.waitKeyringReady, this.state.eventService.waitChainReady]); + + // Check if address and chain are valid + const chainInfoMap = this.state.chainService.getChainInfoMap(); + + if (!chainInfoMap[chain]) { + console.warn(`Chain ${chain} is not supported`); + + return; + } + + // Get necessary data + const assetMap = this.state.chainService.getAssetRegistry(); + const evmApiMap = this.state.chainService.getEvmApiMap(); + const substrateApiMap = this.state.chainService.getSubstrateApiMap(); + const tonApiMap = this.state.chainService.getTonApiMap(); + const cardanoApiMap = this.state.chainService.getCardanoApiMap(); + const bitcoinApiMap = this.state.chainService.getBitcoinApiMap(); + + // Subscribe balance + let cancel = false; + const unsub = subscribeBalance( + [address], + [chain], + [asset], + assetMap, + chainInfoMap, + substrateApiMap, + evmApiMap, + tonApiMap, + cardanoApiMap, + bitcoinApiMap, + (result) => { + !cancel && this.setBalanceItem(result); + + cancel = true; + unsub && unsub(); + this._unsubscribeBalance = undefined; // Clear unsubscribe function + }, + extrinsicType || ExtrinsicType.TRANSFER_BALANCE + ); + + this._unsubscribeBalance = () => { + cancel = true; + unsub && unsub(); + }; + } + /** Unsubscribe balance subscription */ runUnsubscribeBalances () { this._unsubscribeBalance && this._unsubscribeBalance(); diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 3ed6fcf0ccd..4f48bb09633 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -1121,6 +1121,15 @@ export default class TransactionService { ].includes(transaction.extrinsicType)) { this.handlePostEarningTransaction(id); } + + // Trigger balance update for Bitcoin transactions after receiving extrinsicHash + if (ExtrinsicType.TRANSFER_BALANCE && transaction.chainType === 'bitcoin') { + const balanceService = this.state.balanceService; + const inputData = parseTransactionData(transaction.data); + + balanceService.runSubscribeBalanceForAddress(transaction.address, transaction.chain, inputData.tokenSlug, transaction.extrinsicType) + .catch((error) => console.error('Failed to run balance subscription:', error)); + } } private handlePostProcessing (id: string) { // must be done after success/failure to make sure the transaction is finalized From c1dea29d59b16713d8ad52115fe49522aa75eacd Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 6 Jun 2025 10:51:04 +0700 Subject: [PATCH 137/178] [Issue-4263] feat: Improve sorting functionality in history --- .../src/Popup/Home/History/index.tsx | 42 ++++++++++++------- .../extension-koni-ui/src/types/history.ts | 1 + 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx index 1cf96288380..fa8d0563c9a 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx @@ -70,7 +70,8 @@ function getIcon (item: TransactionHistoryItem): SwIconProps['phosphorIcon'] { function getDisplayData (item: TransactionHistoryItem, nameMap: Record, titleMap: Record): TransactionHistoryDisplayData { let displayData: TransactionHistoryDisplayData; - const time = customFormatDate(item.blockTime ? item.blockTime : item.time || 0, '#hhhh#:#mm#'); + const displayTime = item.blockTime || item.time; + const time = customFormatDate(displayTime, '#hhhh#:#mm#'); const displayStatus = item.status === ExtrinsicStatus.FAIL ? 'fail' : ''; @@ -177,6 +178,12 @@ function filterDuplicateItems (items: TransactionHistoryItem[]): TransactionHist return result; } +const PROCESSING_STATUSES: ExtrinsicStatus[] = [ + ExtrinsicStatus.QUEUED, + ExtrinsicStatus.SUBMITTING, + ExtrinsicStatus.PROCESSING +]; + const modalId = HISTORY_DETAIL_MODAL; const remindSeedPhraseModalId = REMIND_BACKUP_SEED_PHRASE_MODAL; const DEFAULT_ITEMS_COUNT = 20; @@ -374,8 +381,9 @@ function Component ({ className = '' }: Props): React.ReactElement { const fromName = accountMap[quickFormatAddressToCompare(item.from) || '']; const toName = accountMap[quickFormatAddressToCompare(item.to) || '']; const key = getHistoryItemKey(item); + const displayTime = item.blockTime || item.time; - finalHistoryMap[key] = { ...item, fromName, toName, displayData: getDisplayData(item, typeNameMap, typeTitleMap) }; + finalHistoryMap[key] = { ...item, fromName, toName, displayData: getDisplayData(item, typeNameMap, typeTitleMap), displayTime }; }); return finalHistoryMap; @@ -384,16 +392,16 @@ function Component ({ className = '' }: Props): React.ReactElement { const [currentItemDisplayCount, setCurrentItemDisplayCount] = useState(DEFAULT_ITEMS_COUNT); const getHistoryItems = useCallback((count: number) => { - return Object.values(historyMap).filter(filterFunction).sort((a, b) => { - const blockTimeA = a.blockTime ?? 0; - const blockTimeB = b.blockTime ?? 0; - - if (blockTimeA !== 0 && blockTimeB !== 0) { - return blockTimeB - blockTimeA; - } - - return b.time - a.time; - }) + return Object.values(historyMap).filter(filterFunction) + .sort((a, b) => { + if (PROCESSING_STATUSES.includes(a.status) && !PROCESSING_STATUSES.includes(b.status)) { + return -1; + } else if (PROCESSING_STATUSES.includes(b.status) && !PROCESSING_STATUSES.includes(a.status)) { + return 1; + } else { + return b.displayTime - a.displayTime; + } + }) .slice(0, count); }, [filterFunction, historyMap]); @@ -492,9 +500,13 @@ function Component ({ className = '' }: Props): React.ReactElement { [onOpenDetail] ); - const groupBy = useCallback((item: TransactionHistoryItem) => { - return formatHistoryDate(item.time ? item.time : item.blockTime || 0, language, 'list'); - }, [language]); + const groupBy = useCallback((item: TransactionHistoryDisplayItem) => { + if (PROCESSING_STATUSES.includes(item.status)) { + return t('Processing'); + } + + return formatHistoryDate(item.displayTime, language, 'list'); + }, [language, t]); const groupSeparator = useCallback((group: TransactionHistoryItem[], idx: number, groupLabel: string) => { return ( diff --git a/packages/extension-koni-ui/src/types/history.ts b/packages/extension-koni-ui/src/types/history.ts index 130a7b4142e..e1436ba6885 100644 --- a/packages/extension-koni-ui/src/types/history.ts +++ b/packages/extension-koni-ui/src/types/history.ts @@ -14,4 +14,5 @@ export interface TransactionHistoryDisplayData { export interface TransactionHistoryDisplayItem extends TransactionHistoryItem { displayData: TransactionHistoryDisplayData; + displayTime: number; } From f29da4271d4a28cc3aacc59d882ca88581b99a3b Mon Sep 17 00:00:00 2001 From: AnhMTV Date: Fri, 6 Jun 2025 11:21:57 +0700 Subject: [PATCH 138/178] [Issue-4428] Update lifecycle for extension => avoid to waste resources --- .../src/koni/background/handlers/State.ts | 45 ++++++++++++++----- .../src/services/balance-service/index.ts | 4 +- .../extension-base/src/services/base/types.ts | 2 + .../src/services/event-service/index.ts | 6 +++ .../src/services/event-service/types.ts | 2 + .../src/helper/ActionHandler.ts | 42 ++++++++++------- packages/webapp/src/webRunner.ts | 2 +- 7 files changed, 75 insertions(+), 28 deletions(-) diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index 15a5300bb2f..0109047da9e 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -144,6 +144,7 @@ export default class KoniState { private generalStatus: ServiceStatus = ServiceStatus.INITIALIZING; private waitSleeping: Promise | null = null; private waitStarting: Promise | null = null; + private waitStartingFull: Promise | null = null; constructor (providers: Providers = {}) { // Init subwallet api sdk @@ -186,7 +187,9 @@ export default class KoniState { // Init state if (targetIsWeb) { - this.init().catch(console.error); + this.init().then(() => { + this.wakeup(true).catch(console.error); + }).catch(console.error); } } @@ -306,7 +309,7 @@ export default class KoniState { await this.swapService.init(); await this.inappNotificationService.init(); - this.onReady(); + // this.onReady(); this.onAccountAdd(); this.onAccountRemove(); @@ -347,12 +350,6 @@ export default class KoniState { }); } - public onReady () { - // Todo: Need optimize in the future to, only run important services onetime to save resources - // Todo: If optimize must check activity of web-runner of mobile - this._start().catch(console.error); - } - public updateKeyringState (isReady = true, callback?: () => void): void { this.keyringService.updateKeyringState(isReady); callback && callback(); @@ -1751,6 +1748,7 @@ export default class KoniState { public async sleep () { // Wait starting finish before sleep to avoid conflict this.generalStatus === ServiceStatus.STARTING && this.waitStarting && await this.waitStarting; + this.generalStatus === ServiceStatus.STARTING_FULL && this.waitStartingFull && await this.waitStartingFull; this.eventService.emit('general.sleep', true); // Avoid sleep multiple times @@ -1811,7 +1809,7 @@ export default class KoniState { } // Start services - await Promise.all([this.cron.start(), this.subscription.start(), this.historyService.start(), this.priceService.start(), this.balanceService.start(), this.earningService.start(), this.swapService.start(), this.inappNotificationService.start()]); + this.eventService.emit('general.start', true); // Complete starting starting.resolve(); @@ -1819,8 +1817,35 @@ export default class KoniState { this.generalStatus = ServiceStatus.STARTED; } - public async wakeup () { + private async _startFull () { + // Continue wait existed starting process + if (this.generalStatus === ServiceStatus.STARTING) { + await this.waitStarting; + } + + // Always start full from start state + if (this.generalStatus !== ServiceStatus.STARTED) { + return; + } + + this.generalStatus = ServiceStatus.STARTING_FULL; + const startingFull = createPromiseHandler(); + + this.waitStartingFull = startingFull.promise; + + await Promise.all([this.cron.start(), this.subscription.start(), this.historyService.start(), this.priceService.start(), this.balanceService.start(), this.earningService.start(), this.swapService.start(), this.inappNotificationService.start()]); + this.eventService.emit('general.start_full', true); + + this.waitStartingFull = null; + this.generalStatus = ServiceStatus.STARTED_FULL; + } + + public async wakeup (fullWakeup = false) { await this._start(); + + if (fullWakeup) { + await this._startFull(); + } } public cancelSubscription (id: string): boolean { diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 5eb1acddc17..649ed6e1730 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -69,7 +69,7 @@ export class BalanceService implements StoppableServiceInterface { this.status = ServiceStatus.INITIALIZED; // Start service - await this.start(); + // await this.start(); // Commented out to avoid auto start when app not fully initialized // Handle events this.state.eventService.onLazy(this.handleEvents.bind(this)); @@ -166,7 +166,7 @@ export class BalanceService implements StoppableServiceInterface { if (needReload) { addLazy('reloadBalanceByEvents', () => { - if (!this.isReload) { + if (!this.isReload && this.status === ServiceStatus.STARTED) { this.runSubscribeBalances().catch(console.error); } }, lazyTime, undefined, true); diff --git a/packages/extension-base/src/services/base/types.ts b/packages/extension-base/src/services/base/types.ts index 66f41f3347e..5f1851c25c0 100644 --- a/packages/extension-base/src/services/base/types.ts +++ b/packages/extension-base/src/services/base/types.ts @@ -10,6 +10,8 @@ export enum ServiceStatus { INITIALIZED = 'initialized', STARTED = 'started', STARTING = 'starting', + STARTED_FULL = 'started_full', + STARTING_FULL = 'starting_full', STOPPED = 'stopped', STOPPING = 'stopping', } diff --git a/packages/extension-base/src/services/event-service/index.ts b/packages/extension-base/src/services/event-service/index.ts index 5a5a65f6017..56da0928c9b 100644 --- a/packages/extension-base/src/services/event-service/index.ts +++ b/packages/extension-base/src/services/event-service/index.ts @@ -21,6 +21,9 @@ export class EventService extends EventEmitter { public readonly waitCryptoReady: Promise; public readonly waitDatabaseReady: Promise; + public readonly waitAppStart: Promise; + public readonly waitAppStartFull: Promise; + public readonly waitKeyringReady: Promise; public readonly waitAccountReady: Promise; public readonly waitInjectReady: Promise; @@ -43,6 +46,9 @@ export class EventService extends EventEmitter { this.waitDatabaseReady = this.generateWaitPromise('database.ready'); this.waitKeyringReady = this.generateWaitPromise('keyring.ready'); this.waitAccountReady = this.generateWaitPromise('account.ready'); + this.waitAppStart = this.generateWaitPromise('general.start'); + this.waitAppStartFull = this.generateWaitPromise('general.start_full'); + // TODO: Need to merge logic on web-runner file this.waitInjectReady = TARGET_ENV === 'webapp' ? this.generateWaitPromise('inject.ready') : Promise.resolve(true); diff --git a/packages/extension-base/src/services/event-service/types.ts b/packages/extension-base/src/services/event-service/types.ts index 0f3f9c1751a..04b28b98e0c 100644 --- a/packages/extension-base/src/services/event-service/types.ts +++ b/packages/extension-base/src/services/event-service/types.ts @@ -5,6 +5,8 @@ import { SWTransactionBase } from '@subwallet/extension-base/services/transactio import { CurrentAccountInfo } from '@subwallet/extension-base/types'; export interface EventRegistry { + 'general.start': [boolean]; + 'general.start_full': [boolean]; 'general.sleep': [boolean]; 'general.wakeup': [boolean]; 'crypto.ready': [boolean]; diff --git a/packages/extension-koni/src/helper/ActionHandler.ts b/packages/extension-koni/src/helper/ActionHandler.ts index dd0f54bfc5c..9c9ddc768fd 100644 --- a/packages/extension-koni/src/helper/ActionHandler.ts +++ b/packages/extension-koni/src/helper/ActionHandler.ts @@ -21,10 +21,10 @@ export class ActionHandler { private connectionMap: Record = {}; private firstTrigger = false; private waitFirstTrigger = createPromiseHandler(); - private waitActiveHandler = createPromiseHandler(); public waitFirstActiveMessage = this.waitFirstTrigger.promise; private isActive = false; + private isFullActive = false; private sleepTimeout?: NodeJS.Timeout; get isContentConnecting (): boolean { @@ -68,15 +68,15 @@ export class ActionHandler { } private async _onPortMessage (port: chrome.runtime.Port, data: TransportRequestMessage, portId: string) { - // console.debug(data.message, data.id, portId); // message and disconnect handlers if (!this.mainHandler) { this.mainHandler = await this.waitMainHandler.promise; } - const requireActive = data.message !== 'pub(phishing.redirectIfDenied)'; + const validMessage = data?.message; + const requireActive = validMessage && validMessage !== 'pub(phishing.redirectIfDenied)'; - if (!this.connectionMap[portId] && data?.message && requireActive) { + if (!this.connectionMap[portId] && requireActive) { this.connectionMap[portId] = port.name; if (!this.firstTrigger) { @@ -84,17 +84,30 @@ export class ActionHandler { this.waitFirstTrigger.resolve(); } - if (this.sleepTimeout) { - console.debug('Clearing sleep timeout'); - clearTimeout(this.sleepTimeout); - this.sleepTimeout = undefined; - } + if (requireActive) { + const requireFullActive = validMessage.startsWith('pri(') || validMessage.startsWith('mobile('); + const needActive = !this.isActive; + const needFullActive = !this.isFullActive && requireFullActive; + + if (this.sleepTimeout) { + console.debug('Clearing sleep timeout'); + clearTimeout(this.sleepTimeout); + this.sleepTimeout = undefined; + } + + if (needActive) { + this.isActive = true; + startHeartbeat(); - if (!this.isActive) { - this.isActive = true; - startHeartbeat(); - this.mainHandler && await this.mainHandler.state.wakeup(); - this.waitActiveHandler.resolve(true); + if (!needFullActive) { + this.mainHandler && await this.mainHandler.state.wakeup(false); + } + } + + if (needFullActive) { + this.isFullActive = true; + this.mainHandler && await this.mainHandler.state.wakeup(true); + } } } @@ -113,7 +126,6 @@ export class ActionHandler { this.sleepTimeout && clearTimeout(this.sleepTimeout); this.sleepTimeout = setTimeout(() => { // Reset active status - this.waitActiveHandler = createPromiseHandler(); this.mainHandler && this.mainHandler.state.sleep().catch(console.error); }, SLEEP_TIMEOUT); } diff --git a/packages/webapp/src/webRunner.ts b/packages/webapp/src/webRunner.ts index ef1b146bdfe..5621f934896 100644 --- a/packages/webapp/src/webRunner.ts +++ b/packages/webapp/src/webRunner.ts @@ -65,7 +65,7 @@ cryptoWaitReady() responseMessage({ id: '0', response: { status: 'crypto_ready' } } as PageStatus); // wake webapp up - SWHandler.instance.state.wakeup().catch((err) => console.warn(err)); + SWHandler.instance.state.wakeup(true).catch((err) => console.warn(err)); console.log('[WebApp] initialization completed'); }) From a119d6b0b98a635a6759d0d7b8723e99a64a2bba Mon Sep 17 00:00:00 2001 From: AnhMTV Date: Fri, 6 Jun 2025 10:47:10 +0700 Subject: [PATCH 139/178] [Issue-4263] Improve get block time for fee rate --- .../strategy/BlockStreamTestnet/index.ts | 45 +++++++++++++------ .../strategy/SubWalletMainnet/index.ts | 45 +++++++++++++------ .../handler/bitcoin/strategy/types.ts | 1 + 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts index e5ce64da86e..7bc2f1e32be 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts @@ -24,14 +24,6 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im this.baseUrl = url; this.isTestnet = url.includes('testnet'); - - this.getBlockTime() - .then((rs) => { - this.timePerBlock = rs; - }) - .catch(() => { - this.timePerBlock = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; - }); } private headers = { @@ -63,6 +55,31 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im }, 0); } + async computeBlockTime (): Promise { + let blockTime = this.timePerBlock; + + if (blockTime > 0) { + return blockTime; + } + + try { + blockTime = await this.getBlockTime(); + + this.timePerBlock = blockTime; + } catch (e) { + console.error('Failed to compute block time', e); + + blockTime = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; // Default to 10 minutes if failed + } + + // Cache block time in 60 seconds + setTimeout(() => { + this.timePerBlock = 0; + }, 60000); + + return blockTime; + } + getAddressSummaryInfo (address: string): Promise { return this.addRequest(async () => { const response = await getRequest(this.getUrl(`address/${address}`), undefined, this.headers); @@ -149,8 +166,10 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im }, 1); } - getFeeRate (): Promise { - return this.addRequest(async (): Promise => { + async getFeeRate (): Promise { + const blockTime = await this.computeBlockTime(); + + return await this.addRequest(async (): Promise => { const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); const estimates = await response.json() as BlockStreamFeeEstimates; @@ -170,9 +189,9 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im type: 'bitcoin', busyNetwork: false, options: { - slow: { feeRate: convertFee(estimates[low] || 10), time: this.timePerBlock * low }, - average: { feeRate: convertFee(estimates[average || 12]), time: this.timePerBlock * average }, - fast: { feeRate: convertFee(estimates[fast] || 15), time: this.timePerBlock * fast }, + slow: { feeRate: convertFee(estimates[low] || 10), time: blockTime * low }, + average: { feeRate: convertFee(estimates[average || 12]), time: blockTime * average }, + fast: { feeRate: convertFee(estimates[fast] || 15), time: blockTime * fast }, default: 'slow' } }; diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/SubWalletMainnet/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/SubWalletMainnet/index.ts index 4ba2d08c96d..2e3805107ee 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/SubWalletMainnet/index.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/SubWalletMainnet/index.ts @@ -26,14 +26,6 @@ export class SubWalletMainnetRequestStrategy extends BaseApiRequestStrategy impl this.baseUrl = url; this.isTestnet = url.includes('testnet'); - - this.getBlockTime() - .then((rs) => { - this.timePerBlock = rs; - }) - .catch(() => { - this.timePerBlock = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; - }); } private headers = { @@ -67,6 +59,31 @@ export class SubWalletMainnetRequestStrategy extends BaseApiRequestStrategy impl }, 0); } + async computeBlockTime (): Promise { + let blockTime = this.timePerBlock; + + if (blockTime > 0) { + return blockTime; + } + + try { + blockTime = await this.getBlockTime(); + + this.timePerBlock = blockTime; + } catch (e) { + console.error('Failed to compute block time', e); + + blockTime = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; // Default to 10 minutes if failed + } + + // Cache block time in 60 seconds + setTimeout(() => { + this.timePerBlock = 0; + }, 60000); + + return blockTime; + } + getAddressSummaryInfo (address: string): Promise { return this.addRequest(async () => { const _rs = await getRequest(this.getUrl(`address/${address}`), undefined, this.headers); @@ -119,8 +136,10 @@ export class SubWalletMainnetRequestStrategy extends BaseApiRequestStrategy impl }, 1); } - getFeeRate (): Promise { - return this.addRequest(async (): Promise => { + async getFeeRate (): Promise { + const timePerBlock = await this.computeBlockTime(); + + return await this.addRequest(async (): Promise => { const _rs = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); const rs = await _rs.json() as OBResponse; @@ -140,9 +159,9 @@ export class SubWalletMainnetRequestStrategy extends BaseApiRequestStrategy impl type: 'bitcoin', busyNetwork: false, options: { - slow: { feeRate: convertFee(result[low]), time: this.timePerBlock * low }, - average: { feeRate: convertFee(result[average]), time: this.timePerBlock * average }, - fast: { feeRate: convertFee(result[fast]), time: this.timePerBlock * fast }, + slow: { feeRate: convertFee(result[low]), time: timePerBlock * low }, + average: { feeRate: convertFee(result[average]), time: timePerBlock * average }, + fast: { feeRate: convertFee(result[fast]), time: timePerBlock * fast }, default: 'slow' } }; 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 d229cb098e6..367fc0bfeff 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 @@ -7,6 +7,7 @@ import EventEmitter from 'eventemitter3'; export interface BitcoinApiStrategy extends Omit { getBlockTime (): Promise; + computeBlockTime (): Promise; getAddressSummaryInfo (address: string): Promise; getRunes (address: string): Promise; // getRuneTxsUtxos (address: string): Promise; // noted: all rune utxos come in account From b6b6e2a99be39cb9eb873ae085653a8e7609fb00 Mon Sep 17 00:00:00 2001 From: AnhMTV Date: Fri, 6 Jun 2025 11:59:49 +0700 Subject: [PATCH 140/178] [Issue-4428] Fix wrong started flow --- .../extension-base/src/koni/background/handlers/State.ts | 9 +++++++++ .../extension-base/src/services/balance-service/index.ts | 5 ++++- .../extension-base/src/services/event-service/index.ts | 2 ++ .../extension-base/src/services/event-service/types.ts | 1 + 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index 0109047da9e..44c8a6ad4a9 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -322,6 +322,9 @@ export default class KoniState { this.chainService.subscribeChainInfoMap().subscribe(() => { this.afterChainServiceInit(); }); + + // Mark app is ready + this.eventService.emit('general.init', true); } public async initMantaPay (password: string) { @@ -1746,6 +1749,9 @@ export default class KoniState { } public async sleep () { + // Wait for app initialized before sleep + await this.eventService.waitAppInitialized; + // Wait starting finish before sleep to avoid conflict this.generalStatus === ServiceStatus.STARTING && this.waitStarting && await this.waitStarting; this.generalStatus === ServiceStatus.STARTING_FULL && this.waitStartingFull && await this.waitStartingFull; @@ -1781,6 +1787,9 @@ export default class KoniState { } private async _start () { + // Wait for app initialized before start + await this.eventService.waitAppInitialized; + // Wait sleep finish before start to avoid conflict this.generalStatus === ServiceStatus.STOPPING && this.waitSleeping && await this.waitSleeping; diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 649ed6e1730..a5d8ba2ee9e 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -42,6 +42,9 @@ export class BalanceService implements StoppableServiceInterface { status: ServiceStatus = ServiceStatus.NOT_INITIALIZED; private isReload = false; + get isStarted (): boolean { + return this.status === ServiceStatus.STARTED; + } private readonly detectAccountBalanceStore = new DetectAccountBalanceStore(); private readonly balanceDetectSubject: BehaviorSubject = new BehaviorSubject({}); @@ -166,7 +169,7 @@ export class BalanceService implements StoppableServiceInterface { if (needReload) { addLazy('reloadBalanceByEvents', () => { - if (!this.isReload && this.status === ServiceStatus.STARTED) { + if (!this.isReload && this.isStarted) { this.runSubscribeBalances().catch(console.error); } }, lazyTime, undefined, true); diff --git a/packages/extension-base/src/services/event-service/index.ts b/packages/extension-base/src/services/event-service/index.ts index 56da0928c9b..b6d32c611fb 100644 --- a/packages/extension-base/src/services/event-service/index.ts +++ b/packages/extension-base/src/services/event-service/index.ts @@ -21,6 +21,7 @@ export class EventService extends EventEmitter { public readonly waitCryptoReady: Promise; public readonly waitDatabaseReady: Promise; + public readonly waitAppInitialized: Promise; public readonly waitAppStart: Promise; public readonly waitAppStartFull: Promise; @@ -46,6 +47,7 @@ export class EventService extends EventEmitter { this.waitDatabaseReady = this.generateWaitPromise('database.ready'); this.waitKeyringReady = this.generateWaitPromise('keyring.ready'); this.waitAccountReady = this.generateWaitPromise('account.ready'); + this.waitAppInitialized = this.generateWaitPromise('general.init'); this.waitAppStart = this.generateWaitPromise('general.start'); this.waitAppStartFull = this.generateWaitPromise('general.start_full'); diff --git a/packages/extension-base/src/services/event-service/types.ts b/packages/extension-base/src/services/event-service/types.ts index 04b28b98e0c..c5e94b75d2c 100644 --- a/packages/extension-base/src/services/event-service/types.ts +++ b/packages/extension-base/src/services/event-service/types.ts @@ -5,6 +5,7 @@ import { SWTransactionBase } from '@subwallet/extension-base/services/transactio import { CurrentAccountInfo } from '@subwallet/extension-base/types'; export interface EventRegistry { + 'general.init': [boolean]; 'general.start': [boolean]; 'general.start_full': [boolean]; 'general.sleep': [boolean]; From c071db5b4eae50af32e19bbba4a2f82cd1a9f353 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 6 Jun 2025 14:42:06 +0700 Subject: [PATCH 141/178] [Issue-4263] fix bug: subscribe balance after transfer --- .../src/services/balance-service/index.ts | 49 ++++++++----------- .../src/services/transaction-service/index.ts | 19 ++++++- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 5eb1acddc17..352140b1874 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -443,8 +443,6 @@ export class BalanceService implements StoppableServiceInterface { } async runSubscribeBalanceForAddress (address: string, chain: string, asset: string, extrinsicType?: ExtrinsicType) { - await Promise.all([this.state.eventService.waitKeyringReady, this.state.eventService.waitChainReady]); - // Check if address and chain are valid const chainInfoMap = this.state.chainService.getChainInfoMap(); @@ -462,33 +460,26 @@ export class BalanceService implements StoppableServiceInterface { const cardanoApiMap = this.state.chainService.getCardanoApiMap(); const bitcoinApiMap = this.state.chainService.getBitcoinApiMap(); - // Subscribe balance - let cancel = false; - const unsub = subscribeBalance( - [address], - [chain], - [asset], - assetMap, - chainInfoMap, - substrateApiMap, - evmApiMap, - tonApiMap, - cardanoApiMap, - bitcoinApiMap, - (result) => { - !cancel && this.setBalanceItem(result); - - cancel = true; - unsub && unsub(); - this._unsubscribeBalance = undefined; // Clear unsubscribe function - }, - extrinsicType || ExtrinsicType.TRANSFER_BALANCE - ); - - this._unsubscribeBalance = () => { - cancel = true; - unsub && unsub(); - }; + return new Promise((resolve) => { + const unsub = subscribeBalance( + [address], + [chain], + [asset], + assetMap, + chainInfoMap, + substrateApiMap, + evmApiMap, + tonApiMap, + cardanoApiMap, + bitcoinApiMap, + (result) => { + this.setBalanceItem(result); + unsub(); + resolve(); + }, + extrinsicType || ExtrinsicType.TRANSFER_BALANCE + ); + }); } /** Unsubscribe balance subscription */ diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 4f48bb09633..f2f1be19d92 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -1127,8 +1127,23 @@ export default class TransactionService { const balanceService = this.state.balanceService; const inputData = parseTransactionData(transaction.data); - balanceService.runSubscribeBalanceForAddress(transaction.address, transaction.chain, inputData.tokenSlug, transaction.extrinsicType) - .catch((error) => console.error('Failed to run balance subscription:', error)); + try { + const sender = keyring.getPair(inputData.from); + + balanceService.runSubscribeBalanceForAddress(sender.address, transaction.chain, inputData.tokenSlug, transaction.extrinsicType) + .catch((error) => console.error('Failed to run balance subscription:', error)); + } catch (e) { + console.error(e); + } + + try { + const recipient = keyring.getPair(inputData.to); + + balanceService.runSubscribeBalanceForAddress(recipient.address, transaction.chain, inputData.tokenSlug, transaction.extrinsicType) + .catch((error) => console.error('Failed to run balance subscription:', error)); + } catch (e) { + console.error(e); + } } } From 51d34ddf7b699e4d1a0aa8c131118c03e7af947d Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 6 Jun 2025 17:01:33 +0700 Subject: [PATCH 142/178] [Issue-4263] refactor: replay api to online --- .../services/chain-service/handler/bitcoin/BitcoinApi.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts index 7681b6e3009..5f78c7c6bdb 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts @@ -12,7 +12,7 @@ import { _BitcoinApi, _ChainConnectionStatus } from '../../types'; // const isBlockStreamProvider = (apiUrl: string): boolean => apiUrl === 'https://blockstream-testnet.openbit.app' || apiUrl === 'https://electrs.openbit.app'; // const BLOCKSTREAM_TESTNET_API_URL = 'https://blockstream.info/testnet/api/'; -const MEMPOOL_TESTNET_V4_API_URL = 'https://mempool.space/testnet4/api/'; +// const MEMPOOL_TESTNET_V4_API_URL = 'https://mempool.space/testnet4/api/'; export class BitcoinApi implements _BitcoinApi { chainSlug: string; @@ -37,7 +37,7 @@ export class BitcoinApi implements _BitcoinApi { const isTestnet = apiUrl.includes('testnet'); if (isTestnet) { - this.api = new BlockStreamTestnetRequestStrategy(MEMPOOL_TESTNET_V4_API_URL); + this.api = new BlockStreamTestnetRequestStrategy(apiUrl); } else { this.api = new SubWalletMainnetRequestStrategy(apiUrl); } @@ -80,7 +80,7 @@ export class BitcoinApi implements _BitcoinApi { const isTestnet = apiUrl.includes('testnet'); if (isTestnet) { - this.api = new BlockStreamTestnetRequestStrategy(MEMPOOL_TESTNET_V4_API_URL); + this.api = new BlockStreamTestnetRequestStrategy(apiUrl); } else { this.api = new SubWalletMainnetRequestStrategy(apiUrl); } From e39f756c8932d56c94e9451605f247da7a2d495c Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 6 Jun 2025 17:37:50 +0700 Subject: [PATCH 143/178] [Issue-4263] refactor: recheck the response api of the mempool testnet v4 --- .../handler/bitcoin/strategy/BlockStreamTestnet/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts index e5ce64da86e..e2994513e5f 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts @@ -149,6 +149,7 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im }, 1); } + // TODO: NOTE: Currently not in use. Recheck the response if you want to use it. getFeeRate (): Promise { return this.addRequest(async (): Promise => { const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); @@ -179,7 +180,6 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im }, 0); } - // TODO: Handle fallback for this route as it is not stable. getRecommendedFeeRate (): Promise { return this.addRequest(async (): Promise => { const convertTimeMilisec = { From 3cdd16937c006738074bfc338b6f344794301565 Mon Sep 17 00:00:00 2001 From: AnhMTV Date: Fri, 6 Jun 2025 17:46:24 +0700 Subject: [PATCH 144/178] [Issue-4428] Fix resolve starting full --- packages/extension-base/src/koni/background/handlers/State.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index 44c8a6ad4a9..e9e885aeaf3 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -1847,6 +1847,7 @@ export default class KoniState { this.waitStartingFull = null; this.generalStatus = ServiceStatus.STARTED_FULL; + startingFull.resolve(); } public async wakeup (fullWakeup = false) { From e711296c22c97bd70714cb344742001b8f0bc800 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 6 Jun 2025 19:07:05 +0700 Subject: [PATCH 145/178] [Issue-4263] fix dustlimit for transfer all --- .../src/koni/background/handlers/Extension.ts | 3 ++- .../extension-base/src/types/balance/transfer.ts | 1 + .../src/utils/bitcoin/utxo-management.ts | 13 ++++++++++++- packages/extension-base/src/utils/fee/transfer.ts | 5 +++-- .../src/Popup/Transaction/variants/SendFund.tsx | 3 ++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 84378d04b9b..14cd2092ff3 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -2100,7 +2100,7 @@ export default class KoniExtension { } private async subscribeMaxTransferable (request: RequestSubscribeTransfer, id: string, port: chrome.runtime.Port): Promise { - const { address, chain, destChain: _destChain, feeCustom, feeOption, token, tokenPayFeeSlug, value } = request; + const { address, chain, destChain: _destChain, feeCustom, feeOption, to, token, tokenPayFeeSlug, value } = request; const cb = createSubscription<'pri(transfer.subscribe)'>(id, port); const transferTokenInfo = this.#koniState.chainService.getAssetBySlug(token); @@ -2123,6 +2123,7 @@ export default class KoniExtension { const _request: CalculateMaxTransferable = { address: address, + to: to, value, // todo: lazy subscribe to improve performance cardanoApi: this.#koniState.chainService.getCardanoApi(chain), destChain, diff --git a/packages/extension-base/src/types/balance/transfer.ts b/packages/extension-base/src/types/balance/transfer.ts index 84962b1c412..a96f5820a8d 100644 --- a/packages/extension-base/src/types/balance/transfer.ts +++ b/packages/extension-base/src/types/balance/transfer.ts @@ -9,6 +9,7 @@ import { FeeChainType, FeeDetail, TransactionFee } from '../fee'; export interface RequestSubscribeTransfer extends TransactionFee { address: string; + to?: string; chain: string; value: string; token: string; diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index 0fde0d228d1..27878ed521c 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -69,7 +69,9 @@ export function determineUtxosForSpendAll ({ feeRate, if (!validateBitcoinAddress(recipient)) { throw new Error('Cannot calculate spend of invalid address type'); } - // TODO: Prevent dust limit when transferring all + + const recipientAddressInfo = getBitcoinAddressInfo(recipient); + const recipientDustLimit = BTC_DUST_AMOUNT[recipientAddressInfo.type] || 546; const recipients = [recipient]; @@ -87,6 +89,15 @@ export function determineUtxosForSpendAll ({ feeRate, throw new InsufficientFundsError(); } + if (amount < recipientDustLimit) { + const atLeastStr = formatNumber(recipientDustLimit, 8, balanceFormatter, { maxNumberFormat: 8, minNumberFormat: 8 }); + + throw new TransactionError( + TransferTxErrorType.NOT_ENOUGH_VALUE, + `You must transfer at least ${atLeastStr} BTC` + ); + } + // Fee has already been deducted from the amount with send all const outputs = [{ value: amount, address: recipient }]; diff --git a/packages/extension-base/src/utils/fee/transfer.ts b/packages/extension-base/src/utils/fee/transfer.ts index 7bcb240b1f5..6b03ad0810a 100644 --- a/packages/extension-base/src/utils/fee/transfer.ts +++ b/packages/extension-base/src/utils/fee/transfer.ts @@ -40,6 +40,7 @@ import { combineEthFee, combineSubstrateFee } from './combine'; export interface CalculateMaxTransferable extends TransactionFee { address: string; + to?: string; value: string; srcToken: _ChainAsset; destToken?: _ChainAsset; @@ -105,7 +106,7 @@ export const calculateMaxTransferable = async (id: string, request: CalculateMax }; export const calculateTransferMaxTransferable = async (id: string, request: CalculateMaxTransferable, freeBalance: AmountData, fee: FeeInfo): Promise => { - const { address, bitcoinApi, cardanoApi, destChain, evmApi, feeCustom, feeOption, isTransferLocalTokenAndPayThatTokenAsFee, isTransferNativeTokenAndPayLocalTokenAsFee, nativeToken, srcChain, srcToken, substrateApi, tonApi, value } = request; + const { address, bitcoinApi, cardanoApi, destChain, evmApi, feeCustom, feeOption, isTransferLocalTokenAndPayThatTokenAsFee, isTransferNativeTokenAndPayLocalTokenAsFee, nativeToken, srcChain, srcToken, substrateApi, to, tonApi, value } = request; const feeChainType = fee.type; let estimatedFee: string; let feeOptions: FeeDetail; @@ -180,7 +181,7 @@ export const calculateTransferMaxTransferable = async (id: string, request: Calc [transaction] = await createBitcoinTransaction({ chain: srcChain.slug, from: address, - to: address, + to: to || address, value, feeInfo: fee, transferAll: false, 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 22f5505c4d7..804e800e84f 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -827,6 +827,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone if (fromValue && assetValue) { subscribeMaxTransfer({ address: fromValue, + to: toValue, chain: assetRegistry[assetValue].originChain, token: assetValue, value: transferAmountValue, @@ -851,7 +852,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone cancel = true; id && cancelSubscription(id).catch(console.error); }; - }, [assetValue, assetRegistry, chainValue, chainStatus, form, fromValue, destChainValue, selectedTransactionFee, nativeTokenSlug, currentTokenPayFee, transferAmountValue]); + }, [assetValue, assetRegistry, chainValue, chainStatus, form, fromValue, destChainValue, selectedTransactionFee, nativeTokenSlug, currentTokenPayFee, transferAmountValue, toValue]); useEffect(() => { const bnTransferAmount = new BN(transferAmountValue || '0'); From 8b2b1d6b0bd3aa292605f85dd9011df72bbc5a23 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 9 Jun 2025 10:07:44 +0700 Subject: [PATCH 146/178] [Issue-4263] allow transfer when changeOutput < dustLimt --- .../extension-base/src/utils/bitcoin/utxo-management.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index 27878ed521c..0545c4a6007 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -207,9 +207,11 @@ export function determineUtxosForSpend ({ amount, // fee: newFee // }; - const atLeastStr = formatNumber(dustLimit, 8, balanceFormatter, { maxNumberFormat: 8, minNumberFormat: 8 }); + // const atLeastStr = formatNumber(dustLimit, 8, balanceFormatter, { maxNumberFormat: 8, minNumberFormat: 8 }); + // throw new TransactionError(TransferTxErrorType.NOT_ENOUGH_VALUE, `You must transfer at least ${atLeastStr} BTC`); - throw new TransactionError(TransferTxErrorType.NOT_ENOUGH_VALUE, `You must transfer at least ${atLeastStr} BTC`); + // Do nothing with the remaining balance (amountLeft < dustLimit) + console.warn(`Change output of ${amountLeft.toString()} satoshis is below dust limit (${dustLimit} satoshis for ${senderAddressInfo.type}). Omitting change output.`); } return { From 043065ab45db0ef31837f21047375e69607a417d Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 9 Jun 2025 15:45:42 +0700 Subject: [PATCH 147/178] [Issue-4263] add: handle for blockstream api --- .../handler/bitcoin/BitcoinApi.ts | 32 +- .../blockstream-testnet-strategy.ts | 398 +++++++++++++++++ .../strategy/BlockStreamTestnet/index.ts | 399 +----------------- .../mempool-testnet-strategy.ts | 398 +++++++++++++++++ 4 files changed, 815 insertions(+), 412 deletions(-) create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts create mode 100644 packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts index 5f78c7c6bdb..be34e6a6056 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinApi.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { BlockStreamTestnetRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet'; +import { BlockStreamTestnetRequestStrategy, MempoolTestnetRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet'; import { SubWalletMainnetRequestStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/SubWalletMainnet'; import { BitcoinApiStrategy } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; import { createPromiseHandler, PromiseHandler } from '@subwallet/extension-base/utils/promise'; @@ -33,14 +33,7 @@ export class BitcoinApi implements _BitcoinApi { this.apiUrl = apiUrl; this.providerName = providerName || 'unknown'; this.isReadyHandler = createPromiseHandler<_BitcoinApi>(); - - const isTestnet = apiUrl.includes('testnet'); - - if (isTestnet) { - this.api = new BlockStreamTestnetRequestStrategy(apiUrl); - } else { - this.api = new SubWalletMainnetRequestStrategy(apiUrl); - } + this.api = this.createApiStrategy(apiUrl); this.connect(); } @@ -49,6 +42,19 @@ export class BitcoinApi implements _BitcoinApi { return this.isApiConnectedSubject.getValue(); } + private createApiStrategy (apiUrl: string): BitcoinApiStrategy { + const isTestnet = apiUrl.includes('testnet'); + const isBlockstreamUrl = apiUrl.includes('blockstream'); + + if (isTestnet) { + return isBlockstreamUrl + ? new BlockStreamTestnetRequestStrategy(apiUrl) + : new MempoolTestnetRequestStrategy(apiUrl); + } + + return new SubWalletMainnetRequestStrategy(apiUrl); + } + get connectionStatus (): _ChainConnectionStatus { return this.connectionStatusSubject.getValue(); } @@ -77,13 +83,7 @@ export class BitcoinApi implements _BitcoinApi { await this.disconnect(); this.apiUrl = apiUrl; - const isTestnet = apiUrl.includes('testnet'); - - if (isTestnet) { - this.api = new BlockStreamTestnetRequestStrategy(apiUrl); - } else { - this.api = new SubWalletMainnetRequestStrategy(apiUrl); - } + this.api = this.createApiStrategy(apiUrl); this.connect(); } diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts new file mode 100644 index 00000000000..9eecc8c3669 --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts @@ -0,0 +1,398 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { SWError } from '@subwallet/extension-base/background/errors/SWError'; +import { BitcoinAddressSummaryInfo, BitcoinApiStrategy, BitcoinTransactionEventMap, BlockstreamAddressResponse, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, BlockStreamUtxo, Inscription, InscriptionFetchedData, RunesInfoByAddress, RunesInfoByAddressFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; +import { HiroService } from '@subwallet/extension-base/services/hiro-service'; +import { RunesService } from '@subwallet/extension-base/services/rune-service'; +import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; +import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; +import { getRequest, postRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; +import { BitcoinFeeInfo, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; +import BigN from 'bignumber.js'; +import EventEmitter from 'eventemitter3'; + +export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy implements BitcoinApiStrategy { + private readonly baseUrl: string; + private readonly isTestnet: boolean; + private timePerBlock = 0; // in milliseconds + + constructor (url: string) { + const context = new BaseApiRequestContext(); + + super(context); + + this.baseUrl = url; + this.isTestnet = url.includes('testnet'); + + this.getBlockTime() + .then((rs) => { + this.timePerBlock = rs; + }) + .catch(() => { + this.timePerBlock = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; + }); + } + + private headers = { + 'Content-Type': 'application/json' + }; + + isRateLimited (): boolean { + return false; + } + + getUrl (path: string): string { + return `${this.baseUrl}/${path}`; + } + + getBlockTime (): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl('blocks'), undefined, this.headers); + const blocks = await response.json() as BlockStreamBlock[]; + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getBlockTime', 'Failed to fetch blocks'); + } + + const length = blocks.length; + const sortedBlocks = blocks.sort((a, b) => b.timestamp - a.timestamp); + const time = (sortedBlocks[0].timestamp - sortedBlocks[length - 1].timestamp) * 1000; + + return time / length; + }, 0); + } + + getAddressSummaryInfo (address: string): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`address/${address}`), undefined, this.headers); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getAddressSummaryInfo', 'Failed to fetch address info'); + } + + const rsRaw = await response.json() as BlockstreamAddressResponse; + const chainBalance = rsRaw.chain_stats.funded_txo_sum - rsRaw.chain_stats.spent_txo_sum; + const pendingLocked = rsRaw.mempool_stats.spent_txo_sum; // Only consider spent UTXOs in mempool + const mempoolReceived = rsRaw.mempool_stats.funded_txo_sum; // Funds received in mempool (e.g., change) + const availableBalance = Math.max(0, chainBalance - pendingLocked + mempoolReceived); // Ensure balance is non-negative + + const rs: BitcoinAddressSummaryInfo = { + address: rsRaw.address, + chain_stats: { + funded_txo_count: rsRaw.chain_stats.funded_txo_count, + funded_txo_sum: rsRaw.chain_stats.funded_txo_sum, + spent_txo_count: rsRaw.chain_stats.spent_txo_count, + spent_txo_sum: rsRaw.chain_stats.spent_txo_sum, + tx_count: rsRaw.chain_stats.tx_count + }, + mempool_stats: { + funded_txo_count: rsRaw.mempool_stats.funded_txo_count, + funded_txo_sum: rsRaw.mempool_stats.funded_txo_sum, + spent_txo_count: rsRaw.mempool_stats.spent_txo_count, + spent_txo_sum: rsRaw.mempool_stats.spent_txo_sum, + tx_count: rsRaw.mempool_stats.tx_count + }, + balance: availableBalance, + total_inscription: 0, + balance_rune: '0', + balance_inscription: '0' + }; + + return rs; + }, 0); + } + + getAddressTransaction (address: string, limit = 100): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`address/${address}/txs`), { limit: `${limit}` }, this.headers); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getAddressTransaction', 'Failed to fetch transactions'); + } + + return await response.json() as BitcoinTx[]; + }, 1); + } + + getTransactionStatus (txHash: string): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`tx/${txHash}/status`), undefined, {}); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.getTransactionStatus', `Failed to fetch transaction status: ${errorText}`); + } + + // Blockstream API trả về object thô + const data = await response.json() as BlockStreamTransactionStatus; + + return { + confirmed: data.confirmed || false, + block_time: data.block_time || 0, + block_height: data.block_height, + block_hash: data.block_hash + }; + }, 1); + } + + getTransactionDetail (txHash: string): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`tx/${txHash}`), undefined, this.headers); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getTransactionDetail', 'Failed to fetch transaction detail'); + } + + return await response.json() as BlockStreamTransactionDetail; + }, 1); + } + + // TODO: NOTE: Currently not in use. Recheck the response if you want to use it. + getFeeRate (): Promise { + return this.addRequest(async (): Promise => { + const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); + const estimates = await response.json() as BlockStreamFeeEstimates; + + console.log('getRecommendedFeeRate: rs', estimates); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getFeeRate', 'Failed to fetch fee estimates'); + } + + const low = 6; + const average = 3; + const fast = 1; + + const convertFee = (fee: number) => parseFloat(new BigN(fee).toFixed(2)); + + return { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: convertFee(estimates[low] || 10), time: this.timePerBlock * low }, + average: { feeRate: convertFee(estimates[average || 12]), time: this.timePerBlock * average }, + fast: { feeRate: convertFee(estimates[fast] || 15), time: this.timePerBlock * fast }, + default: 'slow' + } + }; + }, 0); + } + + getRecommendedFeeRate (): Promise { + return this.addRequest(async (): Promise => { + const convertTimeMilisec = { + fastestFee: 10 * 60000, + halfHourFee: 30 * 60000, + hourFee: 60 * 60000 + }; + + const defaultFeeInfo: BitcoinFeeInfo = { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: 1, time: convertTimeMilisec.hourFee }, + average: { feeRate: 1, time: convertTimeMilisec.halfHourFee }, + fast: { feeRate: 1, time: convertTimeMilisec.fastestFee }, + default: 'slow' + } + }; + + try { + const response = await getRequest(this.getUrl('/fee-estimates'), undefined, this.headers); + + if (!response.ok) { + console.warn(`Failed to fetch fee estimates: ${response.statusText}`); + + return defaultFeeInfo; + } + + const estimates = await response.json() as BlockStreamFeeEstimates; + + const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed(), 10); + + return { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: convertFee(estimates['6'] || 1), time: this.timePerBlock * 6 }, // 6 block + average: { feeRate: convertFee(estimates['3'] || 1), time: this.timePerBlock * 3 }, // 3 block + fast: { feeRate: convertFee(estimates['1'] || 1), time: this.timePerBlock }, // 1 block + default: 'slow' + } + }; + } catch { + return defaultFeeInfo; + } + }, 0); + } + + getUtxos (address: string): Promise { + return this.addRequest(async (): Promise => { + const response = await getRequest(this.getUrl(`address/${address}/utxo`), undefined, {}); + const rs = await response.json() as BlockStreamUtxo[]; + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.getUtxos', `Failed to fetch UTXOs: ${errorText}`); + } + + return rs.map((item: BlockStreamUtxo) => ({ + txid: item.txid, + vout: item.vout, + value: item.value, + status: item.status + })); + }, 0); + } + + sendRawTransaction (rawTransaction: string) { + const eventEmitter = new EventEmitter(); + + this.addRequest(async (): Promise => { + const response = await postRequest( + this.getUrl('tx'), + rawTransaction, + { 'Content-Type': 'text/plain' }, + false + ); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.sendRawTransaction', `Failed to broadcast transaction: ${errorText}`); + } + + return await response.text(); + }, 0) + .then((extrinsicHash) => { + eventEmitter.emit('extrinsicHash', extrinsicHash); + + // Check transaction status + const interval = setInterval(() => { + this.getTransactionStatus(extrinsicHash) + .then((transactionStatus) => { + if (transactionStatus.confirmed && transactionStatus.block_time > 0) { + clearInterval(interval); + eventEmitter.emit('success', transactionStatus); + } + }) + .catch(console.error); + }, 30000); + }) + .catch((error: Error) => { + eventEmitter.emit('error', error.message); + }) + ; + + return eventEmitter; + } + + simpleSendRawTransaction (rawTransaction: string) { + return this.addRequest(async (): Promise => { + const response = await postRequest(this.getUrl('tx'), rawTransaction, { 'Content-Type': 'text/plain' }, false); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.simpleSendRawTransaction', `Failed to broadcast transaction: ${errorText}`); + } + + return await response.text(); + }, 0); + } + + async getRunes (address: string) { + const runesFullList: RunesInfoByAddress[] = []; + const pageSize = 60; + let offset = 0; + + const runeService = RunesService.getInstance(this.isTestnet); + + try { + while (true) { + const response = await runeService.getAddressRunesInfo(address, { + limit: String(pageSize), + offset: String(offset) + }) as unknown as RunesInfoByAddressFetchedData; + + const runes = response.runes; + + if (runes.length !== 0) { + runesFullList.push(...runes); + offset += pageSize; + } else { + break; + } + } + + return runesFullList; + } catch (error) { + console.error(`Failed to get ${address} balances`, error); + throw error; + } + } + + async getRuneUtxos (address: string) { + const runeService = RunesService.getInstance(this.isTestnet); + + try { + const responseRuneUtxos = await runeService.getAddressRuneUtxos(address); + + return responseRuneUtxos.results; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + throw new SWError('BlockStreamTestnetRequestStrategy.getRuneUtxos', `Failed to get ${address} rune utxos: ${errorMessage}`); + } + } + + async getAddressInscriptions (address: string) { + const inscriptionsFullList: Inscription[] = []; + const pageSize = 60; + let offset = 0; + + const hiroService = HiroService.getInstance(this.isTestnet); + + try { + while (true) { + const response = await hiroService.getAddressInscriptionsInfo({ + limit: String(pageSize), + offset: String(offset), + address: String(address) + }) as unknown as InscriptionFetchedData; + + const inscriptions = response.results; + + if (inscriptions.length !== 0) { + inscriptionsFullList.push(...inscriptions); + offset += pageSize; + } else { + break; + } + } + + return inscriptionsFullList; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + throw new SWError('BlockStreamTestnetRequestStrategy.getAddressInscriptions', `Failed to get ${address} inscriptions: ${errorMessage}`); + } + } + + getTxHex (txHash: string): Promise { + return this.addRequest(async (): Promise => { + const response = await getRequest(this.getUrl(`tx/${txHash}/hex`), undefined, this.headers); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.getTxHex', `Failed to fetch transaction hex: ${errorText}`); + } + + return await response.text(); + }, 0); + } +} diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts index e2994513e5f..8cfa4ff1851 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/index.ts @@ -1,398 +1,5 @@ -// Copyright 2019-2022 @subwallet/extension-base +// Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { SWError } from '@subwallet/extension-base/background/errors/SWError'; -import { BitcoinAddressSummaryInfo, BitcoinApiStrategy, BitcoinTransactionEventMap, BlockstreamAddressResponse, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, BlockStreamUtxo, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; -import { HiroService } from '@subwallet/extension-base/services/hiro-service'; -import { RunesService } from '@subwallet/extension-base/services/rune-service'; -import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; -import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; -import { getRequest, postRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; -import { BitcoinFeeInfo, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; -import BigN from 'bignumber.js'; -import EventEmitter from 'eventemitter3'; - -export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy implements BitcoinApiStrategy { - private readonly baseUrl: string; - private readonly isTestnet: boolean; - private timePerBlock = 0; // in milliseconds - - constructor (url: string) { - const context = new BaseApiRequestContext(); - - super(context); - - this.baseUrl = url; - this.isTestnet = url.includes('testnet'); - - this.getBlockTime() - .then((rs) => { - this.timePerBlock = rs; - }) - .catch(() => { - this.timePerBlock = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; - }); - } - - private headers = { - 'Content-Type': 'application/json' - }; - - isRateLimited (): boolean { - return false; - } - - getUrl (path: string): string { - return `${this.baseUrl}/${path}`; - } - - getBlockTime (): Promise { - return this.addRequest(async () => { - const response = await getRequest(this.getUrl('blocks'), undefined, this.headers); - const blocks = await response.json() as BlockStreamBlock[]; - - if (!response.ok) { - throw new SWError('BlockStreamTestnetRequestStrategy.getBlockTime', 'Failed to fetch blocks'); - } - - const length = blocks.length; - const sortedBlocks = blocks.sort((a, b) => b.timestamp - a.timestamp); - const time = (sortedBlocks[0].timestamp - sortedBlocks[length - 1].timestamp) * 1000; - - return time / length; - }, 0); - } - - getAddressSummaryInfo (address: string): Promise { - return this.addRequest(async () => { - const response = await getRequest(this.getUrl(`address/${address}`), undefined, this.headers); - - if (!response.ok) { - throw new SWError('BlockStreamTestnetRequestStrategy.getAddressSummaryInfo', 'Failed to fetch address info'); - } - - const rsRaw = await response.json() as BlockstreamAddressResponse; - const chainBalance = rsRaw.chain_stats.funded_txo_sum - rsRaw.chain_stats.spent_txo_sum; - const pendingLocked = rsRaw.mempool_stats.spent_txo_sum; // Only consider spent UTXOs in mempool - const mempoolReceived = rsRaw.mempool_stats.funded_txo_sum; // Funds received in mempool (e.g., change) - const availableBalance = Math.max(0, chainBalance - pendingLocked + mempoolReceived); // Ensure balance is non-negative - - const rs: BitcoinAddressSummaryInfo = { - address: rsRaw.address, - chain_stats: { - funded_txo_count: rsRaw.chain_stats.funded_txo_count, - funded_txo_sum: rsRaw.chain_stats.funded_txo_sum, - spent_txo_count: rsRaw.chain_stats.spent_txo_count, - spent_txo_sum: rsRaw.chain_stats.spent_txo_sum, - tx_count: rsRaw.chain_stats.tx_count - }, - mempool_stats: { - funded_txo_count: rsRaw.mempool_stats.funded_txo_count, - funded_txo_sum: rsRaw.mempool_stats.funded_txo_sum, - spent_txo_count: rsRaw.mempool_stats.spent_txo_count, - spent_txo_sum: rsRaw.mempool_stats.spent_txo_sum, - tx_count: rsRaw.mempool_stats.tx_count - }, - balance: availableBalance, - total_inscription: 0, - balance_rune: '0', - balance_inscription: '0' - }; - - return rs; - }, 0); - } - - getAddressTransaction (address: string, limit = 100): Promise { - return this.addRequest(async () => { - const response = await getRequest(this.getUrl(`address/${address}/txs`), { limit: `${limit}` }, this.headers); - - if (!response.ok) { - throw new SWError('BlockStreamTestnetRequestStrategy.getAddressTransaction', 'Failed to fetch transactions'); - } - - return await response.json() as BitcoinTx[]; - }, 1); - } - - getTransactionStatus (txHash: string): Promise { - return this.addRequest(async () => { - const response = await getRequest(this.getUrl(`tx/${txHash}/status`), undefined, {}); - - if (!response.ok) { - const errorText = await response.text(); - - throw new SWError('BlockStreamTestnetRequestStrategy.getTransactionStatus', `Failed to fetch transaction status: ${errorText}`); - } - - // Blockstream API trả về object thô - const data = await response.json() as BlockStreamTransactionStatus; - - return { - confirmed: data.confirmed || false, - block_time: data.block_time || 0, - block_height: data.block_height, - block_hash: data.block_hash - }; - }, 1); - } - - getTransactionDetail (txHash: string): Promise { - return this.addRequest(async () => { - const response = await getRequest(this.getUrl(`tx/${txHash}`), undefined, this.headers); - - if (!response.ok) { - throw new SWError('BlockStreamTestnetRequestStrategy.getTransactionDetail', 'Failed to fetch transaction detail'); - } - - return await response.json() as BlockStreamTransactionDetail; - }, 1); - } - - // TODO: NOTE: Currently not in use. Recheck the response if you want to use it. - getFeeRate (): Promise { - return this.addRequest(async (): Promise => { - const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); - const estimates = await response.json() as BlockStreamFeeEstimates; - - console.log('getRecommendedFeeRate: rs', estimates); - - if (!response.ok) { - throw new SWError('BlockStreamTestnetRequestStrategy.getFeeRate', 'Failed to fetch fee estimates'); - } - - const low = 6; - const average = 3; - const fast = 1; - - const convertFee = (fee: number) => parseFloat(new BigN(fee).toFixed(2)); - - return { - type: 'bitcoin', - busyNetwork: false, - options: { - slow: { feeRate: convertFee(estimates[low] || 10), time: this.timePerBlock * low }, - average: { feeRate: convertFee(estimates[average || 12]), time: this.timePerBlock * average }, - fast: { feeRate: convertFee(estimates[fast] || 15), time: this.timePerBlock * fast }, - default: 'slow' - } - }; - }, 0); - } - - getRecommendedFeeRate (): Promise { - return this.addRequest(async (): Promise => { - const convertTimeMilisec = { - fastestFee: 10 * 60000, - halfHourFee: 30 * 60000, - hourFee: 60 * 60000 - }; - - const defaultFeeInfo: BitcoinFeeInfo = { - type: 'bitcoin', - busyNetwork: false, - options: { - slow: { feeRate: 1, time: convertTimeMilisec.hourFee }, - average: { feeRate: 1, time: convertTimeMilisec.halfHourFee }, - fast: { feeRate: 1, time: convertTimeMilisec.fastestFee }, - default: 'slow' - } - }; - - try { - const response = await getRequest(this.getUrl('v1/fees/recommended'), undefined, this.headers); - - if (!response.ok) { - console.warn(`Failed to fetch fee estimates: ${response.statusText}`); - - return defaultFeeInfo; - } - - const estimates = await response.json() as RecommendedFeeEstimates; - - const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed(), 10); - - return { - type: 'bitcoin', - busyNetwork: false, - options: { - slow: { feeRate: convertFee(estimates.hourFee || 1), time: convertTimeMilisec.hourFee }, - average: { feeRate: convertFee(estimates.halfHourFee || 1), time: convertTimeMilisec.halfHourFee }, - fast: { feeRate: convertFee(estimates.fastestFee || 1), time: convertTimeMilisec.fastestFee }, - default: 'slow' - } - }; - } catch { - return defaultFeeInfo; - } - }, 0); - } - - getUtxos (address: string): Promise { - return this.addRequest(async (): Promise => { - const response = await getRequest(this.getUrl(`address/${address}/utxo`), undefined, {}); - const rs = await response.json() as BlockStreamUtxo[]; - - if (!response.ok) { - const errorText = await response.text(); - - throw new SWError('BlockStreamTestnetRequestStrategy.getUtxos', `Failed to fetch UTXOs: ${errorText}`); - } - - return rs.map((item: BlockStreamUtxo) => ({ - txid: item.txid, - vout: item.vout, - value: item.value, - status: item.status - })); - }, 0); - } - - sendRawTransaction (rawTransaction: string) { - const eventEmitter = new EventEmitter(); - - this.addRequest(async (): Promise => { - const response = await postRequest( - this.getUrl('tx'), - rawTransaction, - { 'Content-Type': 'text/plain' }, - false - ); - - if (!response.ok) { - const errorText = await response.text(); - - throw new SWError('BlockStreamTestnetRequestStrategy.sendRawTransaction', `Failed to broadcast transaction: ${errorText}`); - } - - return await response.text(); - }, 0) - .then((extrinsicHash) => { - eventEmitter.emit('extrinsicHash', extrinsicHash); - - // Check transaction status - const interval = setInterval(() => { - this.getTransactionStatus(extrinsicHash) - .then((transactionStatus) => { - if (transactionStatus.confirmed && transactionStatus.block_time > 0) { - clearInterval(interval); - eventEmitter.emit('success', transactionStatus); - } - }) - .catch(console.error); - }, 30000); - }) - .catch((error: Error) => { - eventEmitter.emit('error', error.message); - }) - ; - - return eventEmitter; - } - - simpleSendRawTransaction (rawTransaction: string) { - return this.addRequest(async (): Promise => { - const response = await postRequest(this.getUrl('tx'), rawTransaction, { 'Content-Type': 'text/plain' }, false); - - if (!response.ok) { - const errorText = await response.text(); - - throw new SWError('BlockStreamTestnetRequestStrategy.simpleSendRawTransaction', `Failed to broadcast transaction: ${errorText}`); - } - - return await response.text(); - }, 0); - } - - async getRunes (address: string) { - const runesFullList: RunesInfoByAddress[] = []; - const pageSize = 60; - let offset = 0; - - const runeService = RunesService.getInstance(this.isTestnet); - - try { - while (true) { - const response = await runeService.getAddressRunesInfo(address, { - limit: String(pageSize), - offset: String(offset) - }) as unknown as RunesInfoByAddressFetchedData; - - const runes = response.runes; - - if (runes.length !== 0) { - runesFullList.push(...runes); - offset += pageSize; - } else { - break; - } - } - - return runesFullList; - } catch (error) { - console.error(`Failed to get ${address} balances`, error); - throw error; - } - } - - async getRuneUtxos (address: string) { - const runeService = RunesService.getInstance(this.isTestnet); - - try { - const responseRuneUtxos = await runeService.getAddressRuneUtxos(address); - - return responseRuneUtxos.results; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - - throw new SWError('BlockStreamTestnetRequestStrategy.getRuneUtxos', `Failed to get ${address} rune utxos: ${errorMessage}`); - } - } - - async getAddressInscriptions (address: string) { - const inscriptionsFullList: Inscription[] = []; - const pageSize = 60; - let offset = 0; - - const hiroService = HiroService.getInstance(this.isTestnet); - - try { - while (true) { - const response = await hiroService.getAddressInscriptionsInfo({ - limit: String(pageSize), - offset: String(offset), - address: String(address) - }) as unknown as InscriptionFetchedData; - - const inscriptions = response.results; - - if (inscriptions.length !== 0) { - inscriptionsFullList.push(...inscriptions); - offset += pageSize; - } else { - break; - } - } - - return inscriptionsFullList; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - - throw new SWError('BlockStreamTestnetRequestStrategy.getAddressInscriptions', `Failed to get ${address} inscriptions: ${errorMessage}`); - } - } - - getTxHex (txHash: string): Promise { - return this.addRequest(async (): Promise => { - const response = await getRequest(this.getUrl(`tx/${txHash}/hex`), undefined, this.headers); - - if (!response.ok) { - const errorText = await response.text(); - - throw new SWError('BlockStreamTestnetRequestStrategy.getTxHex', `Failed to fetch transaction hex: ${errorText}`); - } - - return await response.text(); - }, 0); - } -} +export { MempoolTestnetRequestStrategy } from './mempool-testnet-strategy'; +export { BlockStreamTestnetRequestStrategy } from './blockstream-testnet-strategy'; diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts new file mode 100644 index 00000000000..992f78120b9 --- /dev/null +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts @@ -0,0 +1,398 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { SWError } from '@subwallet/extension-base/background/errors/SWError'; +import { BitcoinAddressSummaryInfo, BitcoinApiStrategy, BitcoinTransactionEventMap, BlockstreamAddressResponse, BlockStreamBlock, BlockStreamFeeEstimates, BlockStreamTransactionDetail, BlockStreamTransactionStatus, BlockStreamUtxo, Inscription, InscriptionFetchedData, RecommendedFeeEstimates, RunesInfoByAddress, RunesInfoByAddressFetchedData } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/types'; +import { HiroService } from '@subwallet/extension-base/services/hiro-service'; +import { RunesService } from '@subwallet/extension-base/services/rune-service'; +import { BaseApiRequestStrategy } from '@subwallet/extension-base/strategy/api-request-strategy'; +import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base'; +import { getRequest, postRequest } from '@subwallet/extension-base/strategy/api-request-strategy/utils'; +import { BitcoinFeeInfo, BitcoinTx, UtxoResponseItem } from '@subwallet/extension-base/types'; +import BigN from 'bignumber.js'; +import EventEmitter from 'eventemitter3'; + +export class MempoolTestnetRequestStrategy extends BaseApiRequestStrategy implements BitcoinApiStrategy { + private readonly baseUrl: string; + private readonly isTestnet: boolean; + private timePerBlock = 0; // in milliseconds + + constructor (url: string) { + const context = new BaseApiRequestContext(); + + super(context); + + this.baseUrl = url; + this.isTestnet = url.includes('testnet'); + + this.getBlockTime() + .then((rs) => { + this.timePerBlock = rs; + }) + .catch(() => { + this.timePerBlock = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; + }); + } + + private headers = { + 'Content-Type': 'application/json' + }; + + isRateLimited (): boolean { + return false; + } + + getUrl (path: string): string { + return `${this.baseUrl}/${path}`; + } + + getBlockTime (): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl('blocks'), undefined, this.headers); + const blocks = await response.json() as BlockStreamBlock[]; + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getBlockTime', 'Failed to fetch blocks'); + } + + const length = blocks.length; + const sortedBlocks = blocks.sort((a, b) => b.timestamp - a.timestamp); + const time = (sortedBlocks[0].timestamp - sortedBlocks[length - 1].timestamp) * 1000; + + return time / length; + }, 0); + } + + getAddressSummaryInfo (address: string): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`address/${address}`), undefined, this.headers); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getAddressSummaryInfo', 'Failed to fetch address info'); + } + + const rsRaw = await response.json() as BlockstreamAddressResponse; + const chainBalance = rsRaw.chain_stats.funded_txo_sum - rsRaw.chain_stats.spent_txo_sum; + const pendingLocked = rsRaw.mempool_stats.spent_txo_sum; // Only consider spent UTXOs in mempool + const mempoolReceived = rsRaw.mempool_stats.funded_txo_sum; // Funds received in mempool (e.g., change) + const availableBalance = Math.max(0, chainBalance - pendingLocked + mempoolReceived); // Ensure balance is non-negative + + const rs: BitcoinAddressSummaryInfo = { + address: rsRaw.address, + chain_stats: { + funded_txo_count: rsRaw.chain_stats.funded_txo_count, + funded_txo_sum: rsRaw.chain_stats.funded_txo_sum, + spent_txo_count: rsRaw.chain_stats.spent_txo_count, + spent_txo_sum: rsRaw.chain_stats.spent_txo_sum, + tx_count: rsRaw.chain_stats.tx_count + }, + mempool_stats: { + funded_txo_count: rsRaw.mempool_stats.funded_txo_count, + funded_txo_sum: rsRaw.mempool_stats.funded_txo_sum, + spent_txo_count: rsRaw.mempool_stats.spent_txo_count, + spent_txo_sum: rsRaw.mempool_stats.spent_txo_sum, + tx_count: rsRaw.mempool_stats.tx_count + }, + balance: availableBalance, + total_inscription: 0, + balance_rune: '0', + balance_inscription: '0' + }; + + return rs; + }, 0); + } + + getAddressTransaction (address: string, limit = 100): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`address/${address}/txs`), { limit: `${limit}` }, this.headers); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getAddressTransaction', 'Failed to fetch transactions'); + } + + return await response.json() as BitcoinTx[]; + }, 1); + } + + getTransactionStatus (txHash: string): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`tx/${txHash}/status`), undefined, {}); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.getTransactionStatus', `Failed to fetch transaction status: ${errorText}`); + } + + // Blockstream API trả về object thô + const data = await response.json() as BlockStreamTransactionStatus; + + return { + confirmed: data.confirmed || false, + block_time: data.block_time || 0, + block_height: data.block_height, + block_hash: data.block_hash + }; + }, 1); + } + + getTransactionDetail (txHash: string): Promise { + return this.addRequest(async () => { + const response = await getRequest(this.getUrl(`tx/${txHash}`), undefined, this.headers); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getTransactionDetail', 'Failed to fetch transaction detail'); + } + + return await response.json() as BlockStreamTransactionDetail; + }, 1); + } + + // TODO: NOTE: Currently not in use. Recheck the response if you want to use it. + getFeeRate (): Promise { + return this.addRequest(async (): Promise => { + const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); + const estimates = await response.json() as BlockStreamFeeEstimates; + + console.log('getRecommendedFeeRate: rs', estimates); + + if (!response.ok) { + throw new SWError('BlockStreamTestnetRequestStrategy.getFeeRate', 'Failed to fetch fee estimates'); + } + + const low = 6; + const average = 3; + const fast = 1; + + const convertFee = (fee: number) => parseFloat(new BigN(fee).toFixed(2)); + + return { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: convertFee(estimates[low] || 10), time: this.timePerBlock * low }, + average: { feeRate: convertFee(estimates[average || 12]), time: this.timePerBlock * average }, + fast: { feeRate: convertFee(estimates[fast] || 15), time: this.timePerBlock * fast }, + default: 'slow' + } + }; + }, 0); + } + + getRecommendedFeeRate (): Promise { + return this.addRequest(async (): Promise => { + const convertTimeMilisec = { + fastestFee: 10 * 60000, + halfHourFee: 30 * 60000, + hourFee: 60 * 60000 + }; + + const defaultFeeInfo: BitcoinFeeInfo = { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: 1, time: convertTimeMilisec.hourFee }, + average: { feeRate: 1, time: convertTimeMilisec.halfHourFee }, + fast: { feeRate: 1, time: convertTimeMilisec.fastestFee }, + default: 'slow' + } + }; + + try { + const response = await getRequest(this.getUrl('v1/fees/recommended'), undefined, this.headers); + + if (!response.ok) { + console.warn(`Failed to fetch fee estimates: ${response.statusText}`); + + return defaultFeeInfo; + } + + const estimates = await response.json() as RecommendedFeeEstimates; + + const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed(), 10); + + return { + type: 'bitcoin', + busyNetwork: false, + options: { + slow: { feeRate: convertFee(estimates.hourFee || 1), time: convertTimeMilisec.hourFee }, + average: { feeRate: convertFee(estimates.halfHourFee || 1), time: convertTimeMilisec.halfHourFee }, + fast: { feeRate: convertFee(estimates.fastestFee || 1), time: convertTimeMilisec.fastestFee }, + default: 'slow' + } + }; + } catch { + return defaultFeeInfo; + } + }, 0); + } + + getUtxos (address: string): Promise { + return this.addRequest(async (): Promise => { + const response = await getRequest(this.getUrl(`address/${address}/utxo`), undefined, {}); + const rs = await response.json() as BlockStreamUtxo[]; + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.getUtxos', `Failed to fetch UTXOs: ${errorText}`); + } + + return rs.map((item: BlockStreamUtxo) => ({ + txid: item.txid, + vout: item.vout, + value: item.value, + status: item.status + })); + }, 0); + } + + sendRawTransaction (rawTransaction: string) { + const eventEmitter = new EventEmitter(); + + this.addRequest(async (): Promise => { + const response = await postRequest( + this.getUrl('tx'), + rawTransaction, + { 'Content-Type': 'text/plain' }, + false + ); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.sendRawTransaction', `Failed to broadcast transaction: ${errorText}`); + } + + return await response.text(); + }, 0) + .then((extrinsicHash) => { + eventEmitter.emit('extrinsicHash', extrinsicHash); + + // Check transaction status + const interval = setInterval(() => { + this.getTransactionStatus(extrinsicHash) + .then((transactionStatus) => { + if (transactionStatus.confirmed && transactionStatus.block_time > 0) { + clearInterval(interval); + eventEmitter.emit('success', transactionStatus); + } + }) + .catch(console.error); + }, 30000); + }) + .catch((error: Error) => { + eventEmitter.emit('error', error.message); + }) + ; + + return eventEmitter; + } + + simpleSendRawTransaction (rawTransaction: string) { + return this.addRequest(async (): Promise => { + const response = await postRequest(this.getUrl('tx'), rawTransaction, { 'Content-Type': 'text/plain' }, false); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.simpleSendRawTransaction', `Failed to broadcast transaction: ${errorText}`); + } + + return await response.text(); + }, 0); + } + + async getRunes (address: string) { + const runesFullList: RunesInfoByAddress[] = []; + const pageSize = 60; + let offset = 0; + + const runeService = RunesService.getInstance(this.isTestnet); + + try { + while (true) { + const response = await runeService.getAddressRunesInfo(address, { + limit: String(pageSize), + offset: String(offset) + }) as unknown as RunesInfoByAddressFetchedData; + + const runes = response.runes; + + if (runes.length !== 0) { + runesFullList.push(...runes); + offset += pageSize; + } else { + break; + } + } + + return runesFullList; + } catch (error) { + console.error(`Failed to get ${address} balances`, error); + throw error; + } + } + + async getRuneUtxos (address: string) { + const runeService = RunesService.getInstance(this.isTestnet); + + try { + const responseRuneUtxos = await runeService.getAddressRuneUtxos(address); + + return responseRuneUtxos.results; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + throw new SWError('BlockStreamTestnetRequestStrategy.getRuneUtxos', `Failed to get ${address} rune utxos: ${errorMessage}`); + } + } + + async getAddressInscriptions (address: string) { + const inscriptionsFullList: Inscription[] = []; + const pageSize = 60; + let offset = 0; + + const hiroService = HiroService.getInstance(this.isTestnet); + + try { + while (true) { + const response = await hiroService.getAddressInscriptionsInfo({ + limit: String(pageSize), + offset: String(offset), + address: String(address) + }) as unknown as InscriptionFetchedData; + + const inscriptions = response.results; + + if (inscriptions.length !== 0) { + inscriptionsFullList.push(...inscriptions); + offset += pageSize; + } else { + break; + } + } + + return inscriptionsFullList; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + throw new SWError('BlockStreamTestnetRequestStrategy.getAddressInscriptions', `Failed to get ${address} inscriptions: ${errorMessage}`); + } + } + + getTxHex (txHash: string): Promise { + return this.addRequest(async (): Promise => { + const response = await getRequest(this.getUrl(`tx/${txHash}/hex`), undefined, this.headers); + + if (!response.ok) { + const errorText = await response.text(); + + throw new SWError('BlockStreamTestnetRequestStrategy.getTxHex', `Failed to fetch transaction hex: ${errorText}`); + } + + return await response.text(); + }, 0); + } +} From 2286e8f8ba74c8ef9d5228e8a80a5ed73b6b75e7 Mon Sep 17 00:00:00 2001 From: lw Date: Mon, 9 Jun 2025 17:59:48 +0700 Subject: [PATCH 148/178] [Issue-4425] refactor: Update receive modal logic --- .../Modal/Global/AccountTokenAddressModal.tsx | 5 +- .../Modal/ReceiveModalNew/index.tsx | 2 +- .../AccountSelector/AccountSelectorItem.tsx | 159 +++++++++++ .../parts/AccountSelector/index.tsx | 263 ++++++++++++++++++ .../hooks/common/useGetBitcoinAccounts.tsx | 12 +- .../screen/home/useCoreReceiveModalHelper.tsx | 138 ++++++--- .../src/utils/account/account.ts | 10 +- 7 files changed, 532 insertions(+), 57 deletions(-) create mode 100644 packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/AccountSelectorItem.tsx create mode 100644 packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/index.tsx diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx index 294994cb791..bc914d95d7e 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx @@ -110,7 +110,8 @@ const Component: React.FC = ({ className, items, onBack, onCancel }: Prop title={t('Select address type')} >
-
+ {/* TODO: Move this description content into a prop passed to the component */} +
{t('SubWallet supports three Bitcoin address types for receiving and transferring assets. Make sure you choose the correct address type to avoid risks of fund loss. ')} (({ theme: { token } }: marginTop: 8 }, - '.sub-title': { + '.description': { paddingBottom: token.padding, fontSize: token.fontSizeSM, fontWeight: token.bodyFontWeight, diff --git a/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/index.tsx b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/index.tsx index 0cf9da5e8dd..f94470cd525 100644 --- a/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/index.tsx +++ b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/index.tsx @@ -1,11 +1,11 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AccountSelectorModal } from '@subwallet/extension-koni-ui/components'; import { RECEIVE_MODAL_ACCOUNT_SELECTOR } from '@subwallet/extension-koni-ui/constants'; import { ReceiveModalProps } from '@subwallet/extension-koni-ui/types'; import React from 'react'; +import { AccountSelectorModal } from './parts/AccountSelector'; import { TokenSelectorModal } from './parts/TokenSelector'; const ReceiveModal = ({ accountSelectorItems, diff --git a/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/AccountSelectorItem.tsx b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/AccountSelectorItem.tsx new file mode 100644 index 00000000000..0970c5c0102 --- /dev/null +++ b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/AccountSelectorItem.tsx @@ -0,0 +1,159 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { AccountProxyAvatar } from '@subwallet/extension-koni-ui/components/AccountProxy'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { toShort } from '@subwallet/extension-koni-ui/utils'; +import { Icon } from '@subwallet/react-ui'; +import CN from 'classnames'; +import { CheckCircle } from 'phosphor-react'; +import React from 'react'; +import styled from 'styled-components'; + +type Props = ThemeProps & { + name?: string; + avatarValue?: string; + address: string; + onClick?: VoidFunction; + isSelected?: boolean; + showUnselectIcon?: boolean; +} + +function Component (props: Props): React.ReactElement { + const { address, + avatarValue, + className, isSelected, name, onClick, showUnselectIcon } = props; + + return ( +
+
+ +
+ +
+ {name + ? ( + <> +
+
+ {name} +
+
+
+ {toShort(address, 9, 10)} +
+ + ) + : ( +
+
+ {toShort(address, 9, 10)} +
+
+ )} +
+ +
+ {(isSelected || showUnselectIcon) && ( +
+ +
+ )} +
+
+ ); +} + +export const AccountSelectorItem = styled(Component)(({ theme: { token } }: Props) => { + return { + background: token.colorBgSecondary, + paddingLeft: token.paddingSM, + paddingRight: token.paddingSM, + paddingTop: 6, + paddingBottom: 6, + borderRadius: token.borderRadiusLG, + alignItems: 'center', + display: 'flex', + flexDirection: 'row', + cursor: 'pointer', + transition: `background ${token.motionDurationMid} ease-in-out`, + overflowX: 'hidden', + minHeight: 52, + + '.__avatar': { + marginRight: token.marginXS + }, + + '.__item-center-part': { + display: 'flex', + flexDirection: 'column', + overflowX: 'hidden', + 'white-space': 'nowrap', + flex: 1, + fontSize: token.fontSize, + lineHeight: token.lineHeight + }, + + '.__item-name-wrapper': { + display: 'flex', + alignItems: 'baseline' + }, + + '.__item-address-wrapper': { + display: 'flex', + gap: 12, + alignItems: 'baseline', + '.__address': { + fontSize: token.fontSize + } + }, + + '.__item-right-part': { + display: 'flex' + }, + + '.__checked-icon-wrapper': { + display: 'flex', + justifyContent: 'center', + minWidth: 40, + marginRight: -token.marginXS, + color: token.colorTextLight4, + + '&.-selected': { + color: token.colorSuccess + } + }, + + '.__name': { + color: token.colorTextLight1, + overflow: 'hidden', + textOverflow: 'ellipsis', + fontWeight: token.fontWeightStrong + }, + + '.__address': { + color: token.colorTextLight4, + fontSize: token.fontSizeSM, + fontWeight: token.bodyFontWeight, + lineHeight: token.lineHeightSM + }, + + '&:hover': { + background: token.colorBgInput + } + }; +}); diff --git a/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/index.tsx b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/index.tsx new file mode 100644 index 00000000000..6a05a116988 --- /dev/null +++ b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/index.tsx @@ -0,0 +1,263 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { AccountProxyType } from '@subwallet/extension-base/types'; +import { CloseIcon } from '@subwallet/extension-koni-ui/components'; +import GeneralEmptyList from '@subwallet/extension-koni-ui/components/EmptyList/GeneralEmptyList'; +import Search from '@subwallet/extension-koni-ui/components/Search'; +import { useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { AccountAddressItemType, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { Icon, ModalContext, SwList, SwModal } from '@subwallet/react-ui'; +import CN from 'classnames'; +import { CaretLeft } from 'phosphor-react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +import { AccountSelectorItem } from './AccountSelectorItem'; + +// NOTE: +// This component is specifically designed for ReceiveModal. +// Although it shares similarities with the general AccountSelector (Selector/AccountSelector.tsx), +// it is separated to avoid impacting other use cases and to prevent +// mixing ReceiveModal-specific logic into the shared component, +// which would complicate maintenance. + +type ListItemGroupLabel = { + id: string; + groupLabel: string; +} + +type ListItem = AccountAddressItemType | ListItemGroupLabel; + +interface Props extends ThemeProps { + modalId: string; + onSelectItem?: (item: AccountAddressItemType) => void, + items: AccountAddressItemType[]; + onCancel?: VoidFunction; + onBack?: VoidFunction; + selectedValue?: string; +} + +const renderEmpty = () => ; + +function Component ({ className = '', items, modalId, onBack, onCancel, onSelectItem, selectedValue }: Props): React.ReactElement { + const { t } = useTranslation(); + const { checkActive } = useContext(ModalContext); + + const [searchValue, setSearchValue] = useState(''); + + const isActive = checkActive(modalId); + + const searchFunction = useCallback((item: AccountAddressItemType, searchText: string) => { + const lowerCaseSearchText = searchText.toLowerCase(); + + return item.accountName.toLowerCase().includes(lowerCaseSearchText) || + item.address.toLowerCase().includes(lowerCaseSearchText); + }, []); + + const onSelect = useCallback((item: AccountAddressItemType) => { + return () => { + onSelectItem?.(item); + }; + }, [onSelectItem]); + + const renderItem = useCallback((item: ListItem) => { + if ((item as ListItemGroupLabel).groupLabel) { + return ( +
+ {(item as ListItemGroupLabel).groupLabel} +
+ ); + } + + return ( + + ); + }, [onSelect, selectedValue]); + + const listItems = useMemo(() => { + const result: ListItem[] = []; + const masterAccounts: AccountAddressItemType[] = []; + const qrSignerAccounts: ListItem[] = []; + const watchOnlyAccounts: ListItem[] = []; + const ledgerAccounts: ListItem[] = []; + const injectedAccounts: ListItem[] = []; + const unknownAccounts: ListItem[] = []; + + items.forEach((item) => { + if (searchValue && !searchFunction(item, searchValue)) { + return; + } + + if (item.accountProxyType === AccountProxyType.SOLO || item.accountProxyType === AccountProxyType.UNIFIED) { + masterAccounts.push(item); + } else if (item.accountProxyType === AccountProxyType.QR) { + qrSignerAccounts.push(item); + } else if (item.accountProxyType === AccountProxyType.READ_ONLY) { + watchOnlyAccounts.push(item); + } else if (item.accountProxyType === AccountProxyType.LEDGER) { + ledgerAccounts.push(item); + } else if (item.accountProxyType === AccountProxyType.INJECTED) { + injectedAccounts.push(item); + } else if (item.accountProxyType === AccountProxyType.UNKNOWN) { + unknownAccounts.push(item); + } + }); + + if (masterAccounts.length) { + result.push(...masterAccounts); + } + + if (qrSignerAccounts.length) { + qrSignerAccounts.unshift({ + id: 'qr', + groupLabel: t('QR signer account') + }); + + result.push(...qrSignerAccounts); + } + + if (watchOnlyAccounts.length) { + watchOnlyAccounts.unshift({ + id: 'watch-only', + groupLabel: t('Watch-only account') + }); + + result.push(...watchOnlyAccounts); + } + + if (ledgerAccounts.length) { + ledgerAccounts.unshift({ + id: 'ledger', + groupLabel: t('Ledger account') + }); + + result.push(...ledgerAccounts); + } + + if (injectedAccounts.length) { + injectedAccounts.unshift({ + id: 'injected', + groupLabel: t('Injected account') + }); + + result.push(...ledgerAccounts); + } + + if (unknownAccounts.length) { + unknownAccounts.unshift({ + id: 'unknown', + groupLabel: t('Unknown account') + }); + + result.push(...unknownAccounts); + } + + return result; + }, [items, searchFunction, searchValue, t]); + + const handleSearch = useCallback((value: string) => { + setSearchValue(value); + }, []); + + useEffect(() => { + if (!isActive) { + setTimeout(() => { + setSearchValue(''); + }, 100); + } + }, [isActive]); + + return ( + + ) + : undefined + } + destroyOnClose={true} + id={modalId} + onCancel={onBack || onCancel} + rightIconProps={onBack + ? { + icon: , + onClick: onCancel + } + : undefined} + title={t('Select account')} + > + ('Enter your account name or address')} + searchValue={searchValue} + /> + + + ); +} + +export const AccountSelectorModal = styled(Component)(({ theme: { token } }: Props) => { + return ({ + '.ant-sw-modal-content': { + height: '100vh' + }, + + '.ant-sw-modal-body': { + overflow: 'auto', + display: 'flex', + flex: 1, + flexDirection: 'column' + }, + + '.list-item-group-label': { + textTransform: 'uppercase', + fontSize: 11, + lineHeight: '18px', + fontWeight: token.headingFontWeight, + color: token.colorTextLight3 + }, + + '.__search-box': { + marginBottom: token.marginXS + }, + + '.__list-container': { + flex: 1, + overflow: 'auto', + + '> div + div': { + marginTop: token.marginXS + } + }, + + '.account-selector-item + .account-selector-item': { + marginTop: token.marginXS + } + }); +}); + +export default AccountSelectorModal; diff --git a/packages/extension-koni-ui/src/hooks/common/useGetBitcoinAccounts.tsx b/packages/extension-koni-ui/src/hooks/common/useGetBitcoinAccounts.tsx index fc79998a287..1d4695cd63e 100644 --- a/packages/extension-koni-ui/src/hooks/common/useGetBitcoinAccounts.tsx +++ b/packages/extension-koni-ui/src/hooks/common/useGetBitcoinAccounts.tsx @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { _ChainInfo } from '@subwallet/chain-list/types'; +import { _isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-base/services/chain-service/utils'; +import { AccountChainType } from '@subwallet/extension-base/types'; import { AccountInfoType, AccountTokenAddress } from '@subwallet/extension-koni-ui/types'; import { getBitcoinAccountDetails } from '@subwallet/extension-koni-ui/utils'; -import { BitcoinMainnetKeypairTypes, BitcoinTestnetKeypairTypes } from '@subwallet/keyring/types'; import { useCallback } from 'react'; const transformBitcoinAccounts = ( @@ -13,12 +14,9 @@ const transformBitcoinAccounts = ( tokenSlug: string, chainInfo: _ChainInfo ): AccountTokenAddress[] => { - const isBitcoinTestnet = chainInfo.isTestnet; - const keypairTypes = isBitcoinTestnet ? BitcoinTestnetKeypairTypes : BitcoinMainnetKeypairTypes; - return accounts .filter( - (acc) => keypairTypes.includes(acc.type) + (acc) => _isChainInfoCompatibleWithAccountInfo(chainInfo, AccountChainType.BITCOIN, acc.type) ) .map((item) => ({ accountInfo: item, @@ -28,9 +26,9 @@ const transformBitcoinAccounts = ( }; const useGetBitcoinAccounts = () => { - return useCallback((chainSlug: string, tokenSlug: string, chainInfo: _ChainInfo, accountProxy: AccountInfoType[]): AccountTokenAddress[] => { + return useCallback((chainSlug: string, tokenSlug: string, chainInfo: _ChainInfo, accounts: AccountInfoType[]): AccountTokenAddress[] => { const accountTokenAddressList = transformBitcoinAccounts( - accountProxy || [], + accounts || [], chainSlug, tokenSlug, chainInfo diff --git a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx index 04b6d7eaf77..faf0fa40850 100644 --- a/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx +++ b/packages/extension-koni-ui/src/hooks/screen/home/useCoreReceiveModalHelper.tsx @@ -1,17 +1,17 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { _ChainAsset } from '@subwallet/chain-list/types'; -import { _getAssetOriginChain, _getMultiChainAsset, _isChainBitcoinCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; +import { _getAssetOriginChain, _getMultiChainAsset, _isChainBitcoinCompatible, _isChainInfoCompatibleWithAccountInfo } from '@subwallet/extension-base/services/chain-service/utils'; import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; -import { AccountActions, AccountProxyType } from '@subwallet/extension-base/types'; +import { AccountActions, AccountChainType, AccountJson, AccountProxy, AccountProxyType } from '@subwallet/extension-base/types'; import { RECEIVE_MODAL_ACCOUNT_SELECTOR, RECEIVE_MODAL_TOKEN_SELECTOR } from '@subwallet/extension-koni-ui/constants'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; import { useCoreCreateReformatAddress, useGetBitcoinAccounts, useGetChainSlugsByCurrentAccountProxy, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain } from '@subwallet/extension-koni-ui/hooks'; import { useChainAssets } from '@subwallet/extension-koni-ui/hooks/assets'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, AccountTokenAddress, ReceiveModalProps } from '@subwallet/extension-koni-ui/types'; -import { KeypairType } from '@subwallet/keyring/types'; +import { BitcoinMainnetKeypairTypes, BitcoinTestnetKeypairTypes, KeypairType } from '@subwallet/keyring/types'; import { ModalContext } from '@subwallet/react-ui'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; @@ -21,6 +21,11 @@ type HookType = { receiveModalProps: ReceiveModalProps; }; +type SelectedTokenInfo = { + tokenSlug: string; + chainSlug: string; +} + const tokenSelectorModalId = RECEIVE_MODAL_TOKEN_SELECTOR; const accountSelectorModalId = RECEIVE_MODAL_ACCOUNT_SELECTOR; @@ -33,7 +38,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const currentAccountProxy = useSelector((state: RootState) => state.accountState.currentAccountProxy); const assetRegistryMap = useSelector((state: RootState) => state.assetRegistry.assetRegistry); const chainInfoMap = useSelector((state: RootState) => state.chainStore.chainInfoMap); - const [selectedChain, setSelectedChain] = useState(); + const [selectedTokenInfo, setSelectedTokenInfo] = useState(); const [selectedAccountAddressItem, setSelectedAccountAddressItem] = useState(); const { accountTokenAddressModal, addressQrModal, selectAddressFormatModal } = useContext(WalletModalContext); const chainSupported = useGetChainSlugsByCurrentAccountProxy(); @@ -41,12 +46,15 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const onHandleLedgerGenericAccountWarning = useHandleLedgerGenericAccountWarning(); const getReformatAddress = useCoreCreateReformatAddress(); const checkIsPolkadotUnifiedChain = useIsPolkadotUnifiedChain(); - const getBitcoinAccount = useGetBitcoinAccounts(); + const getBitcoinAccounts = useGetBitcoinAccounts(); - // chain related to tokenGroupSlug, if it is token slug - const specificChain = useMemo(() => { + // token info related to tokenGroupSlug, if it is token slug + const specificSelectedTokenInfo = useMemo(() => { if (tokenGroupSlug && assetRegistryMap[tokenGroupSlug]) { - return _getAssetOriginChain(assetRegistryMap[tokenGroupSlug]); + return { + tokenSlug: tokenGroupSlug, + chainSlug: _getAssetOriginChain(assetRegistryMap[tokenGroupSlug]) + }; } return undefined; @@ -137,7 +145,10 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo return; } - setSelectedChain(chainSlug); + setSelectedTokenInfo({ + tokenSlug: item.slug, + chainSlug + }); if (isAllAccount) { setTimeout(() => { @@ -150,14 +161,14 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const isBitcoinChain = _isChainBitcoinCompatible(chainInfo); if (isBitcoinChain) { - const accountTokenAddressList = getBitcoinAccount(chainSlug, item.slug, chainInfo, currentAccountProxy.accounts); + const accountTokenAddressList = getBitcoinAccounts(chainSlug, item.slug, chainInfo, currentAccountProxy.accounts); if (accountTokenAddressList.length > 1) { openAccountTokenAddressModal(accountTokenAddressList, () => { inactiveModal(tokenSelectorModalId); setSelectedAccountAddressItem(undefined); }); - } else { + } else if (accountTokenAddressList.length === 1) { openAddressQrModal(accountTokenAddressList[0].accountInfo.address, accountTokenAddressList[0].accountInfo.type, currentAccountProxy.id, chainSlug, () => { inactiveModal(tokenSelectorModalId); setSelectedAccountAddressItem(undefined); @@ -198,15 +209,15 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo break; } } - }, [currentAccountProxy, chainInfoMap, isAllAccount, checkIsPolkadotUnifiedChain, activeModal, getBitcoinAccount, openAccountTokenAddressModal, inactiveModal, openAddressQrModal, getReformatAddress, openAddressFormatModal]); + }, [currentAccountProxy, chainInfoMap, isAllAccount, checkIsPolkadotUnifiedChain, activeModal, getBitcoinAccounts, openAccountTokenAddressModal, inactiveModal, openAddressQrModal, getReformatAddress, openAddressFormatModal]); /* token Selector --- */ /* --- account Selector */ const accountSelectorItems = useMemo(() => { - const targetChain = specificChain || selectedChain; - const chainInfo = targetChain ? chainInfoMap[targetChain] : undefined; + const targetTokenInfo = specificSelectedTokenInfo || selectedTokenInfo; + const chainInfo = targetTokenInfo ? chainInfoMap[targetTokenInfo.chainSlug] : undefined; if (!chainInfo) { return []; @@ -214,53 +225,101 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo const result: AccountAddressItemType[] = []; + const updateResult = (ap: AccountProxy, a: AccountJson, chainInfo: _ChainInfo) => { + const reformatedAddress = getReformatAddress(a, chainInfo); + + if (reformatedAddress) { + result.push({ + accountName: ap.name, + accountProxyId: ap.id, + accountProxyType: ap.accountType, + accountType: a.type, + address: reformatedAddress, + accountActions: ap.accountActions + }); + } + }; + + const getPreferredBitcoinAccount = (accounts: AccountJson[]) => { + const bitcoinAccounts = accounts.filter((a) => a.chainType === AccountChainType.BITCOIN && _isChainInfoCompatibleWithAccountInfo(chainInfo, a.chainType, a.type)); + + return bitcoinAccounts.find((a) => a.type === 'bitcoin-84' || a.type === 'bittest-84') || bitcoinAccounts[0]; + }; + accountProxies.forEach((ap) => { - ap.accounts.forEach((a) => { - const reformatedAddress = getReformatAddress(a, chainInfo); + // case bitcoin accounts + if (ap.chainTypes.includes(AccountChainType.BITCOIN)) { + const preferredBitcoinAccount = getPreferredBitcoinAccount(ap.accounts); - if (reformatedAddress) { - result.push({ - accountName: ap.name, - accountProxyId: ap.id, - accountProxyType: ap.accountType, - accountType: a.type, - address: reformatedAddress, - accountActions: ap.accountActions - }); + preferredBitcoinAccount && updateResult(ap, preferredBitcoinAccount, chainInfo); + } + + // case non-bitcoin accounts + ap.accounts.forEach((a) => { + if (a.chainType === AccountChainType.BITCOIN) { + return; } + + updateResult(ap, a, chainInfo); }); }); return result; - }, [accountProxies, chainInfoMap, getReformatAddress, selectedChain, specificChain]); + }, [accountProxies, chainInfoMap, getReformatAddress, selectedTokenInfo, specificSelectedTokenInfo]); const onBackAccountSelector = useMemo(() => { // if specificChain has value, it means tokenSelector does not show up, so accountSelector does not have back action - if (specificChain) { + if (specificSelectedTokenInfo) { return undefined; } return () => { inactiveModal(accountSelectorModalId); }; - }, [inactiveModal, specificChain]); + }, [inactiveModal, specificSelectedTokenInfo]); const onCloseAccountSelector = useCallback(() => { inactiveModal(accountSelectorModalId); inactiveModal(tokenSelectorModalId); - setSelectedChain(undefined); + setSelectedTokenInfo(undefined); setSelectedAccountAddressItem(undefined); }, [inactiveModal]); const onSelectAccountSelector = useCallback((item: AccountAddressItemType) => { - const targetChain = specificChain || selectedChain; + const targetTokenInfo = specificSelectedTokenInfo || selectedTokenInfo; - if (!targetChain) { + if (!targetTokenInfo) { return; } + const targetChain = targetTokenInfo.chainSlug; + const chainInfo = chainInfoMap[targetChain]; + if (!chainInfo) { + return; + } + + const isBitcoinAccountItem = [...BitcoinMainnetKeypairTypes, ...BitcoinTestnetKeypairTypes].includes(item.accountType); + + if (isBitcoinAccountItem) { + const targetAccountProxy = accountProxies.find((ap) => ap.id === item.accountProxyId); + + if (!targetAccountProxy) { + return; + } + + const accountTokenAddressList = getBitcoinAccounts(targetChain, targetTokenInfo.tokenSlug, chainInfo, targetAccountProxy.accounts); + + if (accountTokenAddressList.length > 1) { + openAccountTokenAddressModal(accountTokenAddressList, onCloseAccountSelector); + } else { + openAddressQrModal(item.address, item.accountType, item.accountProxyId, targetChain, onCloseAccountSelector); + } + + return; + } + setSelectedAccountAddressItem(item); const isPolkadotUnifiedChain = checkIsPolkadotUnifiedChain(targetChain); @@ -269,7 +328,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo } else { openAddressQrModal(item.address, item.accountType, item.accountProxyId, targetChain, onCloseAccountSelector); } - }, [chainInfoMap, checkIsPolkadotUnifiedChain, onCloseAccountSelector, openAddressFormatModal, openAddressQrModal, selectedChain, specificChain]); + }, [accountProxies, chainInfoMap, checkIsPolkadotUnifiedChain, getBitcoinAccounts, onCloseAccountSelector, openAccountTokenAddressModal, openAddressFormatModal, openAddressQrModal, selectedTokenInfo, specificSelectedTokenInfo]); /* account Selector --- */ @@ -316,8 +375,8 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo } }; - if (specificChain) { - if (!chainSupported.includes(specificChain)) { + if (specificSelectedTokenInfo) { + if (!chainSupported.includes(specificSelectedTokenInfo.chainSlug)) { console.warn('tokenGroupSlug does not work with current account'); return; @@ -332,14 +391,17 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo // current account is not All, just do show QR logic - handleShowQrModal(specificChain); + handleShowQrModal(specificSelectedTokenInfo.chainSlug); return; } if (tokenSelectorItems.length === 1 && tokenGroupSlug) { if (isAllAccount) { - setSelectedChain(tokenSelectorItems[0].originChain); + setSelectedTokenInfo({ + tokenSlug: tokenSelectorItems[0].slug, + chainSlug: tokenSelectorItems[0].originChain + }); activeModal(accountSelectorModalId); return; @@ -351,7 +413,7 @@ export default function useCoreReceiveModalHelper (tokenGroupSlug?: string): Hoo } activeModal(tokenSelectorModalId); - }, [activeModal, chainInfoMap, chainSupported, checkIsPolkadotUnifiedChain, currentAccountProxy, getReformatAddress, isAllAccount, openAddressFormatModal, openAddressQrModal, specificChain, tokenGroupSlug, tokenSelectorItems]); + }, [activeModal, chainInfoMap, chainSupported, checkIsPolkadotUnifiedChain, currentAccountProxy, getReformatAddress, isAllAccount, openAddressFormatModal, openAddressQrModal, specificSelectedTokenInfo, tokenGroupSlug, tokenSelectorItems]); useEffect(() => { if (addressQrModal.checkActive() && selectedAccountAddressItem) { diff --git a/packages/extension-koni-ui/src/utils/account/account.ts b/packages/extension-koni-ui/src/utils/account/account.ts index 62958c45c26..98d402efba2 100644 --- a/packages/extension-koni-ui/src/utils/account/account.ts +++ b/packages/extension-koni-ui/src/utils/account/account.ts @@ -14,7 +14,7 @@ import { AccountAddressType, AccountSignMode, AccountType, BitcoinAccountInfo } import { getNetworkKeyByGenesisHash } from '@subwallet/extension-koni-ui/utils/chain/getNetworkJsonByGenesisHash'; import { AccountInfoByNetwork } from '@subwallet/extension-koni-ui/utils/types'; import { isAddress, isCardanoAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; -import { BitcoinTestnetKeypairTypes, KeypairType } from '@subwallet/keyring/types'; +import { KeypairType } from '@subwallet/keyring/types'; import { Web3LogoMap } from '@subwallet/react-ui/es/config-provider/context'; import { decodeAddress, encodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; @@ -236,14 +236,6 @@ export function getReformatedAddressRelatedToChain (accountJson: AccountJson, ch } else if (accountJson.chainType === AccountChainType.CARDANO && chainInfo.cardanoInfo) { return reformatAddress(accountJson.address, chainInfo.isTestnet ? 0 : 1); } else if (accountJson.chainType === AccountChainType.BITCOIN && chainInfo.bitcoinInfo) { - const isTestnet = chainInfo.isTestnet; - const isBitcoinTestnet = BitcoinTestnetKeypairTypes.includes(accountJson.type); - - // Both must be testnet or both must be mainnet - if (isTestnet !== isBitcoinTestnet) { - return undefined; - } - return accountJson.address; } From f682c88c75bd77d1da5602a7055bb1b074c6dc72 Mon Sep 17 00:00:00 2001 From: lw Date: Mon, 9 Jun 2025 19:09:59 +0700 Subject: [PATCH 149/178] [Issue-4425] refactor: Update auto-select account logic in history screen --- .../src/Popup/Home/History/index.tsx | 1 + .../Field/AccountAddressSelector.tsx | 8 +++-- .../Modal/Selector/AccountSelector.tsx | 31 ++++++++++++++++++- .../src/hooks/history/useHistorySelection.tsx | 19 +++--------- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx index fa8d0563c9a..538e68dbec4 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx @@ -539,6 +539,7 @@ function Component ({ className = '' }: Props): React.ReactElement { { (isAllAccount || accountAddressItems.length > 1) && ( ): React.ReactElement => { - const { className = '', disabled, id = 'account-selector', items - , label, labelStyle, placeholder - , readOnly, statusHelp, tooltip, value } = props; + const { autoSelectFirstItem, className = '', disabled, id = 'account-selector' + , items, label, labelStyle + , placeholder, readOnly, statusHelp, tooltip, value } = props; const { t } = useTranslation(); const { onSelect } = useSelectModalInputHelper(props, ref); @@ -102,6 +103,7 @@ const Component = (props: Props, ref: ForwardedRef): React.ReactElemen />
; @@ -37,7 +38,7 @@ function isAccountAddressItem (item: ListItem): item is AccountAddressItemType { return 'address' in item && 'accountProxyId' in item && 'accountName' in item && !('groupLabel' in item); } -function Component ({ className = '', items, modalId, onBack, onCancel, onSelectItem, selectedValue }: Props): React.ReactElement { +function Component ({ autoSelectFirstItem, className = '', items, modalId, onBack, onCancel, onSelectItem, selectedValue }: Props): React.ReactElement { const { t } = useTranslation(); const { checkActive } = useContext(ModalContext); @@ -191,6 +192,34 @@ function Component ({ className = '', items, modalId, onBack, onCancel, onSelect } }, [isActive]); + useEffect(() => { + const doFunction = () => { + if (!listItems.length) { + return; + } + + const firstItem = listItems.find((i) => isAccountAddressItem(i)) as AccountAddressItemType | undefined; + + if (!firstItem) { + return; + } + + if (!selectedValue) { + onSelectItem?.(firstItem); + + return; + } + + if (!listItems.some((i) => isAccountAddressItem(i) && i.address === selectedValue)) { + onSelectItem?.(firstItem); + } + }; + + if (autoSelectFirstItem) { + doFunction(); + } + }, [autoSelectFirstItem, listItems, onSelectItem, selectedValue]); + return ( { - setSelectedAddress((prevResult) => { - if (accountAddressItems.length) { - if (!prevResult) { - return accountAddressItems[0].address; - } - - if (!accountAddressItems.some((a) => a.address === prevResult)) { - return accountAddressItems[0].address; - } - } - - return prevResult; - }); - }, [accountAddressItems, propAddress]); + // NOTE: This hook doesn't handle selected address manually, + // because it's now controlled via the `autoSelectFirstItem` prop + // in the AccountAddressSelector component. This is the best approach for now; + // can be revised if a better solution arises in the future. return { chainItems, From 6cbeeb382bd660019fc015e646db3a46cc0a8d20 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 9 Jun 2025 19:10:59 +0700 Subject: [PATCH 150/178] [Issue-4263] add: sort history tx follow mempool --- packages/extension-base/src/background/KoniTypes.ts | 1 + .../src/services/history-service/index.ts | 11 ++++++++--- .../src/Popup/Home/History/index.tsx | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index e9d82c20056..282da74f605 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -765,6 +765,7 @@ export interface TransactionHistoryItem { - return parseBitcoinTransferData(address, i, chainInfo); + const parsedItems = transferItems.map((item, index) => { + const parsedItem = parseBitcoinTransferData(address, item, chainInfo); + + return { ...parsedItem, apiTxIndex: index }; }); - await this.addHistoryItems(parsedItems); + allParsedItems.push(...parsedItems); } + + await this.addHistoryItems(allParsedItems); } subscribeHistories (chain: string, proxyId: string, cb: (items: TransactionHistoryItem[]) => void) { diff --git a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx index fa8d0563c9a..5f2ff4cfbec 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx @@ -398,8 +398,10 @@ function Component ({ className = '' }: Props): React.ReactElement { return -1; } else if (PROCESSING_STATUSES.includes(b.status) && !PROCESSING_STATUSES.includes(a.status)) { return 1; - } else { + } else if (b.displayTime !== a.displayTime) { return b.displayTime - a.displayTime; + } else { + return a.txIndex - b.txIndex; } }) .slice(0, count); From 10d359f80ab2dd0ff5f9b7bb163d536f84cbd682 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 10 Jun 2025 09:18:38 +0700 Subject: [PATCH 151/178] [Issue-4263] fix eslint --- packages/extension-base/src/background/KoniTypes.ts | 2 +- packages/extension-koni-ui/src/Popup/Home/History/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 282da74f605..df04fd9cb7d 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -765,7 +765,7 @@ export interface TransactionHistoryItem { } else if (b.displayTime !== a.displayTime) { return b.displayTime - a.displayTime; } else { - return a.txIndex - b.txIndex; + return (a.apiTxIndex ?? 0) - (b.apiTxIndex ?? 0); } }) .slice(0, count); From d5e2cba43f37e80a86e5599698de27021da20f1c Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 10 Jun 2025 11:39:34 +0700 Subject: [PATCH 152/178] [Issue-4263] update add index to history bitcoin transaction --- .../extension-base/src/services/history-service/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/extension-base/src/services/history-service/index.ts b/packages/extension-base/src/services/history-service/index.ts index fb75cca5a0d..0ae29b48aca 100644 --- a/packages/extension-base/src/services/history-service/index.ts +++ b/packages/extension-base/src/services/history-service/index.ts @@ -192,12 +192,15 @@ export class HistoryService implements StoppableServiceInterface, PersistDataSer const allParsedItems: TransactionHistoryItem[] = []; for (const address of addresses) { + const existingItems = await this.dbService.getHistories({ address, chain }); + const maxIndex = existingItems.length > 0 ? Math.max(...existingItems.map((item) => item.apiTxIndex || -1)) : -1; + const transferItems = await bitcoinApi.api.getAddressTransaction(address); const parsedItems = transferItems.map((item, index) => { const parsedItem = parseBitcoinTransferData(address, item, chainInfo); - return { ...parsedItem, apiTxIndex: index }; + return { ...parsedItem, apiTxIndex: maxIndex + index + 1 }; }); allParsedItems.push(...parsedItems); From 2fa76b44a7997ea7e4f6f5286aa3405a85ebb109 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 10 Jun 2025 16:18:07 +0700 Subject: [PATCH 153/178] [Issue-4263] adapt code after merging branch `4428` --- .../blockstream-testnet-strategy.ts | 45 +++++++++++++------ .../mempool-testnet-strategy.ts | 45 +++++++++++++------ 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts index 9eecc8c3669..fd200297f9f 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts @@ -24,14 +24,6 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im this.baseUrl = url; this.isTestnet = url.includes('testnet'); - - this.getBlockTime() - .then((rs) => { - this.timePerBlock = rs; - }) - .catch(() => { - this.timePerBlock = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; - }); } private headers = { @@ -63,6 +55,31 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im }, 0); } + async computeBlockTime (): Promise { + let blockTime = this.timePerBlock; + + if (blockTime > 0) { + return blockTime; + } + + try { + blockTime = await this.getBlockTime(); + + this.timePerBlock = blockTime; + } catch (e) { + console.error('Failed to compute block time', e); + + blockTime = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; // Default to 10 minutes if failed + } + + // Cache block time in 60 seconds + setTimeout(() => { + this.timePerBlock = 0; + }, 60000); + + return blockTime; + } + getAddressSummaryInfo (address: string): Promise { return this.addRequest(async () => { const response = await getRequest(this.getUrl(`address/${address}`), undefined, this.headers); @@ -150,8 +167,10 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im } // TODO: NOTE: Currently not in use. Recheck the response if you want to use it. - getFeeRate (): Promise { - return this.addRequest(async (): Promise => { + async getFeeRate (): Promise { + const blockTime = await this.computeBlockTime(); + + return await this.addRequest(async (): Promise => { const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); const estimates = await response.json() as BlockStreamFeeEstimates; @@ -171,9 +190,9 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im type: 'bitcoin', busyNetwork: false, options: { - slow: { feeRate: convertFee(estimates[low] || 10), time: this.timePerBlock * low }, - average: { feeRate: convertFee(estimates[average || 12]), time: this.timePerBlock * average }, - fast: { feeRate: convertFee(estimates[fast] || 15), time: this.timePerBlock * fast }, + slow: { feeRate: convertFee(estimates[low] || 10), time: blockTime * low }, + average: { feeRate: convertFee(estimates[average || 12]), time: blockTime * average }, + fast: { feeRate: convertFee(estimates[fast] || 15), time: blockTime * fast }, default: 'slow' } }; diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts index 992f78120b9..1ecabd59b7c 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts @@ -24,14 +24,6 @@ export class MempoolTestnetRequestStrategy extends BaseApiRequestStrategy implem this.baseUrl = url; this.isTestnet = url.includes('testnet'); - - this.getBlockTime() - .then((rs) => { - this.timePerBlock = rs; - }) - .catch(() => { - this.timePerBlock = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; - }); } private headers = { @@ -63,6 +55,31 @@ export class MempoolTestnetRequestStrategy extends BaseApiRequestStrategy implem }, 0); } + async computeBlockTime (): Promise { + let blockTime = this.timePerBlock; + + if (blockTime > 0) { + return blockTime; + } + + try { + blockTime = await this.getBlockTime(); + + this.timePerBlock = blockTime; + } catch (e) { + console.error('Failed to compute block time', e); + + blockTime = (this.isTestnet ? 5 * 60 : 10 * 60) * 1000; // Default to 10 minutes if failed + } + + // Cache block time in 60 seconds + setTimeout(() => { + this.timePerBlock = 0; + }, 60000); + + return blockTime; + } + getAddressSummaryInfo (address: string): Promise { return this.addRequest(async () => { const response = await getRequest(this.getUrl(`address/${address}`), undefined, this.headers); @@ -150,8 +167,10 @@ export class MempoolTestnetRequestStrategy extends BaseApiRequestStrategy implem } // TODO: NOTE: Currently not in use. Recheck the response if you want to use it. - getFeeRate (): Promise { - return this.addRequest(async (): Promise => { + async getFeeRate (): Promise { + const blockTime = await this.computeBlockTime(); + + return await this.addRequest(async (): Promise => { const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); const estimates = await response.json() as BlockStreamFeeEstimates; @@ -171,9 +190,9 @@ export class MempoolTestnetRequestStrategy extends BaseApiRequestStrategy implem type: 'bitcoin', busyNetwork: false, options: { - slow: { feeRate: convertFee(estimates[low] || 10), time: this.timePerBlock * low }, - average: { feeRate: convertFee(estimates[average || 12]), time: this.timePerBlock * average }, - fast: { feeRate: convertFee(estimates[fast] || 15), time: this.timePerBlock * fast }, + slow: { feeRate: convertFee(estimates[low] || 10), time: blockTime * low }, + average: { feeRate: convertFee(estimates[average || 12]), time: blockTime * average }, + fast: { feeRate: convertFee(estimates[fast] || 15), time: blockTime * fast }, default: 'slow' } }; From 0fca16207039b59e969610e0f350064afb574437 Mon Sep 17 00:00:00 2001 From: AnhMTV Date: Mon, 9 Jun 2025 18:33:13 +0700 Subject: [PATCH 154/178] [Issue-4434] Update debounce on UI --- .../Popup/Transaction/variants/SendFund.tsx | 3 +- .../transaction/useWatchTransactionLazy.ts | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts 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 804e800e84f..6f8e175321a 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -25,6 +25,7 @@ import { ADDRESS_INPUT_AUTO_FORMAT_VALUE } from '@subwallet/extension-koni-ui/co import { MktCampaignModalContext } from '@subwallet/extension-koni-ui/contexts/MktCampaignModalContext'; import { useAlert, useCoreCreateGetChainSlugsByAccountProxy, useCoreCreateReformatAddress, useDefaultNavigate, useFetchChainAssetInfo, useGetAccountTokenBalance, useGetBalance, useHandleSubmitMultiTransaction, useIsPolkadotUnifiedChain, useNotification, usePreCheckAction, useRestoreTransaction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; import useGetConfirmationByScreen from '@subwallet/extension-koni-ui/hooks/campaign/useGetConfirmationByScreen'; +import useLazyWatchTransaction from '@subwallet/extension-koni-ui/hooks/transaction/useWatchTransactionLazy'; import { approveSpending, cancelSubscription, getOptimalTransferProcess, getTokensCanPayFee, isTonBounceableAddress, makeCrossChainTransfer, makeTransfer, subscribeMaxTransfer } from '@subwallet/extension-koni-ui/messaging'; import { CommonActionType, commonProcessReducer, DEFAULT_COMMON_PROCESS } from '@subwallet/extension-koni-ui/reducer'; import { RootState } from '@subwallet/extension-koni-ui/stores'; @@ -113,7 +114,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone const toValue = useWatchTransaction('to', form, defaultData); const destChainValue = useWatchTransaction('destChain', form, defaultData); - const transferAmountValue = useWatchTransaction('value', form, defaultData); + const transferAmountValue = useLazyWatchTransaction('value', form, defaultData, 600); const fromValue = useWatchTransaction('from', form, defaultData); const chainValue = useWatchTransaction('chain', form, defaultData); const assetValue = useWatchTransaction('asset', form, defaultData); diff --git a/packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts b/packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts new file mode 100644 index 00000000000..06d5a673840 --- /dev/null +++ b/packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts @@ -0,0 +1,32 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { TransactionFormBaseProps } from '@subwallet/extension-koni-ui/types'; +import { noop } from '@subwallet/extension-koni-ui/utils'; +import { Form, FormInstance } from '@subwallet/react-ui'; +import { useEffect, useState } from 'react'; +import { useIsFirstRender } from 'usehooks-ts'; + +const useLazyWatchTransaction = (key: K, form: FormInstance, defaultData: T, lazyTime = 300): T[K] => { + const isFirstRender = useIsFirstRender(); + const watch = Form.useWatch(key, form); + const [value, setValue] = useState(defaultData[key]); + + useEffect(() => { + if (isFirstRender) { + setValue(defaultData[key]); + + return noop; + } else { + const timer = setTimeout(() => { + setValue(watch); + }, lazyTime); + + return () => clearTimeout(timer); + } + }, [defaultData, isFirstRender, key, lazyTime, watch]); + + return value; +}; + +export default useLazyWatchTransaction; From 53ccdaf26ab8b33c1496d48a2a64a42e486ec4a0 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 10 Jun 2025 18:52:27 +0700 Subject: [PATCH 155/178] [Issue-4263] update sorting history --- .../extension-base/src/services/history-service/index.ts | 8 +++----- .../extension-koni-ui/src/Popup/Home/History/index.tsx | 8 ++++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/extension-base/src/services/history-service/index.ts b/packages/extension-base/src/services/history-service/index.ts index 0ae29b48aca..eb9d9bef883 100644 --- a/packages/extension-base/src/services/history-service/index.ts +++ b/packages/extension-base/src/services/history-service/index.ts @@ -180,6 +180,7 @@ export class HistoryService implements StoppableServiceInterface, PersistDataSer }); } + // Only 1 address is passed in private async fetchBitcoinTransactionHistory (chain: string, addresses: string[]) { const chainInfo = this.chainService.getChainInfoByKey(chain); const chainState = this.chainService.getChainStateByKey(chain); @@ -192,15 +193,12 @@ export class HistoryService implements StoppableServiceInterface, PersistDataSer const allParsedItems: TransactionHistoryItem[] = []; for (const address of addresses) { - const existingItems = await this.dbService.getHistories({ address, chain }); - const maxIndex = existingItems.length > 0 ? Math.max(...existingItems.map((item) => item.apiTxIndex || -1)) : -1; - const transferItems = await bitcoinApi.api.getAddressTransaction(address); const parsedItems = transferItems.map((item, index) => { const parsedItem = parseBitcoinTransferData(address, item, chainInfo); - return { ...parsedItem, apiTxIndex: maxIndex + index + 1 }; + return { ...parsedItem, apiTxIndex: index }; }); allParsedItems.push(...parsedItems); @@ -269,7 +267,7 @@ export class HistoryService implements StoppableServiceInterface, PersistDataSer (item_) => item_.extrinsicHash === item.extrinsicHash && item.chain === item_.chain && item.address === item_.address); if (needUpdateItem) { - updateRecords.push({ ...needUpdateItem, status: item.status }); + updateRecords.push({ ...needUpdateItem, status: item.status, apiTxIndex: item.apiTxIndex }); return; } diff --git a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx index ffdcd1cbf96..775e7649ba7 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx @@ -398,8 +398,10 @@ function Component ({ className = '' }: Props): React.ReactElement { return -1; } else if (PROCESSING_STATUSES.includes(b.status) && !PROCESSING_STATUSES.includes(a.status)) { return 1; - } else if (b.displayTime !== a.displayTime) { - return b.displayTime - a.displayTime; + } else if ((!!b.blockTime && !!a.blockTime) && (b.blockTime !== a.blockTime)) { + return b.blockTime - a.blockTime; + } else if ((!!b.time && !!a.time) && (b.time !== a.time)) { + return b.time - a.time; } else { return (a.apiTxIndex ?? 0) - (b.apiTxIndex ?? 0); } @@ -624,6 +626,8 @@ function Component ({ className = '' }: Props): React.ReactElement { setLoading(true); setCurrentItemDisplayCount(DEFAULT_ITEMS_COUNT); + console.log('selectedAddress', selectedAddress); + console.log('selectedChain', selectedChain); subscribeTransactionHistory( selectedChain, From 198f7a8d3437a24386513a918c66f227a18a36ab Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Wed, 11 Jun 2025 18:36:59 +0700 Subject: [PATCH 156/178] [Issue-4263] The data in the keyring is indeed x-only not need slice --- .../src/services/balance-service/transfer/bitcoin-transfer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts index 248f95b3b4e..742acab7874 100644 --- a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts +++ b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts @@ -81,7 +81,7 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P script: pair.bitcoin.output, value: input.value // UTXO value in satoshis }, - tapInternalKey: pair.bitcoin.internalPubkey.slice(1) // X-only public key (32 bytes) + tapInternalKey: pair.bitcoin.internalPubkey // X-only public key (32 bytes) }); } else { throw new Error(`Unsupported address type: ${addressInfo.type}`); @@ -109,6 +109,7 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P throw e; } + console.warn('Failed to create Bitcoin transaction:', e); throw new Error(`You don’t have enough BTC (${convertChainToSymbol(chain)}) for the transaction. Lower your BTC amount and try again`); } } From ba519ce6e66f8616998d990f77a82b3b1341be0d Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 12 Jun 2025 10:15:48 +0700 Subject: [PATCH 157/178] [Issue-4263] remove log --- .../services/balance-service/helpers/subscribe/bitcoin.ts | 2 -- .../services/balance-service/transfer/bitcoin-transfer.ts | 6 +----- .../BlockStreamTestnet/blockstream-testnet-strategy.ts | 2 -- .../strategy/BlockStreamTestnet/mempool-testnet-strategy.ts | 2 -- .../request-service/handler/BitcoinRequestHandler.ts | 5 ----- .../src/services/transaction-service/index.ts | 1 - packages/extension-koni-ui/src/Popup/Home/History/index.tsx | 2 -- 7 files changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts index ea53cbabf6e..8d32a050982 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/bitcoin.ts @@ -26,8 +26,6 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) bitcoinApi.api.getAddressSummaryInfo(address) ]); - console.log('addressSummaryInfo', addressSummaryInfo); - if (Number(addressSummaryInfo.balance) < 0) { return getDefaultBalanceResult(); } diff --git a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts index 742acab7874..2b9212b0899 100644 --- a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts +++ b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts @@ -42,7 +42,7 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P utxos }; - const { fee, inputs, outputs } = transferAll + const { inputs, outputs } = transferAll ? determineUtxosForSpendAll(determineUtxosArgs) : determineUtxosForSpend(determineUtxosArgs); @@ -99,10 +99,6 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P } } - console.log(inputs, inputs.reduce((v, i) => v + i.value, 0)); - console.log(outputs, (outputs as Array<{value: number}>).reduce((v, i) => v + i.value, 0)); - console.log(fee, bitcoinFee); - return [tx, transferAmount.toString()]; } catch (e) { if (e instanceof TransactionError) { diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts index fd200297f9f..37c3ba6f49d 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts @@ -174,8 +174,6 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); const estimates = await response.json() as BlockStreamFeeEstimates; - console.log('getRecommendedFeeRate: rs', estimates); - if (!response.ok) { throw new SWError('BlockStreamTestnetRequestStrategy.getFeeRate', 'Failed to fetch fee estimates'); } diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts index 1ecabd59b7c..4f69e56a83d 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts @@ -174,8 +174,6 @@ export class MempoolTestnetRequestStrategy extends BaseApiRequestStrategy implem const response = await getRequest(this.getUrl('fee-estimates'), undefined, this.headers); const estimates = await response.json() as BlockStreamFeeEstimates; - console.log('getRecommendedFeeRate: rs', estimates); - if (!response.ok) { throw new SWError('BlockStreamTestnetRequestStrategy.getFeeRate', 'Failed to fetch fee estimates'); } 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 8f09e3d06fd..b2caba24964 100644 --- a/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts +++ b/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts @@ -76,8 +76,6 @@ export default class BitcoinRequestHandler { const isInternal = isInternalRequest(url); if (['bitcoinSignatureRequest', 'bitcoinSendTransactionRequest', 'bitcoinSendTransactionRequestAfterConfirmation'].includes(type)) { - console.log('bitcoinSendTransactionRequest.type', type); - console.log('bitcoinSendTransactionRequest.payload', payload); const isAlwaysRequired = await this.#requestService.settingService.isAlwaysRequired; if (isAlwaysRequired) { @@ -419,9 +417,6 @@ export default class BitcoinRequestHandler { if (isApproved) { try { // Fill signature for some special type - console.log('request.type', type); - console.log('request.confirmation', confirmation); - console.log('request.result', result); await this.decorateResultBitcoin(type, confirmation, result); const error = validator && validator(result); diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index f2f1be19d92..2da22b489c1 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -2010,7 +2010,6 @@ export default class TransactionService { } else { this.state.requestService.addConfirmationBitcoin(id, url || EXTENSION_REQUEST_URL, 'bitcoinSendTransactionRequest', payload, {}) .then(({ isApproved, payload }) => { - console.log('Transaction isApproved:', isApproved); if (isApproved) { if (!payload) { diff --git a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx index 775e7649ba7..c3a1e1181be 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx @@ -626,8 +626,6 @@ function Component ({ className = '' }: Props): React.ReactElement { setLoading(true); setCurrentItemDisplayCount(DEFAULT_ITEMS_COUNT); - console.log('selectedAddress', selectedAddress); - console.log('selectedChain', selectedChain); subscribeTransactionHistory( selectedChain, From d1a4faa4fb9bb68f2c130a9f5ac95bb1189c77a6 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 12 Jun 2025 11:59:27 +0700 Subject: [PATCH 158/178] [Issue-4263] remove log (2) --- .../extension-base/src/services/transaction-service/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 2da22b489c1..808d73eb8f9 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -2010,7 +2010,6 @@ export default class TransactionService { } else { this.state.requestService.addConfirmationBitcoin(id, url || EXTENSION_REQUEST_URL, 'bitcoinSendTransactionRequest', payload, {}) .then(({ isApproved, payload }) => { - if (isApproved) { if (!payload) { throw new Error('Bad signature'); From 2cd1387b9e7eca26c3cdad69dc2d98c111c97186 Mon Sep 17 00:00:00 2001 From: AnhMTV Date: Thu, 12 Jun 2025 13:11:57 +0700 Subject: [PATCH 159/178] [Issue-4434] Improve debounce on UI --- .../transaction/useWatchTransactionLazy.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts b/packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts index 06d5a673840..07d5c3170c4 100644 --- a/packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts +++ b/packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts @@ -11,11 +11,31 @@ const useLazyWatchTransaction = (defaultData[key]); + const [isBlur, setIsBlur] = useState(false); + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const inputElement = form.getFieldInstance(key)?.input as HTMLInputElement; + + if (inputElement) { + inputElement.onfocus = () => { + setIsBlur(false); + }; + + inputElement.onblur = () => { + setIsBlur(true); + }; + } + }, [form, key]); useEffect(() => { if (isFirstRender) { setValue(defaultData[key]); + return noop; + } else if (isBlur) { + setValue(watch); + return noop; } else { const timer = setTimeout(() => { @@ -24,7 +44,7 @@ const useLazyWatchTransaction = clearTimeout(timer); } - }, [defaultData, isFirstRender, key, lazyTime, watch]); + }, [defaultData, isBlur, isFirstRender, key, lazyTime, watch]); return value; }; From 599f445ad4c3ec95f8e91dcbbc975a47153d7662 Mon Sep 17 00:00:00 2001 From: AnhMTV Date: Thu, 12 Jun 2025 14:31:07 +0700 Subject: [PATCH 160/178] [Issue-4434] Improve debounce on UI (fix eslint) --- .../src/hooks/transaction/useWatchTransactionLazy.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts b/packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts index 07d5c3170c4..1caef28c5a9 100644 --- a/packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts +++ b/packages/extension-koni-ui/src/hooks/transaction/useWatchTransactionLazy.ts @@ -4,6 +4,7 @@ import { TransactionFormBaseProps } from '@subwallet/extension-koni-ui/types'; import { noop } from '@subwallet/extension-koni-ui/utils'; import { Form, FormInstance } from '@subwallet/react-ui'; +import { NamePath } from '@subwallet/react-ui/es/form/interface'; import { useEffect, useState } from 'react'; import { useIsFirstRender } from 'usehooks-ts'; @@ -14,8 +15,10 @@ const useLazyWatchTransaction = { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const formObject = form.getFieldInstance(key as NamePath); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const inputElement = form.getFieldInstance(key)?.input as HTMLInputElement; + const inputElement = formObject?.input as HTMLInputElement; if (inputElement) { inputElement.onfocus = () => { From 00ddc9b1bd15985a018d2769c3616056db81bee2 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 12 Jun 2025 18:03:02 +0700 Subject: [PATCH 161/178] [Issue-4263] Fix case transfer changeOuput < dust limit --- .../src/koni/background/handlers/Extension.ts | 18 +++++++--- .../transfer/bitcoin-transfer.ts | 8 +++-- .../src/utils/bitcoin/utxo-management.ts | 33 +++++++++---------- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 14cd2092ff3..99105ed3b20 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -57,7 +57,7 @@ import { isProposalExpired, isSupportWalletConnectChain, isSupportWalletConnectN import { ResultApproveWalletConnectSession, WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; import { SWStorage } from '@subwallet/extension-base/storage'; import { AccountsStore } from '@subwallet/extension-base/stores'; -import { AccountJson, AccountProxyMap, AccountSignMode, AccountsWithCurrentAddress, BalanceJson, BasicTxErrorType, BasicTxWarningCode, BriefProcessStep, BuyServiceInfo, BuyTokenInfo, CommonOptimalTransferPath, CommonStepFeeInfo, CommonStepType, EarningProcessType, EarningRewardJson, EvmFeeInfo, FeeChainType, FeeInfo, HandleYieldStepData, NominationPoolInfo, OptimalYieldPathParams, ProcessStep, ProcessTransactionData, ProcessType, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeAllowOneSign, RequestChangeTonWalletContractVersion, RequestCheckPublicAndSecretKey, RequestClaimBridge, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestEarningSlippage, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetAmountForPair, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetTokensCanPayFee, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitProcessTransaction, RequestSubscribeProcessById, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, ResponseSubscribeProcessAlive, ResponseSubscribeProcessById, StakingTxErrorType, StepStatus, StorageDataInterface, SummaryEarningProcessData, SwapBaseTxData, SwapFeeType, SwapRequestV2, TokenSpendingApprovalParams, ValidateYieldProcessParams, YieldPoolType, YieldStepType, YieldTokenBaseInfo } from '@subwallet/extension-base/types'; +import { AccountJson, AccountProxyMap, AccountSignMode, AccountsWithCurrentAddress, BalanceJson, BasicTxErrorType, BasicTxWarningCode, BitcoinFeeRate, BriefProcessStep, BuyServiceInfo, BuyTokenInfo, CommonOptimalTransferPath, CommonStepFeeInfo, CommonStepType, EarningProcessType, EarningRewardJson, EvmFeeInfo, FeeChainType, FeeCustom, FeeInfo, HandleYieldStepData, NominationPoolInfo, OptimalYieldPathParams, ProcessStep, ProcessTransactionData, ProcessType, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeAllowOneSign, RequestChangeTonWalletContractVersion, RequestCheckPublicAndSecretKey, RequestClaimBridge, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestEarningSlippage, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetAmountForPair, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetTokensCanPayFee, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitProcessTransaction, RequestSubscribeProcessById, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, ResponseSubscribeProcessAlive, ResponseSubscribeProcessById, StakingTxErrorType, StepStatus, StorageDataInterface, SummaryEarningProcessData, SwapBaseTxData, SwapFeeType, SwapRequestV2, TokenSpendingApprovalParams, ValidateYieldProcessParams, YieldPoolType, YieldStepType, YieldTokenBaseInfo } from '@subwallet/extension-base/types'; import { RequestAccountProxyEdit, RequestAccountProxyForget } from '@subwallet/extension-base/types/account/action/edit'; import { RequestSubmitTransfer, RequestSubscribeTransfer, ResponseSubscribeTransfer } from '@subwallet/extension-base/types/balance/transfer'; import { GetNotificationParams, RequestIsClaimedPolygonBridge, RequestSwitchStatusParams } from '@subwallet/extension-base/types/notification'; @@ -1426,6 +1426,7 @@ export default class KoniExtension { const transferAmount: AmountData = { value: '0', symbol: _getAssetSymbol(transferTokenInfo), decimals: _getAssetDecimals(transferTokenInfo) }; let transaction: SWTransaction['transaction'] | null | undefined; + let overrideFeeCustom: FeeCustom | undefined; const transferTokenAvailable = await this.getAddressTransferableBalance({ address: from, networkKey: chain, token: tokenSlug, extrinsicType }); @@ -1510,8 +1511,9 @@ export default class KoniExtension { const txVal: string = transferAll ? transferTokenAvailable.value : (value || '0'); const bitcoinApi = this.#koniState.getBitcoinApi(chain); const feeInfo = await this.#koniState.feeService.subscribeChainFee(getId(), chain, 'bitcoin'); + let calculatedBitcoinFeeRate: string; - [transaction, transferAmount.value] = await createBitcoinTransaction({ bitcoinApi, + [transaction, recalBitcoinFee, transferAmount.value] = await createBitcoinTransaction({ bitcoinApi, chain, from, feeInfo, @@ -1521,6 +1523,14 @@ export default class KoniExtension { network: network }); // TODO: This is a hotfix until transferMax for Bitcoin is supported. + + const feeRate = parseFloat(calculatedBitcoinFeeRate); + + overrideFeeCustom = { feeRate } as BitcoinFeeRate; + + console.log('feeRate', feeRate); + console.log('recalBitcoinFee', recalBitcoinFee); + if (transferAll) { inputData.value = transferAmount.value; } @@ -1618,8 +1628,8 @@ export default class KoniExtension { warnings, address: from, chain, - feeCustom, - feeOption, + feeCustom: overrideFeeCustom || feeCustom, + feeOption: overrideFeeCustom ? 'custom' : feeOption, tokenPayFeeSlug, chainType, transferNativeAmount, diff --git a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts index 2b9212b0899..5b3399d6edc 100644 --- a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts +++ b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts @@ -23,7 +23,7 @@ export interface TransferBitcoinProps extends TransactionFee { network: Network } -export async function createBitcoinTransaction (params: TransferBitcoinProps): Promise<[Psbt, string]> { +export async function createBitcoinTransaction (params: TransferBitcoinProps): Promise<[Psbt, string, string]> { const { bitcoinApi, chain, feeCustom: _feeCustom, feeInfo: _feeInfo, feeOption, from, network, to, transferAll, value } = params; const feeCustom = _feeCustom as BitcoinFeeRate; @@ -42,7 +42,7 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P utxos }; - const { inputs, outputs } = transferAll + const { fee, inputs, outputs, size } = transferAll ? determineUtxosForSpendAll(determineUtxosArgs) : determineUtxosForSpend(determineUtxosArgs); @@ -99,7 +99,9 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P } } - return [tx, transferAmount.toString()]; + const customFeeRate = fee / size; + + return [tx, customFeeRate.toString(), transferAmount.toString()]; } catch (e) { if (e instanceof TransactionError) { throw e; diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index 0545c4a6007..b6643711cf8 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -192,26 +192,23 @@ export function determineUtxosForSpend ({ amount, outputs.push({ value: amountLeft.toNumber(), address: sender }); } else { // Todo: This solution for improve later, current throw error - - // console.warn( - // `Change output of ${amountLeft.toString()} satoshis is below dust limit (${dustLimit} satoshis for ${senderAddressInfo.type}). Omitting change output and adding to fee.` - // ); // // Increase the fee to use the remaining balance - // const newFee = sum.minus(amount).toNumber(); - // - // return { - // filteredUtxos, - // inputs: neededUtxos, - // outputs, - // size: sizeInfo.txVBytes, - // fee: newFee - // }; - - // const atLeastStr = formatNumber(dustLimit, 8, balanceFormatter, { maxNumberFormat: 8, minNumberFormat: 8 }); - // throw new TransactionError(TransferTxErrorType.NOT_ENOUGH_VALUE, `You must transfer at least ${atLeastStr} BTC`); - - // Do nothing with the remaining balance (amountLeft < dustLimit) console.warn(`Change output of ${amountLeft.toString()} satoshis is below dust limit (${dustLimit} satoshis for ${senderAddressInfo.type}). Omitting change output.`); + // + sizeInfo = getSizeInfo({ + inputLength: neededUtxos.length, + sender, + recipients: recipients.slice(-1) + }); + const newFee = sum.minus(amount).toNumber(); + + return { + filteredUtxos, + inputs: neededUtxos, + outputs, + size: sizeInfo.txVBytes, + fee: newFee + }; } return { From 5082b662b270a1ab73bbdeeebeb6cf86d9345a1e Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 12 Jun 2025 18:43:20 +0700 Subject: [PATCH 162/178] [Issue-4263] Fix case transfer changeOuput < dust limit (2) --- .../src/koni/background/handlers/Extension.ts | 19 ++++++++++--------- .../transfer/bitcoin-transfer.ts | 7 ++++--- .../src/utils/bitcoin/utxo-management.ts | 9 ++++++--- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 99105ed3b20..aa061e8f4b8 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -57,7 +57,7 @@ import { isProposalExpired, isSupportWalletConnectChain, isSupportWalletConnectN import { ResultApproveWalletConnectSession, WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; import { SWStorage } from '@subwallet/extension-base/storage'; import { AccountsStore } from '@subwallet/extension-base/stores'; -import { AccountJson, AccountProxyMap, AccountSignMode, AccountsWithCurrentAddress, BalanceJson, BasicTxErrorType, BasicTxWarningCode, BitcoinFeeRate, BriefProcessStep, BuyServiceInfo, BuyTokenInfo, CommonOptimalTransferPath, CommonStepFeeInfo, CommonStepType, EarningProcessType, EarningRewardJson, EvmFeeInfo, FeeChainType, FeeCustom, FeeInfo, HandleYieldStepData, NominationPoolInfo, OptimalYieldPathParams, ProcessStep, ProcessTransactionData, ProcessType, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeAllowOneSign, RequestChangeTonWalletContractVersion, RequestCheckPublicAndSecretKey, RequestClaimBridge, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestEarningSlippage, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetAmountForPair, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetTokensCanPayFee, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitProcessTransaction, RequestSubscribeProcessById, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, ResponseSubscribeProcessAlive, ResponseSubscribeProcessById, StakingTxErrorType, StepStatus, StorageDataInterface, SummaryEarningProcessData, SwapBaseTxData, SwapFeeType, SwapRequestV2, TokenSpendingApprovalParams, ValidateYieldProcessParams, YieldPoolType, YieldStepType, YieldTokenBaseInfo } from '@subwallet/extension-base/types'; +import { AccountJson, AccountProxyMap, AccountSignMode, AccountsWithCurrentAddress, BalanceJson, BasicTxErrorType, BasicTxWarningCode, BriefProcessStep, BuyServiceInfo, BuyTokenInfo, CommonOptimalTransferPath, CommonStepFeeInfo, CommonStepType, EarningProcessType, EarningRewardJson, EvmFeeInfo, FeeChainType, FeeCustom, FeeInfo, HandleYieldStepData, NominationPoolInfo, OptimalYieldPathParams, ProcessStep, ProcessTransactionData, ProcessType, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeAllowOneSign, RequestChangeTonWalletContractVersion, RequestCheckPublicAndSecretKey, RequestClaimBridge, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestEarningSlippage, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetAmountForPair, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetTokensCanPayFee, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitProcessTransaction, RequestSubscribeProcessById, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, ResponseSubscribeProcessAlive, ResponseSubscribeProcessById, StakingTxErrorType, StepStatus, StorageDataInterface, SummaryEarningProcessData, SwapBaseTxData, SwapFeeType, SwapRequestV2, TokenSpendingApprovalParams, ValidateYieldProcessParams, YieldPoolType, YieldStepType, YieldTokenBaseInfo } from '@subwallet/extension-base/types'; import { RequestAccountProxyEdit, RequestAccountProxyForget } from '@subwallet/extension-base/types/account/action/edit'; import { RequestSubmitTransfer, RequestSubscribeTransfer, ResponseSubscribeTransfer } from '@subwallet/extension-base/types/balance/transfer'; import { GetNotificationParams, RequestIsClaimedPolygonBridge, RequestSwitchStatusParams } from '@subwallet/extension-base/types/notification'; @@ -1511,9 +1511,9 @@ export default class KoniExtension { const txVal: string = transferAll ? transferTokenAvailable.value : (value || '0'); const bitcoinApi = this.#koniState.getBitcoinApi(chain); const feeInfo = await this.#koniState.feeService.subscribeChainFee(getId(), chain, 'bitcoin'); - let calculatedBitcoinFeeRate: string; + let calculatedBitcoinFeeRate: string | undefined; - [transaction, recalBitcoinFee, transferAmount.value] = await createBitcoinTransaction({ bitcoinApi, + [transaction, transferAmount.value, calculatedBitcoinFeeRate] = await createBitcoinTransaction({ bitcoinApi, chain, from, feeInfo, @@ -1522,15 +1522,16 @@ export default class KoniExtension { value: txVal, network: network }); - // TODO: This is a hotfix until transferMax for Bitcoin is supported. - - const feeRate = parseFloat(calculatedBitcoinFeeRate); - overrideFeeCustom = { feeRate } as BitcoinFeeRate; + if (calculatedBitcoinFeeRate) { + const feeRate = parseFloat(calculatedBitcoinFeeRate); - console.log('feeRate', feeRate); - console.log('recalBitcoinFee', recalBitcoinFee); + if (!isNaN(feeRate)) { + overrideFeeCustom = { feeRate }; + } + } + // TODO: This is a hotfix until transferMax for Bitcoin is supported. if (transferAll) { inputData.value = transferAmount.value; } diff --git a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts index 5b3399d6edc..bb540282bc3 100644 --- a/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts +++ b/packages/extension-base/src/services/balance-service/transfer/bitcoin-transfer.ts @@ -23,7 +23,7 @@ export interface TransferBitcoinProps extends TransactionFee { network: Network } -export async function createBitcoinTransaction (params: TransferBitcoinProps): Promise<[Psbt, string, string]> { +export async function createBitcoinTransaction (params: TransferBitcoinProps): Promise<[Psbt, string, string|undefined]> { const { bitcoinApi, chain, feeCustom: _feeCustom, feeInfo: _feeInfo, feeOption, from, network, to, transferAll, value } = params; const feeCustom = _feeCustom as BitcoinFeeRate; @@ -42,7 +42,7 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P utxos }; - const { fee, inputs, outputs, size } = transferAll + const { fee, inputs, isCustomFeeRate, outputs, size } = transferAll ? determineUtxosForSpendAll(determineUtxosArgs) : determineUtxosForSpend(determineUtxosArgs); @@ -100,8 +100,9 @@ export async function createBitcoinTransaction (params: TransferBitcoinProps): P } const customFeeRate = fee / size; + const customFeeRateResult = isCustomFeeRate ? customFeeRate.toString() : undefined; - return [tx, customFeeRate.toString(), transferAmount.toString()]; + return [tx, transferAmount.toString(), customFeeRateResult]; } catch (e) { if (e instanceof TransactionError) { throw e; diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index b6643711cf8..55bd20ca9a7 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -107,7 +107,8 @@ export function determineUtxosForSpendAll ({ feeRate, inputs: filteredUtxos, outputs, size: sizeInfo.txVBytes, - fee + fee, + isCustomFeeRate: false }; } @@ -207,7 +208,8 @@ export function determineUtxosForSpend ({ amount, inputs: neededUtxos, outputs, size: sizeInfo.txVBytes, - fee: newFee + fee: newFee, + isCustomFeeRate: true }; } @@ -216,7 +218,8 @@ export function determineUtxosForSpend ({ amount, inputs: neededUtxos, outputs, size: sizeInfo.txVBytes, - fee + fee, + isCustomFeeRate: false }; } From 0b5dfb323a46ae2c8966af952ea39a6c541fc80b Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 12 Jun 2025 19:14:08 +0700 Subject: [PATCH 163/178] [issue-4263] Increase feerate for bitcoin transactions to a minimum of 1.5. --- .../blockstream-testnet-strategy.ts | 12 ++++++++---- .../BlockStreamTestnet/mempool-testnet-strategy.ts | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts index 37c3ba6f49d..4e32b55d560 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/blockstream-testnet-strategy.ts @@ -209,9 +209,9 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im type: 'bitcoin', busyNetwork: false, options: { - slow: { feeRate: 1, time: convertTimeMilisec.hourFee }, - average: { feeRate: 1, time: convertTimeMilisec.halfHourFee }, - fast: { feeRate: 1, time: convertTimeMilisec.fastestFee }, + slow: { feeRate: 1.5, time: convertTimeMilisec.hourFee }, + average: { feeRate: 1.5, time: convertTimeMilisec.halfHourFee }, + fast: { feeRate: 1.5, time: convertTimeMilisec.fastestFee }, default: 'slow' } }; @@ -227,7 +227,11 @@ export class BlockStreamTestnetRequestStrategy extends BaseApiRequestStrategy im const estimates = await response.json() as BlockStreamFeeEstimates; - const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed(), 10); + const convertFee = (fee: number) => { + const adjustedFee = parseInt(new BigN(fee).toFixed(), 10); + + return Math.max(adjustedFee, 1.5); + }; return { type: 'bitcoin', diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts index 4f69e56a83d..7182a9f2eb7 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/strategy/BlockStreamTestnet/mempool-testnet-strategy.ts @@ -209,9 +209,9 @@ export class MempoolTestnetRequestStrategy extends BaseApiRequestStrategy implem type: 'bitcoin', busyNetwork: false, options: { - slow: { feeRate: 1, time: convertTimeMilisec.hourFee }, - average: { feeRate: 1, time: convertTimeMilisec.halfHourFee }, - fast: { feeRate: 1, time: convertTimeMilisec.fastestFee }, + slow: { feeRate: 1.5, time: convertTimeMilisec.hourFee }, + average: { feeRate: 1.5, time: convertTimeMilisec.halfHourFee }, + fast: { feeRate: 1.5, time: convertTimeMilisec.fastestFee }, default: 'slow' } }; @@ -227,7 +227,11 @@ export class MempoolTestnetRequestStrategy extends BaseApiRequestStrategy implem const estimates = await response.json() as RecommendedFeeEstimates; - const convertFee = (fee: number) => parseInt(new BigN(fee).toFixed(), 10); + const convertFee = (fee: number) => { + const adjustedFee = parseInt(new BigN(fee).toFixed(), 10); + + return Math.max(adjustedFee, 1.5); + }; return { type: 'bitcoin', From 840662c380ed5da9d5cc62c50aea9b6aaa6e0348 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 12 Jun 2025 19:21:55 +0700 Subject: [PATCH 164/178] [issue-4263] fix eslint --- .../extension-base/src/koni/background/handlers/Extension.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index aa061e8f4b8..6ae980336e5 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -1522,7 +1522,6 @@ export default class KoniExtension { value: txVal, network: network }); - if (calculatedBitcoinFeeRate) { const feeRate = parseFloat(calculatedBitcoinFeeRate); From b85a2a3e7de2b75f90f3abf3dee74b1aee6011e6 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 13 Jun 2025 10:02:32 +0700 Subject: [PATCH 165/178] [issue-4263] update sorting history --- packages/extension-koni-ui/src/Popup/Home/History/index.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx index c3a1e1181be..2402c023cf5 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/index.tsx @@ -398,10 +398,8 @@ function Component ({ className = '' }: Props): React.ReactElement { return -1; } else if (PROCESSING_STATUSES.includes(b.status) && !PROCESSING_STATUSES.includes(a.status)) { return 1; - } else if ((!!b.blockTime && !!a.blockTime) && (b.blockTime !== a.blockTime)) { - return b.blockTime - a.blockTime; - } else if ((!!b.time && !!a.time) && (b.time !== a.time)) { - return b.time - a.time; + } else if ((!!b.displayTime && !!a.displayTime) && (b.displayTime !== a.displayTime)) { + return b.displayTime - a.displayTime; } else { return (a.apiTxIndex ?? 0) - (b.apiTxIndex ?? 0); } From b4d1471dc8ad653eb80a005784195f0fd71b900c Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 16 Jun 2025 19:03:58 +0700 Subject: [PATCH 166/178] [Issue-4380] In case the change output < dust limit, only the recipient is included in the output. --- packages/extension-base/src/utils/bitcoin/utxo-management.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension-base/src/utils/bitcoin/utxo-management.ts b/packages/extension-base/src/utils/bitcoin/utxo-management.ts index 55bd20ca9a7..f93563f5a6c 100644 --- a/packages/extension-base/src/utils/bitcoin/utxo-management.ts +++ b/packages/extension-base/src/utils/bitcoin/utxo-management.ts @@ -199,7 +199,7 @@ export function determineUtxosForSpend ({ amount, sizeInfo = getSizeInfo({ inputLength: neededUtxos.length, sender, - recipients: recipients.slice(-1) + recipients: recipients.slice(0, 1) }); const newFee = sum.minus(amount).toNumber(); From 32198f6e0f1ad3870a46dcec606a31acfdf52094 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Tue, 17 Jun 2025 12:21:13 +0700 Subject: [PATCH 167/178] [Issue-4263] hide custom fee for Bitcoin --- .../src/Popup/Transaction/variants/SendFund.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6f8e175321a..4311de4dab1 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -93,7 +93,7 @@ const hiddenFields: Array = ['chain', 'fromAccountProxy', const alertModalId = 'confirmation-alert-modal'; const defaultAddressInputRenderKey = 'address-input-render-key'; -const FEE_SHOW_TYPES: Array = ['substrate', 'evm', 'bitcoin']; +const FEE_SHOW_TYPES: Array = ['substrate', 'evm']; const Component = ({ className = '', isAllAccount, targetAccountProxy }: ComponentProps): React.ReactElement => { useSetCurrentPage('/transaction/send-fund'); From d4cecf0ad87660101c0d869796c98eb62eff641f Mon Sep 17 00:00:00 2001 From: S2kael Date: Tue, 17 Jun 2025 16:35:28 +0700 Subject: [PATCH 168/178] [Issue-4263] Fix error when pay fee with custom token --- .../src/services/transaction-service/index.ts | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 808d73eb8f9..93712908097 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -50,10 +50,6 @@ import NotificationService from '../notification-service/NotificationService'; export default class TransactionService { private readonly state: KoniState; - private readonly eventService: EventService; - private readonly historyService: HistoryService; - private readonly notificationService: NotificationService; - private readonly chainService: ChainService; private readonly watchTransactionSubscribes: Record> = {}; @@ -70,10 +66,6 @@ export default class TransactionService { constructor (state: KoniState) { this.state = state; - this.eventService = state.eventService; - this.historyService = state.historyService; - this.notificationService = state.notificationService; - this.chainService = state.chainService; } private get allTransactions (): SWTransactionBase[] { @@ -160,7 +152,7 @@ export default class TransactionService { const nativeTokenInfo = this.state.chainService.getNativeTokenInfo(chain); const tokenPayFeeSlug = transactionInput.tokenPayFeeSlug; const isNonNativeTokenPayFee = tokenPayFeeSlug && !_isNativeTokenBySlug(tokenPayFeeSlug); - const nonNativeTokenPayFeeInfo = isNonNativeTokenPayFee ? this.chainService.getAssetBySlug(tokenPayFeeSlug) : undefined; + const nonNativeTokenPayFeeInfo = isNonNativeTokenPayFee ? this.state.chainService.getAssetBySlug(tokenPayFeeSlug) : undefined; const priceMap = (await this.state.priceService.getPrice()).priceMap; validationResponse.estimateFee = await estimateFeeForTransaction(validationResponse, transaction, chainInfo, evmApi, substrateApi, priceMap, feeInfo, nativeTokenInfo, nonNativeTokenPayFeeInfo, transactionInput.isTransferLocalTokenAndPayThatTokenAsFee); @@ -1247,16 +1239,16 @@ export default class TransactionService { if (transaction) { this.updateTransaction(id, { status: nextStatus, errors, extrinsicHash }); - this.historyService.updateHistoryByExtrinsicHash(transaction.extrinsicHash, { + this.state.historyService.updateHistoryByExtrinsicHash(transaction.extrinsicHash, { extrinsicHash: extrinsicHash || transaction.extrinsicHash, status: nextStatus, blockNumber: blockNumber || 0, blockHash: blockHash || '' }).catch(console.error); - const info = isHex(transaction?.extrinsicHash) ? transaction?.extrinsicHash : getBaseTransactionInfo(transaction, this.chainService.getChainInfoMap()); + const info = isHex(transaction?.extrinsicHash) ? transaction?.extrinsicHash : getBaseTransactionInfo(transaction, this.state.chainService.getChainInfoMap()); - this.notificationService.notify({ + this.state.notificationService.notify({ type: NotificationType.ERROR, title: t('Transaction timed out'), message: t('Transaction {{info}} timed out', { replace: { info } }), @@ -1265,7 +1257,7 @@ export default class TransactionService { }); } - this.eventService.emit('transaction.timeout', transaction); + this.state.eventService.emit('transaction.timeout', transaction); } public generateHashPayload (chain: string, transaction: TransactionConfig): HexString { @@ -1955,7 +1947,7 @@ export default class TransactionService { // Add start info emitter.emit('send', eventData); - const event = this.chainService.getBitcoinApi(chain).api.sendRawTransaction(payload); + const event = this.state.chainService.getBitcoinApi(chain).api.sendRawTransaction(payload); event.on('extrinsicHash', (txHash) => { eventData.extrinsicHash = txHash; From 1f30905ab9dba151cad5bd71559ac83a4a64d276 Mon Sep 17 00:00:00 2001 From: S2kael Date: Tue, 17 Jun 2025 16:41:54 +0700 Subject: [PATCH 169/178] [Issue-4263] Chore: fix eslint --- .../extension-base/src/services/transaction-service/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 93712908097..6c35117293a 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -10,10 +10,7 @@ import KoniState from '@subwallet/extension-base/koni/background/handlers/State' import { cellToBase64Str, externalMessage, getTransferCellPromise } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/utils'; import { CardanoTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/cardano-transfer'; import { TonTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; -import { ChainService } from '@subwallet/extension-base/services/chain-service'; import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getEvmChainId, _isChainEvmCompatible, _isNativeTokenBySlug } from '@subwallet/extension-base/services/chain-service/utils'; -import { EventService } from '@subwallet/extension-base/services/event-service'; -import { HistoryService } from '@subwallet/extension-base/services/history-service'; import { ClaimAvailBridgeNotificationMetadata } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; import { EXTENSION_REQUEST_URL } from '@subwallet/extension-base/services/request-service/constants'; import { TRANSACTION_TIMEOUT } from '@subwallet/extension-base/services/transaction-service/constants'; @@ -46,8 +43,6 @@ import { SignerPayloadJSON } from '@polkadot/types/types/extrinsic'; import { hexToU8a, isHex } from '@polkadot/util'; import { HexString } from '@polkadot/util/types'; -import NotificationService from '../notification-service/NotificationService'; - export default class TransactionService { private readonly state: KoniState; From cf2ddb9fc3277a1cdf38fc0afb395da69cd35e4e Mon Sep 17 00:00:00 2001 From: S2kael Date: Wed, 18 Jun 2025 17:17:12 +0700 Subject: [PATCH 170/178] [Issue-4263] Chore: update some logic on filter account and fetch history --- .../helpers/subscribe/index.ts | 12 +++++------ .../history-service/bitcoin-history.ts | 13 +++++++----- .../src/utils/account/common.ts | 21 +++++++++++-------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index 7ca6f17d69d..f1ca9b27f7f 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -42,16 +42,16 @@ export const getAccountJsonByAddress = (address: string): AccountJson | null => /** Filter addresses to subscribe by chain info */ const filterAddress = (addresses: string[], chainInfo: _ChainInfo): [string[], string[]] => { - const { bitcoin, cardano, evm, substrate, ton } = getAddressesByChainTypeMap(addresses, chainInfo); + const { bitcoin, cardano, evm, substrate, ton, _bitcoin } = getAddressesByChainTypeMap(addresses, chainInfo); if (_isChainEvmCompatible(chainInfo)) { - return [evm, [...bitcoin, ...substrate, ...ton, ...cardano]]; + return [evm, [bitcoin, substrate, ton, cardano, _bitcoin].flat()]; } else if (_isChainBitcoinCompatible(chainInfo)) { - return [bitcoin, [...evm, ...substrate, ...ton, ...cardano]]; + return [bitcoin, [evm, substrate, ton, cardano, _bitcoin].flat()]; } else if (_isChainTonCompatible(chainInfo)) { - return [ton, [...bitcoin, ...evm, ...substrate, ...cardano]]; + return [ton, [bitcoin, evm, substrate, cardano, _bitcoin].flat()]; } else if (_isChainCardanoCompatible(chainInfo)) { - return [cardano, [...bitcoin, ...evm, ...substrate, ...ton]]; + return [cardano, [bitcoin, evm, substrate, ton, _bitcoin].flat()]; } else { const fetchList: string[] = []; const unfetchList: string[] = []; @@ -81,7 +81,7 @@ const filterAddress = (addresses: string[], chainInfo: _ChainInfo): [string[], s } }); - return [fetchList, [...unfetchList, ...bitcoin, ...evm, ...ton, ...cardano]]; + return [fetchList, [unfetchList, bitcoin, evm, ton, cardano, _bitcoin].flat()]; } }; diff --git a/packages/extension-base/src/services/history-service/bitcoin-history.ts b/packages/extension-base/src/services/history-service/bitcoin-history.ts index 8a5817e4b04..8a1d0a1ef39 100644 --- a/packages/extension-base/src/services/history-service/bitcoin-history.ts +++ b/packages/extension-base/src/services/history-service/bitcoin-history.ts @@ -20,11 +20,14 @@ export function parseBitcoinTransferData (address: string, transferItem: Bitcoin const receiver = isCurrentAddressSender ? transferItem.vout[0]?.scriptpubkey_address || '' : address; const amountValue = (() => { - if (isCurrentAddressSender) { - return (transferItem.vout.find((i) => i.scriptpubkey_address === receiver))?.value || '0'; - } + const targetAddress = isCurrentAddressSender ? receiver : address; + const vouts = transferItem.vout.filter((i) => i.scriptpubkey_address === targetAddress); - return (transferItem.vout.find((i) => i.scriptpubkey_address === address))?.value || '0'; + if (vouts.length) { + return vouts.reduce((total, item) => total + item.value, 0).toString(); + } else { + return '0' + } })(); return { @@ -47,7 +50,7 @@ export function parseBitcoinTransferData (address: string, transferItem: Bitcoin blockNumber: transferItem.status.block_height || 0, blockHash: transferItem.status.block_hash || '', amount: { - value: `${amountValue}`, + value: amountValue, decimals: nativeDecimals, symbol: nativeSymbol }, diff --git a/packages/extension-base/src/utils/account/common.ts b/packages/extension-base/src/utils/account/common.ts index 6772b48a706..ff8b26af884 100644 --- a/packages/extension-base/src/utils/account/common.ts +++ b/packages/extension-base/src/utils/account/common.ts @@ -71,12 +71,12 @@ export const getAccountChainTypeForAddress = (address: string): AccountChainType return getAccountChainTypeFromKeypairType(type); }; -interface AddressesByChainType { - [ChainType.SUBSTRATE]: string[], - [ChainType.EVM]: string[], - [ChainType.BITCOIN]: string[], - [ChainType.TON]: string[], - [ChainType.CARDANO]: string[] +type AddressesByChainType = { + [key in ChainType]: string[] +} + +interface ExtendAddressesByChainType extends AddressesByChainType { + _bitcoin: string[]; } // TODO: Recheck the usage of this function for Bitcoin; it is currently applied to history. @@ -88,13 +88,14 @@ export function getAddressesByChainType (addresses: string[], chainTypes: ChainT }).flat(); // todo: recheck } -export function getAddressesByChainTypeMap (addresses: string[], chainInfo?: _ChainInfo): AddressesByChainType { - const addressByChainType: AddressesByChainType = { +export function getAddressesByChainTypeMap (addresses: string[], chainInfo?: _ChainInfo): ExtendAddressesByChainType { + const addressByChainType: ExtendAddressesByChainType = { substrate: [], evm: [], bitcoin: [], ton: [], - cardano: [] + cardano: [], + _bitcoin: [] }; addresses.forEach((address) => { @@ -110,6 +111,8 @@ export function getAddressesByChainTypeMap (addresses: string[], chainInfo?: _Ch if (isNetworkMatch) { addressByChainType.bitcoin.push(address); + } else { + addressByChainType._bitcoin.push(address); } } } else if (isCardanoAddress(address)) { From b3f3ca9b6cd2dc28fe6ec3b3ac40279e7811d596 Mon Sep 17 00:00:00 2001 From: S2kael Date: Wed, 18 Jun 2025 17:18:06 +0700 Subject: [PATCH 171/178] [Issue-4263] Chore: Refactor some interface --- .../extension-base/src/services/balance-service/index.ts | 2 +- .../chain-service/handler/bitcoin/BitcoinChainHandler.ts | 2 +- .../services/keyring-service/context/handlers/Mnemonic.ts | 8 ++++---- .../src/services/transaction-service/index.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index cf02e849270..8fad007dd46 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -445,7 +445,7 @@ export class BalanceService implements StoppableServiceInterface { }; } - async runSubscribeBalanceForAddress (address: string, chain: string, asset: string, extrinsicType?: ExtrinsicType) { + async refreshBalanceForAddress (address: string, chain: string, asset: string, extrinsicType?: ExtrinsicType) { // Check if address and chain are valid const chainInfoMap = this.state.chainService.getChainInfoMap(); diff --git a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts index 31b4473e797..85da1abadfd 100644 --- a/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts +++ b/packages/extension-base/src/services/chain-service/handler/bitcoin/BitcoinChainHandler.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { ChainService } from '@subwallet/extension-base/services/chain-service/index'; +import { ChainService } from '@subwallet/extension-base/services/chain-service'; import { AbstractChainHandler } from '../AbstractChainHandler'; import { _ApiOptions } from '../types'; diff --git a/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts b/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts index e2dc6e4beb7..68b2a47ae42 100644 --- a/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts +++ b/packages/extension-base/src/services/keyring-service/context/handlers/Mnemonic.ts @@ -4,7 +4,7 @@ import { CommonAccountErrorType, MnemonicType, RequestAccountCreateSuriV2, RequestExportAccountProxyMnemonic, RequestMnemonicCreateV2, RequestMnemonicValidateV2, ResponseAccountCreateSuriV2, ResponseExportAccountProxyMnemonic, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, SWCommonAccountError } from '@subwallet/extension-base/types'; import { createAccountProxyId, getSuri } from '@subwallet/extension-base/utils'; import { tonMnemonicGenerate } from '@subwallet/keyring'; -import { KeypairType, KeyringPair } from '@subwallet/keyring/types'; +import { BitcoinKeypairTypes, CardanoKeypairTypes, EthereumKeypairTypes, KeypairType, KeyringPair } from '@subwallet/keyring/types'; import { tonMnemonicValidate } from '@subwallet/keyring/utils'; import { keyring } from '@subwallet/ui-keyring'; import { t } from 'i18next'; @@ -27,7 +27,7 @@ export class AccountMnemonicHandler extends AccountBaseHandler { /* Create seed */ public async mnemonicCreateV2 ({ length = SEED_DEFAULT_LENGTH, mnemonic: _seed, type = 'general' }: RequestMnemonicCreateV2): Promise { - const types: KeypairType[] = type === 'general' ? ['sr25519', 'ethereum', 'ton', 'cardano', 'bitcoin-44', 'bitcoin-84', 'bitcoin-86', 'bittest-44', 'bittest-84', 'bittest-86'] : ['ton-native']; + const types: KeypairType[] = type === 'general' ? ['sr25519', ...EthereumKeypairTypes, 'ton', ...CardanoKeypairTypes, ...BitcoinKeypairTypes] : ['ton-native']; const seed = _seed || type === 'general' ? mnemonicGenerate(length) @@ -57,7 +57,7 @@ export class AccountMnemonicHandler extends AccountBaseHandler { assert(mnemonicValidate(phrase), t('Invalid seed phrase. Please try again.')); mnemonicTypes = 'general'; - pairTypes = ['sr25519', 'ethereum', 'ton', 'cardano', 'bitcoin-44', 'bitcoin-84', 'bitcoin-86', 'bittest-44', 'bittest-84', 'bittest-86']; + pairTypes = ['sr25519', ...EthereumKeypairTypes, 'ton', ...CardanoKeypairTypes, ...BitcoinKeypairTypes]; } catch (e) { assert(tonMnemonicValidate(phrase), t('Invalid seed phrase. Please try again.')); mnemonicTypes = 'ton'; @@ -89,7 +89,7 @@ export class AccountMnemonicHandler extends AccountBaseHandler { const addressDict = {} as Record; let changedAccount = false; const hasMasterPassword = keyring.keyring.hasMasterPassword; - const types: KeypairType[] = type ? [type] : ['sr25519', 'ethereum', 'ton', 'cardano', 'bitcoin-44', 'bitcoin-84', 'bitcoin-86', 'bittest-44', 'bittest-84', 'bittest-86']; + const types: KeypairType[] = type ? [type] : ['sr25519', ...EthereumKeypairTypes, 'ton', ...CardanoKeypairTypes, ...BitcoinKeypairTypes]; if (!hasMasterPassword) { if (!password) { diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 6c35117293a..b19b81d33ec 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -1117,7 +1117,7 @@ export default class TransactionService { try { const sender = keyring.getPair(inputData.from); - balanceService.runSubscribeBalanceForAddress(sender.address, transaction.chain, inputData.tokenSlug, transaction.extrinsicType) + balanceService.refreshBalanceForAddress(sender.address, transaction.chain, inputData.tokenSlug, transaction.extrinsicType) .catch((error) => console.error('Failed to run balance subscription:', error)); } catch (e) { console.error(e); @@ -1126,7 +1126,7 @@ export default class TransactionService { try { const recipient = keyring.getPair(inputData.to); - balanceService.runSubscribeBalanceForAddress(recipient.address, transaction.chain, inputData.tokenSlug, transaction.extrinsicType) + balanceService.refreshBalanceForAddress(recipient.address, transaction.chain, inputData.tokenSlug, transaction.extrinsicType) .catch((error) => console.error('Failed to run balance subscription:', error)); } catch (e) { console.error(e); From 2a3f148d229f1429afe67ae2feaa680f23bbf364 Mon Sep 17 00:00:00 2001 From: S2kael Date: Wed, 18 Jun 2025 17:31:17 +0700 Subject: [PATCH 172/178] [Issue-4263] Chore: fix eslint --- .../src/services/balance-service/helpers/subscribe/index.ts | 2 +- .../src/services/history-service/bitcoin-history.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts index f1ca9b27f7f..1721b508398 100644 --- a/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts +++ b/packages/extension-base/src/services/balance-service/helpers/subscribe/index.ts @@ -42,7 +42,7 @@ export const getAccountJsonByAddress = (address: string): AccountJson | null => /** Filter addresses to subscribe by chain info */ const filterAddress = (addresses: string[], chainInfo: _ChainInfo): [string[], string[]] => { - const { bitcoin, cardano, evm, substrate, ton, _bitcoin } = getAddressesByChainTypeMap(addresses, chainInfo); + const { _bitcoin, bitcoin, cardano, evm, substrate, ton } = getAddressesByChainTypeMap(addresses, chainInfo); if (_isChainEvmCompatible(chainInfo)) { return [evm, [bitcoin, substrate, ton, cardano, _bitcoin].flat()]; diff --git a/packages/extension-base/src/services/history-service/bitcoin-history.ts b/packages/extension-base/src/services/history-service/bitcoin-history.ts index 8a1d0a1ef39..26de897a954 100644 --- a/packages/extension-base/src/services/history-service/bitcoin-history.ts +++ b/packages/extension-base/src/services/history-service/bitcoin-history.ts @@ -26,7 +26,7 @@ export function parseBitcoinTransferData (address: string, transferItem: Bitcoin if (vouts.length) { return vouts.reduce((total, item) => total + item.value, 0).toString(); } else { - return '0' + return '0'; } })(); From a433f4bf491fa5fce8ffbda8d60fbbdec082a381 Mon Sep 17 00:00:00 2001 From: bluezdot <72647326+bluezdot@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:22:00 +0700 Subject: [PATCH 173/178] [Issue-4263] chores: migrate local flag `isAcknowledgedUnifiedAccountMigration` --- .../scripts/MigrateNewUnifiedAccount.ts | 23 +++++++++++++++++++ .../migration-service/scripts/index.ts | 4 +++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 packages/extension-base/src/services/migration-service/scripts/MigrateNewUnifiedAccount.ts diff --git a/packages/extension-base/src/services/migration-service/scripts/MigrateNewUnifiedAccount.ts b/packages/extension-base/src/services/migration-service/scripts/MigrateNewUnifiedAccount.ts new file mode 100644 index 00000000000..5e7a1777428 --- /dev/null +++ b/packages/extension-base/src/services/migration-service/scripts/MigrateNewUnifiedAccount.ts @@ -0,0 +1,23 @@ +// Copyright 2019-2022 @subwallet/extension-koni authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import BaseMigrationJob from '@subwallet/extension-base/services/migration-service/Base'; + +export default class MigrateNewUnifiedAccount extends BaseMigrationJob { + public override async run (): Promise { + try { + return new Promise((resolve) => { + this.state.settingService.getSettings((currentSettings) => { + this.state.settingService.setSettings({ + ...currentSettings, + isAcknowledgedUnifiedAccountMigration: false + }); + + resolve(); + }); + }); + } catch (e) { + console.error(e); + } + } +} diff --git a/packages/extension-base/src/services/migration-service/scripts/index.ts b/packages/extension-base/src/services/migration-service/scripts/index.ts index a2c217ebc86..c8d052b92fc 100644 --- a/packages/extension-base/src/services/migration-service/scripts/index.ts +++ b/packages/extension-base/src/services/migration-service/scripts/index.ts @@ -23,6 +23,7 @@ import EnableVaraChain from './EnableVaraChain'; import MigrateAuthUrls from './MigrateAuthUrls'; import MigrateImportedToken from './MigrateImportedToken'; import MigrateNetworkSettings from './MigrateNetworkSettings'; +import MigrateNewUnifiedAccount from './MigrateNewUnifiedAccount'; import MigrateTokenDecimals from './MigrateTokenDecimals'; import MigrateTransactionHistory from './MigrateTransactionHistory'; import MigrateTransactionHistoryBridge from './MigrateTransactionHistoryBridge'; @@ -65,7 +66,8 @@ export default >{ '1.3.6-01': MigrateTransactionHistoryBridge, '1.3.10-01': ClearMetadataDatabase, '1.3.26-01': DisableZeroBalanceTokens, - [MYTHOS_MIGRATION_KEY]: ClearMetadataForMythos + [MYTHOS_MIGRATION_KEY]: ClearMetadataForMythos, // [`${EVERYTIME}-1.1.42-02`]: MigrateTransactionHistoryBySymbol // [`${EVERYTIME}-1`]: AutoEnableChainsTokens + '1.3.42-01': MigrateNewUnifiedAccount }; From f8b1fe962ddea741ec8db2a29b64eb1f39773636 Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 19 Jun 2025 11:28:32 +0700 Subject: [PATCH 174/178] refactor: Update ChainDetail --- .../src/Popup/Settings/Chains/ChainDetail.tsx | 162 ++++++++++-------- 1 file changed, 95 insertions(+), 67 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Settings/Chains/ChainDetail.tsx b/packages/extension-koni-ui/src/Popup/Settings/Chains/ChainDetail.tsx index bf80c2c6523..e5e05721951 100644 --- a/packages/extension-koni-ui/src/Popup/Settings/Chains/ChainDetail.tsx +++ b/packages/extension-koni-ui/src/Popup/Settings/Chains/ChainDetail.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _NetworkUpsertParams } from '@subwallet/extension-base/services/chain-service/types'; -import { _getBlockExplorerFromChain, _getChainNativeTokenBasicInfo, _getChainSubstrateAddressPrefix, _getCrowdloanUrlFromChain, _getEvmChainId, _getSubstrateParaId, _isChainEvmCompatible, _isChainSubstrateCompatible, _isCustomChain, _isPureEvmChain, _isPureTonChain } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getBlockExplorerFromChain, _getChainNativeTokenBasicInfo, _getChainSubstrateAddressPrefix, _getCrowdloanUrlFromChain, _getEvmChainId, _getSubstrateParaId, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainSubstrateCompatible, _isChainTonCompatible, _isCustomChain, _isPureEvmChain, _isPureSubstrateChain } from '@subwallet/extension-base/services/chain-service/utils'; import { isUrl } from '@subwallet/extension-base/utils'; import { Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; import { ProviderSelector } from '@subwallet/extension-koni-ui/components/Field/ProviderSelector'; @@ -62,14 +62,6 @@ function Component ({ className = '' }: Props): React.ReactElement { const [chainInfo] = useState(_chainInfo); const [chainState] = useState(_chainState); - const isPureTonChain = useMemo(() => { - return chainInfo && _isPureTonChain(chainInfo); - }, [chainInfo]); - - const isPureEvmChain = useMemo(() => { - return chainInfo && _isPureEvmChain(chainInfo); - }, [chainInfo]); - const { decimals, symbol } = useMemo(() => { return _getChainNativeTokenBasicInfo(chainInfo); }, [chainInfo]); @@ -134,8 +126,16 @@ function Component ({ className = '' }: Props): React.ReactElement { types.push('EVM'); } - if (chainInfo.slug === 'ton') { - types.push('TON'); + if (_isChainTonCompatible(chainInfo)) { + types.push('Ton'); + } + + if (_isChainCardanoCompatible(chainInfo)) { + types.push('Cardano'); + } + + if (_isChainBitcoinCompatible(chainInfo)) { + types.push('Bitcoin'); } for (let i = 0; i < types.length; i++) { @@ -269,6 +269,33 @@ function Component ({ className = '' }: Props): React.ReactElement { }); }, [t]); + const { isAddressPrefixVisible, + isChainIdVisible, + isCrowdloanURLVisible, + isParaIdVisible } = useMemo(() => { + if (!chainInfo) { + return { + isParaIdVisible: false, + isChainIdVisible: false, + isAddressPrefixVisible: false, + isCrowdloanURLVisible: false + }; + } + + const isPureSubstrateChain = _isPureSubstrateChain(chainInfo); + const isPureEvmChain = _isPureEvmChain(chainInfo); + // const isPureTonChain = _isPureTonChain(chainInfo); + // const isPureCardanoChain = _isPureCardanoChain(chainInfo); + // const isPureBitcoinChain = _isPureBitcoinChain(chainInfo); + + return { + isParaIdVisible: isPureSubstrateChain, + isChainIdVisible: isPureEvmChain, + isAddressPrefixVisible: isPureSubstrateChain, + isCrowdloanURLVisible: isPureSubstrateChain + }; + }, [chainInfo]); + return ( { - - + + { { - !isPureTonChain && - - { - !isPureEvmChain - ? ( - -1 ? paraId : undefined} - placeholder={t('ParaId')} - tooltip={t('ParaId')} - tooltipPlacement={'topLeft'} - /> - ) - : ( - -1 ? chainId : 'None'} - placeholder={t('Chain ID')} - tooltip={t('Chain ID')} - tooltipPlacement={'topLeft'} - /> - ) - } - + isParaIdVisible && ( + + -1 ? paraId : undefined} + placeholder={t('ParaId')} + tooltip={t('ParaId')} + tooltipPlacement={'topLeft'} + /> + + ) } - { - isPureTonChain && - - - - } - - { - (!isPureEvmChain && !isPureTonChain) && - - - + isChainIdVisible && ( + + -1 ? chainId : 'None'} + placeholder={t('Chain ID')} + tooltip={t('Chain ID')} + tooltipPlacement={'topLeft'} + /> + + ) } { - !isPureTonChain && - - - + isAddressPrefixVisible && ( + + + + ) } + + + + { { - (!_isPureEvmChain(chainInfo) && !isPureTonChain) && (({ theme: { token } }: Props) => { marginLeft: token.margin }, + '.ant-field-wrapper.ant-field-wrapper': { + paddingLeft: token.paddingSM, + paddingRight: token.paddingSM + }, + + '.auto-sizing-col-container': { + flexWrap: 'wrap', + gap: token.sizeSM, + + '.ant-col': { + flexGrow: 1, + flexBasis: '35%' + } + }, + '.chain_detail__attributes_container': { display: 'flex', flexDirection: 'column', From 3b415bffcbb70f09e7f4a8aeb90201f918f92f79 Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 19 Jun 2025 11:47:32 +0700 Subject: [PATCH 175/178] refactor: Update content for SeedPhraseTermModal --- .../Modal/TermsAndConditions/SeedPhraseTermModal.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/extension-koni-ui/src/components/Modal/TermsAndConditions/SeedPhraseTermModal.tsx b/packages/extension-koni-ui/src/components/Modal/TermsAndConditions/SeedPhraseTermModal.tsx index b64f1f742ef..69cb17d1146 100644 --- a/packages/extension-koni-ui/src/components/Modal/TermsAndConditions/SeedPhraseTermModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/TermsAndConditions/SeedPhraseTermModal.tsx @@ -99,7 +99,9 @@ const Component = ({ className }: Props) => { }, [inactiveModal, isCheckDontShow, setConfirmTermSeedPhrase]); const subTitle = useMemo(() => { - return useDefaultContent ? t('Tap on all checkboxes to confirm you understand the importance of your seed phrase') : t('This seed phrase creates a unified account that can be used for Polkadot, Ethereum, Bitcoin and TON ecosystem. Keep in mind that for TON specifically, this seed phrase is not compatible with TON-native wallets.'); + return useDefaultContent + ? t('Tap on all checkboxes to confirm you understand the importance of your seed phrase') + : t('This seed phrase creates a unified account that can be used for Polkadot, Ethereum, TON, Cardano & Bitcoin ecosystems. Keep in mind that for TON specifically, this seed phrase is not compatible with TON-native wallets.'); }, [useDefaultContent, t]); return ( From 39c240223752e7838531ad7d75e71e7e15444307 Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 19 Jun 2025 15:26:58 +0700 Subject: [PATCH 176/178] refactor: Update message for swap submit preCheck --- .../Popup/Transaction/variants/Swap/index.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx index 3f193a5082a..98b9c0c8b34 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/Swap/index.tsx @@ -8,7 +8,7 @@ import { validateRecipientAddress } from '@subwallet/extension-base/core/logic-v import { ActionType } from '@subwallet/extension-base/core/types'; import { AcrossErrorMsg } from '@subwallet/extension-base/services/balance-service/transfer/xcm/acrossBridge'; import { _ChainState } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetDecimals, _getAssetOriginChain, _getMultiChainAsset, _getOriginChainOfAsset, _isAssetFungibleToken, _isChainEvmCompatible, _isChainInfoCompatibleWithAccountInfo, _parseAssetRefKey } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getAssetOriginChain, _getAssetSymbol, _getChainName, _getMultiChainAsset, _getOriginChainOfAsset, _isAssetFungibleToken, _isChainEvmCompatible, _isChainInfoCompatibleWithAccountInfo, _parseAssetRefKey } from '@subwallet/extension-base/services/chain-service/utils'; import { KyberSwapQuoteMetadata } from '@subwallet/extension-base/services/swap-service/handler/kyber-handler'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; import { AccountProxy, AccountProxyType, AnalyzedGroup, CommonOptimalSwapPath, ProcessType, SwapRequestResult, SwapRequestV2 } from '@subwallet/extension-base/types'; @@ -197,7 +197,23 @@ const Component = ({ targetAccountProxy }: ComponentProps) => { }, [availableBalanceHookResult.isLoading, availableBalanceHookResult.nativeTokenBalance, availableBalanceHookResult.nativeTokenSlug, availableBalanceHookResult.tokenBalance, fromTokenSlugValue]); const { checkChainConnected, turnOnChain } = useChainConnection(); - const onPreCheck = usePreCheckAction(fromValue); + + const preCheckMessage = useMemo(() => { + if (!fromTokenSlugValue) { + return undefined; + } + + const chainAsset = assetRegistryMap[fromTokenSlugValue]; + const chainSlug = _getAssetOriginChain(chainAsset); + const chainName = _getChainName(chainInfoMap[chainSlug]); + + return t('{{symbol}} on {{chainName}} is not supported for swapping. Select another token and try again', { replace: { + symbol: _getAssetSymbol(chainAsset), + chainName + } }); + }, [assetRegistryMap, chainInfoMap, fromTokenSlugValue, t]); + + const onPreCheck = usePreCheckAction(fromValue, undefined, preCheckMessage); const oneSign = useOneSignProcess(fromValue); const getReformatAddress = useCoreCreateReformatAddress(); const getChainSlugsByAccountProxy = useCoreCreateGetChainSlugsByAccountProxy(); From 53d715615982eb0fc7931e23d93d05cef31142ff Mon Sep 17 00:00:00 2001 From: lw Date: Fri, 20 Jun 2025 10:11:28 +0700 Subject: [PATCH 177/178] refactor: Update address display for history screen --- .../src/hooks/history/useHistorySelection.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx b/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx index 4f8157f1e58..6996648f968 100644 --- a/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx +++ b/packages/extension-koni-ui/src/hooks/history/useHistorySelection.tsx @@ -49,17 +49,15 @@ export default function useHistorySelection () { const updateResult = (ap: AccountProxy) => { ap.accounts.forEach((a) => { - // TODO: This is a temporary validation method. - // Find a more efficient way to get isValid. - const isValid = getReformatAddress(a, chainInfo); + const formatedAddress = getReformatAddress(a, chainInfo); - if (isValid) { + if (formatedAddress) { result.push({ accountName: ap.name, accountProxyId: ap.id, accountProxyType: ap.accountType, accountType: a.type, - address: a.address + address: formatedAddress }); } }); From 3b543e264faacac0446bae20af5d09484a164656 Mon Sep 17 00:00:00 2001 From: lw Date: Fri, 20 Jun 2025 15:41:42 +0700 Subject: [PATCH 178/178] refactor: Update prop displayAddress for type AccountAddressItemType and refactor code for AccountSelector --- .../parts/AccountSelector/index.tsx | 4 +- .../Modal/Selector/AccountSelector.tsx | 186 +++++++++--------- .../src/hooks/history/useHistorySelection.tsx | 3 +- .../extension-koni-ui/src/types/account.ts | 8 +- 4 files changed, 107 insertions(+), 94 deletions(-) diff --git a/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/index.tsx b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/index.tsx index 6a05a116988..da0ec7f74b3 100644 --- a/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/index.tsx +++ b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/AccountSelector/index.tsx @@ -52,7 +52,7 @@ function Component ({ className = '', items, modalId, onBack, onCancel, onSelect const lowerCaseSearchText = searchText.toLowerCase(); return item.accountName.toLowerCase().includes(lowerCaseSearchText) || - item.address.toLowerCase().includes(lowerCaseSearchText); + (item.displayAddress || item.address).toLowerCase().includes(lowerCaseSearchText); }, []); const onSelect = useCallback((item: AccountAddressItemType) => { @@ -75,7 +75,7 @@ function Component ({ className = '', items, modalId, onBack, onCancel, onSelect return ( void, @@ -50,7 +59,7 @@ function Component ({ autoSelectFirstItem, className = '', items, modalId, onBac const lowerCaseSearchText = searchText.toLowerCase(); return item.accountName.toLowerCase().includes(lowerCaseSearchText) || - item.address.toLowerCase().includes(lowerCaseSearchText); + (item.displayAddress || item.address).toLowerCase().includes(lowerCaseSearchText); }, []); const onSelect = useCallback((item: AccountAddressItemType) => { @@ -71,9 +80,13 @@ function Component ({ autoSelectFirstItem, className = '', items, modalId, onBac ); } + // NOTE: + // displayAddress is only for visual representation. + // The original address should always be used for identification, selection, comparison, and any logic-related operations. + return ( (() => { - const result: ListItem[] = []; - const masterAccounts: AccountAddressItemType[] = []; - const qrSignerAccounts: ListItem[] = []; - const watchOnlyAccounts: ListItem[] = []; - const ledgerAccounts: ListItem[] = []; - const injectedAccounts: ListItem[] = []; - const unknownAccounts: ListItem[] = []; - - items.forEach((item) => { - if (searchValue && !searchFunction(item, searchValue)) { - return; - } - - if (item.accountProxyType === AccountProxyType.SOLO || item.accountProxyType === AccountProxyType.UNIFIED) { - masterAccounts.push(item); - } else if (item.accountProxyType === AccountProxyType.QR) { - qrSignerAccounts.push(item); - } else if (item.accountProxyType === AccountProxyType.READ_ONLY) { - watchOnlyAccounts.push(item); - } else if (item.accountProxyType === AccountProxyType.LEDGER) { - ledgerAccounts.push(item); - } else if (item.accountProxyType === AccountProxyType.INJECTED) { - injectedAccounts.push(item); - } else if (item.accountProxyType === AccountProxyType.UNKNOWN) { - unknownAccounts.push(item); - } - }); - - if (masterAccounts.length) { - result.push(...masterAccounts); - } - - if (qrSignerAccounts.length) { - qrSignerAccounts.unshift({ - id: 'qr', - groupLabel: t('QR signer account') - }); - - result.push(...qrSignerAccounts); - } + const sortedItems = useMemo(() => { + return [...items].sort((a, b) => { + const _isABitcoin = isBitcoinAddress(a.address); + const _isBBitcoin = isBitcoinAddress(b.address); + const _isSameProxyId = a.accountProxyId === b.accountProxyId; - if (watchOnlyAccounts.length) { - watchOnlyAccounts.unshift({ - id: 'watch-only', - groupLabel: t('Watch-only account') - }); + if (_isABitcoin && _isBBitcoin && _isSameProxyId) { + const aDetails = getBitcoinAccountDetails(a.accountType); + const bDetails = getBitcoinAccountDetails(b.accountType); - result.push(...watchOnlyAccounts); - } - - if (ledgerAccounts.length) { - ledgerAccounts.unshift({ - id: 'ledger', - groupLabel: t('Ledger account') - }); - - result.push(...ledgerAccounts); - } - - if (injectedAccounts.length) { - injectedAccounts.unshift({ - id: 'injected', - groupLabel: t('Injected account') - }); + return aDetails.order - bDetails.order; + } - result.push(...ledgerAccounts); - } + return 0; + }); + }, [items]); + + const groupedItemMap = useMemo(() => { + const result: GroupedItems = { + master: [], + qrSigner: [], + watchOnly: [], + ledger: [], + injected: [], + unknown: [] + }; - if (unknownAccounts.length) { - unknownAccounts.unshift({ - id: 'unknown', - groupLabel: t('Unknown account') - }); + sortedItems.forEach((item) => { + switch (item.accountProxyType) { + case AccountProxyType.SOLO: + case AccountProxyType.UNIFIED: + result.master.push(item); + break; + case AccountProxyType.QR: + result.qrSigner.push(item); + break; + case AccountProxyType.READ_ONLY: + result.watchOnly.push(item); + break; + case AccountProxyType.LEDGER: + result.ledger.push(item); + break; + case AccountProxyType.INJECTED: + result.injected.push(item); + break; + default: + result.unknown.push(item); + } + }); - result.push(...unknownAccounts); - } + return result; + }, [sortedItems]); - return result.sort((a: ListItem, b: ListItem) => { - if (isAccountAddressItem(a) && isAccountAddressItem(b)) { - const _isABitcoin = isBitcoinAddress(a.address); - const _isBBitcoin = isBitcoinAddress(b.address); - const _isSameProxyId = a.accountProxyId === b.accountProxyId; + const listItems = useMemo(() => { + const result: ListItem[] = []; - if (_isABitcoin && _isBBitcoin && _isSameProxyId) { - const aDetails = getBitcoinAccountDetails(a.accountType); - const bDetails = getBitcoinAccountDetails(b.accountType); + const addGroup = (group: AccountAddressItemType[], label?: string, id?: string) => { + const filtered = group.filter((item) => + !searchValue || searchFunction(item, searchValue) + ); - return aDetails.order - bDetails.order; + if (filtered.length) { + if (label && id) { + result.push({ id, groupLabel: t(label) }); } + + result.push(...filtered); } + }; - return 0; - }); - }, [items, searchFunction, searchValue, t]); + addGroup(groupedItemMap.master); + addGroup(groupedItemMap.qrSigner, 'QR signer account', 'qr'); + addGroup(groupedItemMap.watchOnly, 'Watch-only account', 'watch-only'); + addGroup(groupedItemMap.ledger, 'Ledger account', 'ledger'); + addGroup(groupedItemMap.injected, 'Injected account', 'injected'); + addGroup(groupedItemMap.unknown, 'Unknown account', 'unknown'); + + return result; + }, [groupedItemMap, searchFunction, searchValue, t]); const handleSearch = useCallback((value: string) => { setSearchValue(value); @@ -194,11 +191,20 @@ function Component ({ autoSelectFirstItem, className = '', items, modalId, onBac useEffect(() => { const doFunction = () => { - if (!listItems.length) { + const _items = [ + ...groupedItemMap.master, + ...groupedItemMap.qrSigner, + ...groupedItemMap.watchOnly, + ...groupedItemMap.ledger, + ...groupedItemMap.injected, + ...groupedItemMap.unknown + ]; + + if (!_items.length) { return; } - const firstItem = listItems.find((i) => isAccountAddressItem(i)) as AccountAddressItemType | undefined; + const firstItem = _items[0]; if (!firstItem) { return; @@ -210,7 +216,7 @@ function Component ({ autoSelectFirstItem, className = '', items, modalId, onBac return; } - if (!listItems.some((i) => isAccountAddressItem(i) && i.address === selectedValue)) { + if (!_items.some((i) => isAccountAddressItem(i) && i.address === selectedValue)) { onSelectItem?.(firstItem); } }; @@ -218,7 +224,7 @@ function Component ({ autoSelectFirstItem, className = '', items, modalId, onBac if (autoSelectFirstItem) { doFunction(); } - }, [autoSelectFirstItem, listItems, onSelectItem, selectedValue]); + }, [autoSelectFirstItem, groupedItemMap, onSelectItem, selectedValue]); return (