diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 48e5ba99e7..c4e2779328 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -2232,6 +2232,12 @@ export interface RequestAccountProxyCreateSuri { suri: string; } +export type BitcoinBalanceMetadata = { + inscriptionCount: number +} + +export type _BalanceMetadata = unknown; + /* Campaign */ // Use stringify to communicate, pure boolean value will error with case 'false' value @@ -2278,6 +2284,7 @@ export interface KoniRequestSignatures { 'pri(assetSetting.update)': [AssetSettingUpdateReq, boolean]; // NFT functions + 'pri(inscription.loadMoreInscription)': [null, null] 'pri(evmNft.submitTransaction)': [NftTransactionRequest, SWTransactionResponse]; 'pri(evmNft.getTransaction)': [NftTransactionRequest, EvmNftTransaction]; 'pri(substrateNft.submitTransaction)': [NftTransactionRequest, SWTransactionResponse]; diff --git a/packages/extension-base/src/koni/api/nft/index.ts b/packages/extension-base/src/koni/api/nft/index.ts index 4acf9f3230..586b3e8ac3 100644 --- a/packages/extension-base/src/koni/api/nft/index.ts +++ b/packages/extension-base/src/koni/api/nft/index.ts @@ -48,12 +48,18 @@ function createSubstrateNftApi (chain: string, substrateApi: _SubstrateApi | nul } function createBitcoinInscriptionApi (chain: string, addresses: string[]) { - const filteredAddresses = addresses.filter((a) => { + const mainnetAddresses = addresses.filter((a) => { return getKeypairTypeByAddress(a) === 'bitcoin-86'; }); + const testnetAddresses = addresses.filter((a) => { + return getKeypairTypeByAddress(a) === 'bittest-86'; + }); + if (_NFT_CHAIN_GROUP.bitcoin.includes(chain)) { - return new InscriptionApi(chain, filteredAddresses); + return new InscriptionApi(chain, mainnetAddresses); + } else if (_NFT_CHAIN_GROUP.bitcoinTest.includes(chain)) { + return new InscriptionApi(chain, testnetAddresses); } else { return null; } @@ -75,7 +81,7 @@ const createOrdinalApi = (chain: string, subscanChain: string, addresses: string return new OrdinalNftApi(addresses, chain, subscanChain); }; -export class NftHandler { +export class NftService { // General settings chainInfoMap: Record = {}; addresses: string[] = []; @@ -213,4 +219,20 @@ export class NftHandler { }); })); } + + public async loadMoreNfts ( + updateItem: (chain: string, data: NftItem, owner: string) => void, + updateCollection: (chain: string, data: NftCollection) => void, + getOffset?: (address: string, chain: string) => Promise + ) { + const inscriptionApiHandlers = this.handlers.filter((handler) => handler instanceof InscriptionApi); + + await Promise.all(inscriptionApiHandlers.map(async (handler) => { + await handler.fetchNfts({ + updateItem, + updateCollection, + getOffset + }); + })); + } } diff --git a/packages/extension-base/src/koni/api/nft/inscription/index.ts b/packages/extension-base/src/koni/api/nft/inscription/index.ts index a078520642..24043551c3 100644 --- a/packages/extension-base/src/koni/api/nft/inscription/index.ts +++ b/packages/extension-base/src/koni/api/nft/inscription/index.ts @@ -17,21 +17,38 @@ interface FetchedData { results: InscriptionResponseItem[] } -const ORDINAL_COLLECTION_INFO: NftCollection = { +export const ORDINAL_COLLECTION_INFO: NftCollection = { chain: 'bitcoin', collectionId: 'INSCRIPTION', collectionName: 'Inscriptions' }; +export const ORDINAL_COLLECTION_INFO_TEST: NftCollection = { + chain: 'bitcoinTestnet', + collectionId: 'INSCRIPTION_TESTNET', + collectionName: 'Inscriptions Testnet' +}; + +const checkTestnet = (chain: string) => { + return chain === ORDINAL_COLLECTION_INFO_TEST.chain; +}; + export class InscriptionApi extends BaseNftApi { + private isTestnet: boolean; + constructor (chain: string, addresses: string[]) { super(chain, undefined, addresses); + this.isTestnet = checkTestnet(chain); } private createIframePreviewUrl (id: string) { return `https://ordinals.com/preview/${id}`; } + get collectionInfo () { + return this.isTestnet ? ORDINAL_COLLECTION_INFO_TEST : ORDINAL_COLLECTION_INFO; + } + private parseInsUrl (id: string, type: string) { if (type.startsWith('audio/') || type.startsWith('text/html') || type.startsWith('image/svg') || type.startsWith('video/') || type.startsWith('model/gltf')) { return this.createIframePreviewUrl(id); @@ -123,19 +140,54 @@ export class InscriptionApi extends BaseNftApi { return propertiesMap; } + public async updateTextInscription (inscriptions: Inscription[], params: HandleNftParams, collectionMap: Record ) { + await Promise.all(inscriptions.map(async (ins) => { + const content = await getInscriptionContent(this.isTestnet, ins.id); + const propertiesMap = this.handleProperties(ins); + + const parsedNft: NftItem = { + id: ins.id, + chain: this.chain, + owner: ins.address, + name: `#${ins.number.toString()}`, + image: this.parseInsUrl(ins.id, ins.content_type), + description: content ? JSON.stringify(content) : undefined, + collectionId: this.collectionInfo.collectionId, + rarity: ins.sat_rarity, + properties: propertiesMap + }; + + params.updateItem(this.chain, parsedNft, ins.address); + + if (!collectionMap[this.collectionInfo.collectionId]) { + const parsedCollection: NftCollection = { + collectionId: this.collectionInfo.collectionId, + chain: this.chain, + collectionName: this.collectionInfo.collectionName, + image: this.collectionInfo.image + }; + + collectionMap[this.collectionInfo.collectionId] = parsedCollection; + params.updateCollection(this.chain, parsedCollection); + } + })); + } + public async handleNfts (params: HandleNftParams) { try { await Promise.all(this.addresses.map(async (address) => { - const balances = await getAddressInscriptions(address); + const offset = params.getOffset && await params.getOffset(address, this.collectionInfo.chain); + const balances = await getAddressInscriptions(address, this.isTestnet, offset); if (balances.length > 0) { const collectionMap: Record = {}; + const textIns: Inscription[] = []; for (const ins of balances) { let content; if (ins.content_type.startsWith('text/plain') || ins.content_type.startsWith('application/json')) { - content = await getInscriptionContent(ins.id); + textIns.push(ins); } const propertiesMap = this.handleProperties(ins); @@ -147,25 +199,28 @@ export class InscriptionApi extends BaseNftApi { name: `#${ins.number.toString()}`, image: this.parseInsUrl(ins.id, ins.content_type), description: content ? JSON.stringify(content) : undefined, - collectionId: ORDINAL_COLLECTION_INFO.collectionId, + collectionId: this.collectionInfo.collectionId, rarity: ins.sat_rarity, properties: propertiesMap }; params.updateItem(this.chain, parsedNft, ins.address); - if (!collectionMap[ORDINAL_COLLECTION_INFO.collectionId]) { + if (!collectionMap[this.collectionInfo.collectionId]) { const parsedCollection: NftCollection = { - collectionId: ORDINAL_COLLECTION_INFO.collectionId, + collectionId: this.collectionInfo.collectionId, chain: this.chain, - collectionName: ORDINAL_COLLECTION_INFO.collectionName, - image: ORDINAL_COLLECTION_INFO.image + collectionName: this.collectionInfo.collectionName, + image: this.collectionInfo.image }; - collectionMap[ORDINAL_COLLECTION_INFO.collectionId] = parsedCollection; + collectionMap[this.collectionInfo.collectionId] = parsedCollection; params.updateCollection(this.chain, parsedCollection); } } + + // handle all inscriptions has text content + await this.updateTextInscription(textIns, params, collectionMap); } })); } catch (error) { diff --git a/packages/extension-base/src/koni/api/nft/nft.ts b/packages/extension-base/src/koni/api/nft/nft.ts index 396265a172..5e4ae6909c 100644 --- a/packages/extension-base/src/koni/api/nft/nft.ts +++ b/packages/extension-base/src/koni/api/nft/nft.ts @@ -7,7 +7,8 @@ import { baseParseIPFSUrl } from '@subwallet/extension-base/utils'; export interface HandleNftParams { updateItem: (chain: string, data: NftItem, owner: string) => void, - updateCollection: (chain: string, data: NftCollection) => void + updateCollection: (chain: string, data: NftCollection) => void, + getOffset?: (address: string, chain: string) => Promise } export abstract class BaseNftApi { diff --git a/packages/extension-base/src/koni/background/cron.ts b/packages/extension-base/src/koni/background/cron.ts index ea3b0f78f8..3be21ad687 100644 --- a/packages/extension-base/src/koni/background/cron.ts +++ b/packages/extension-base/src/koni/background/cron.ts @@ -195,7 +195,7 @@ export class KoniCron { }; checkNetworkAvailable = (serviceInfo: ServiceInfo): boolean => { - return Object.keys(serviceInfo.chainApiMap.substrate).length > 0 || Object.keys(serviceInfo.chainApiMap.evm).length > 0; + return Object.keys(serviceInfo.chainApiMap.substrate).length > 0 || Object.keys(serviceInfo.chainApiMap.evm).length > 0 || Object.keys(serviceInfo.chainApiMap.bitcoin).length > 0; }; public async reloadNft () { diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 9511e694a0..d7afff0207 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -2135,6 +2135,10 @@ export default class KoniExtension { }); } + private loadMoreInscription () { + this.#koniState.loadMoreInscription(); + } + private async evmNftSubmitTransaction (inputData: NftTransactionRequest): Promise { const { networkKey, params, recipientAddress, senderAddress } = inputData; const contractAddress = params.contractAddress as string; @@ -5373,6 +5377,10 @@ export default class KoniExtension { case 'pri(accounts.get.meta)': return this.getAccountMeta(request as RequestAccountMeta); + // Load more Inscriptions + case 'pri(inscription.loadMoreInscription)': + return this.loadMoreInscription(); + /// Send NFT case 'pri(evmNft.submitTransaction)': return this.evmNftSubmitTransaction(request as NftTransactionRequest); diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index ff065cb705..a668f0ba4c 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -8,6 +8,7 @@ import { isSubscriptionRunning, unsubscribe } from '@subwallet/extension-base/ba import { AccountRefMap, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, BasicTxErrorType, ChainStakingMetadata, ChainType, ConfirmationsQueue, CrowdloanItem, CrowdloanJson, CurrentAccountInfo, CurrentAccountProxyInfo, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestCheckPublicAndSecretKey, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCheckPublicAndSecretKey, ServiceInfo, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; import { AccountJson, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning } from '@subwallet/extension-base/background/types'; import { ALL_ACCOUNT_KEY, ALL_GENESIS_HASH, MANTA_PAY_BALANCE_INTERVAL } from '@subwallet/extension-base/constants'; +import { NftService } from '@subwallet/extension-base/koni/api/nft'; import { BalanceService } from '@subwallet/extension-base/services/balance-service'; import { ServiceStatus } from '@subwallet/extension-base/services/base/types'; import BuyService from '@subwallet/extension-base/services/buy-service'; @@ -136,6 +137,7 @@ export default class KoniState { readonly buyService: BuyService; readonly earningService: EarningService; readonly feeService: FeeService; + readonly nftService: NftService; // Handle the general status of the extension private generalStatus: ServiceStatus = ServiceStatus.INITIALIZING; @@ -166,6 +168,7 @@ export default class KoniState { this.transactionService = new TransactionService(this); this.earningService = new EarningService(this); this.feeService = new FeeService(this); + this.nftService = new NftService(); this.subscription = new KoniSubscription(this, this.dbService); this.cron = new KoniCron(this, this.subscription, this.dbService); @@ -498,6 +501,14 @@ export default class KoniState { this.dbService.updateNominatorMetadata(item).catch((e) => this.logger.warn(e)); } + public loadMoreInscription () { + this.nftService.loadMoreNfts( + (...args) => this.updateNftData(...args), + (...args) => this.setNftCollection(...args), + (address: string, chain: string) => this.dbService.getAddressTotalInscriptions([address], chain) + ).catch(this.logger.log); + } + public setNftCollection (network: string, data: NftCollection, callback?: (data: NftCollection) => void): void { this.dbService.addNftCollection(data).catch((e) => this.logger.warn(e)); callback && callback(data); @@ -1885,9 +1896,10 @@ export default class KoniState { } public async reloadNft () { - const currentAddress = this.keyringService.currentAccount.address; + const accountProxyId = this.keyringService.currentAccountProxy.proxyId; + const addresses = this.getAccountProxyAddresses(accountProxyId); - await this.dbService.removeNftsByAddress(currentAddress); + await this.dbService.removeNftsByAddress(addresses); return await this.cron.reloadNft(); } diff --git a/packages/extension-base/src/koni/background/subscription.ts b/packages/extension-base/src/koni/background/subscription.ts index aea4c35568..d8a0b2acba 100644 --- a/packages/extension-base/src/koni/background/subscription.ts +++ b/packages/extension-base/src/koni/background/subscription.ts @@ -3,7 +3,7 @@ import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; import { subscribeCrowdloan } from '@subwallet/extension-base/koni/api/dotsama/crowdloan'; -import { NftHandler } from '@subwallet/extension-base/koni/api/nft'; +import { NftService } from '@subwallet/extension-base/koni/api/nft'; import { _EvmApi, _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; import { COMMON_RELOAD_EVENTS, EventItem, EventType } from '@subwallet/extension-base/services/event-service/types'; import DatabaseService from '@subwallet/extension-base/services/storage-service/DatabaseService'; @@ -16,8 +16,6 @@ import KoniState from './handlers/State'; type SubscriptionName = 'balance' | 'crowdloan' | 'yieldPoolStats' | 'yieldPosition'; -const nftHandler = new NftHandler(); - export class KoniSubscription { private eventHandler?: (events: EventItem[], eventTypes: EventType[]) => void; private subscriptionMap: Record void) | undefined> = { @@ -30,11 +28,13 @@ export class KoniSubscription { public dbService: DatabaseService; private state: KoniState; private logger: Logger; + private nftService: NftService; constructor (state: KoniState, dbService: DatabaseService) { this.dbService = dbService; this.state = state; this.logger = createLogger('Subscription'); + this.nftService = this.state.nftService; } getSubscriptionMap () { @@ -142,12 +142,12 @@ export class KoniSubscription { } initNftSubscription (addresses: string[], substrateApiMap: Record, evmApiMap: Record, smartContractNfts: _ChainAsset[], chainInfoMap: Record) { - nftHandler.setChainInfoMap(chainInfoMap); - nftHandler.setDotSamaApiMap(substrateApiMap); - nftHandler.setWeb3ApiMap(evmApiMap); - nftHandler.setAddresses(addresses); + this.nftService.setChainInfoMap(chainInfoMap); + this.nftService.setDotSamaApiMap(substrateApiMap); + this.nftService.setWeb3ApiMap(evmApiMap); + this.nftService.setAddresses(addresses); - nftHandler.handleNfts( + this.nftService.handleNfts( smartContractNfts, (...args) => this.state.updateNftData(...args), (...args) => this.state.setNftCollection(...args) diff --git a/packages/extension-base/src/services/balance-service/helpers/balance/bitcoin.ts b/packages/extension-base/src/services/balance-service/helpers/balance/bitcoin.ts index f07a085e37..fa41356885 100644 --- a/packages/extension-base/src/services/balance-service/helpers/balance/bitcoin.ts +++ b/packages/extension-base/src/services/balance-service/helpers/balance/bitcoin.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; -import { APIItemState } from '@subwallet/extension-base/background/KoniTypes'; +import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { COMMON_REFRESH_BALANCE_INTERVAL } from '@subwallet/extension-base/constants'; import { Brc20BalanceItem } from '@subwallet/extension-base/services/chain-service/handler/bitcoin/strategy/BlockStream/types'; import { _BitcoinApi } from '@subwallet/extension-base/services/chain-service/types'; @@ -161,7 +161,14 @@ 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 = await getTransferableBitcoinUtxos(bitcoinApi, address); + const [filteredUtxos, addressSummaryInfo] = await Promise.all([ + getTransferableBitcoinUtxos(bitcoinApi, address), + bitcoinApi.api.getAddressSummaryInfo(address) + ]); + + const bitcoinBalanceMetadata = { + inscriptionCount: addressSummaryInfo.total_inscription + } as BitcoinBalanceMetadata; let balanceValue = new BigN(0); @@ -169,11 +176,19 @@ async function getBitcoinBalance (bitcoinApi: _BitcoinApi, addresses: string[]) balanceValue = balanceValue.plus(utxo.value); }); - return balanceValue.toString(); + return { + balance: balanceValue.toString(), + bitcoinBalanceMetadata: bitcoinBalanceMetadata + }; } catch (error) { console.log('Error while fetching Bitcoin balances', error); - return '0'; + return { + balance: '0', + bitcoinBalanceMetadata: { + inscriptionCount: 0 + } + }; } })); } @@ -184,13 +199,14 @@ export function subscribeBitcoinBalance (addresses: string[], chainInfo: _ChainI const getBalance = () => { getBitcoinBalance(bitcoinApi, addresses) .then((balances) => { - return balances.map((balance, index): BalanceItem => { + return balances.map(({ balance, bitcoinBalanceMetadata }, index): BalanceItem => { return { address: addresses[index], tokenSlug: nativeSlug, state: APIItemState.READY, free: balance, - locked: '0' + locked: '0', + metadata: bitcoinBalanceMetadata }; }); }) diff --git a/packages/extension-base/src/services/balance-service/helpers/group.ts b/packages/extension-base/src/services/balance-service/helpers/group.ts index bafe7a93da..e2d25f7a93 100644 --- a/packages/extension-base/src/services/balance-service/helpers/group.ts +++ b/packages/extension-base/src/services/balance-service/helpers/group.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { APIItemState } from '@subwallet/extension-base/background/KoniTypes'; +import { APIItemState, BitcoinBalanceMetadata } from '@subwallet/extension-base/background/KoniTypes'; import { BalanceItem } from '@subwallet/extension-base/types'; import { sumBN } from '@subwallet/extension-base/utils'; import BN from 'bn.js'; @@ -28,22 +28,15 @@ export const groupBalance = (items: BalanceItem[], address: string, token: strin : APIItemState.PENDING }; + let allInscription = 0; + for (const item of items) { - if (item.substrateInfo) { - if (!result.substrateInfo) { - result.substrateInfo = { ...item.substrateInfo }; - } else { - const old = { ...result.substrateInfo }; - const _new = { ...item.substrateInfo }; - - result.substrateInfo = { - reserved: new BN(old.reserved || '0').add(new BN(_new.reserved || '0')).toString(), - feeFrozen: new BN(old.feeFrozen || '0').add(new BN(_new.feeFrozen || '0')).toString(), - miscFrozen: new BN(old.miscFrozen || '0').add(new BN(_new.miscFrozen || '0')).toString() - }; - } - } + const inscriptionCount = (item.metadata as BitcoinBalanceMetadata)?.inscriptionCount; + + allInscription = inscriptionCount ? allInscription + inscriptionCount : allInscription; } + result.metadata = { inscriptionCount: allInscription } as BitcoinBalanceMetadata; + return result; }; diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 989e906551..b7561580ae 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -305,7 +305,7 @@ export class BalanceService implements StoppableServiceInterface { } addLazy('updateBalanceStore', () => { - const isAllAccount = isAccountAll(this.state.keyringService.currentAccount.address); + const isAllAccount = isAccountAll(this.state.keyringService.currentAccountProxy.proxyId); this.balanceMap.updateBalanceItems(this.balanceUpdateCache, isAllAccount); diff --git a/packages/extension-base/src/services/chain-service/constants.ts b/packages/extension-base/src/services/chain-service/constants.ts index 19b16ed932..ab751268ad 100644 --- a/packages/extension-base/src/services/chain-service/constants.ts +++ b/packages/extension-base/src/services/chain-service/constants.ts @@ -65,7 +65,8 @@ export const _NFT_CHAIN_GROUP = { unique_network: ['unique_network', 'quartz', 'opal'], bitcountry: ['bitcountry', 'pioneer', 'continuum_network'], vara: ['vara_network'], - bitcoin: ['bitcoin'] + bitcoin: ['bitcoin'], + bitcoinTest: ['bitcoinTestnet'] }; // Staking-------------------------------------------------------------------------------------------------------------- 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 e964350aea..a51a393f9e 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 @@ -32,7 +32,9 @@ export interface BitcoinAddressSummaryInfo { spent_txo_count: number, spent_txo_sum: number, tx_count: number - } + }, + balance: number, + total_inscription: number } // todo: combine RunesByAddressResponse & RunesCollectionInfoResponse 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 a46ecbe3e4..8a8eaa88ff 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, Inscription, 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/BlockStream/types'; import { HiroService } from '@subwallet/extension-base/services/hiro-service'; // todo: handle inscription testnet @@ -31,8 +31,8 @@ export async function getBrc20Metadata (ticker: string) { } } -export async function getInscriptionContent (inscriptionId: string) { - const hiroService = HiroService.getInstance(); +export async function getInscriptionContent (isTestnet: boolean, inscriptionId: string) { + const hiroService = HiroService.getInstance(isTestnet); try { return await hiroService.getInscriptionContent(inscriptionId); @@ -44,34 +44,19 @@ export async function getInscriptionContent (inscriptionId: string) { } // todo: handle large inscriptions -export async function getAddressInscriptions (address: string, isCompress = true) { - const inscriptionsFullList: Inscription[] = []; - const pageSize = 60; - let offset = 0; - - const hiroService = HiroService.getInstance(); +export async function getAddressInscriptions (address: string, isTestnet: boolean, offset = 0, limit = 25) { + const hiroService = HiroService.getInstance(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 && !(isCompress && offset >= 360)) { - inscriptionsFullList.push(...inscriptions); - offset += pageSize; - } else { - break; - } - } + const response = await hiroService.getAddressInscriptionsInfo({ + limit: String(limit), + offset: String(offset), + address: String(address) + }) as unknown as InscriptionFetchedData; - return inscriptionsFullList; + return response.results; } catch (error) { - console.error(`Failed to get ${address} inscriptions`, error); + console.error(`Failed to get ${address} inscriptions with offset ${offset} and limit ${limit}`, error); throw error; } } diff --git a/packages/extension-base/src/services/storage-service/DatabaseService.ts b/packages/extension-base/src/services/storage-service/DatabaseService.ts index a47b710f68..dc60c89338 100644 --- a/packages/extension-base/src/services/storage-service/DatabaseService.ts +++ b/packages/extension-base/src/services/storage-service/DatabaseService.ts @@ -214,6 +214,10 @@ export default class DatabaseService { return this.stores.nft.cleanUpNfts(chain, reformatAddress(owner, 42), collectionIds, nftIds); } + async getAddressTotalInscriptions (addresses: string[], chain: string) { + return this.stores.nft.getAddressTotalInscriptions(addresses, chain); + } + async getNft (addresses: string[], chainHashes?: string[]) { return this.stores.nft.getNft(addresses, chainHashes); } @@ -230,8 +234,8 @@ export default class DatabaseService { return this.stores.nft.removeNfts(chain, address, collectionId, nftIds); } - removeNftsByAddress (address: string) { - return this.stores.nft.removeNftsByAddress([address]); + removeNftsByAddress (addresses: string[]) { + return this.stores.nft.removeNftsByAddress(addresses); } // Chain diff --git a/packages/extension-base/src/services/storage-service/db-stores/Nft.ts b/packages/extension-base/src/services/storage-service/db-stores/Nft.ts index 696cfe9e10..a1feb7e580 100644 --- a/packages/extension-base/src/services/storage-service/db-stores/Nft.ts +++ b/packages/extension-base/src/services/storage-service/db-stores/Nft.ts @@ -2,12 +2,25 @@ // SPDX-License-Identifier: Apache-2.0 import { NftItem } from '@subwallet/extension-base/background/KoniTypes'; +import { ORDINAL_COLLECTION_INFO, ORDINAL_COLLECTION_INFO_TEST } from '@subwallet/extension-base/koni/api/nft/inscription'; import BaseStoreWithAddressAndChain from '@subwallet/extension-base/services/storage-service/db-stores/BaseStoreWithAddressAndChain'; import { liveQuery } from 'dexie'; import { INft } from '../databases'; export default class NftStore extends BaseStoreWithAddressAndChain { + getAddressTotalInscriptions (addresses: string[], chain: string) { + const collectionId = chain === 'bitcoin' ? ORDINAL_COLLECTION_INFO.collectionId : ORDINAL_COLLECTION_INFO_TEST.collectionId; + + if (addresses.length) { + return this.table + .where('address').anyOfIgnoreCase(addresses) + .and((item) => chain === item.chain && item.collectionId === collectionId).count(); + } + + return this.table.filter((item) => chain === item.chain).count(); + } + getNft (addresses: string[], chainList: string[] = []) { if (addresses.length) { return this.table.where('address').anyOfIgnoreCase(addresses).and((item) => chainList && chainList.includes(item.chain)).toArray(); diff --git a/packages/extension-base/src/types/balance/balance.ts b/packages/extension-base/src/types/balance/balance.ts index 1846b1c5bf..fffdb50d7d 100644 --- a/packages/extension-base/src/types/balance/balance.ts +++ b/packages/extension-base/src/types/balance/balance.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; -import { APIItemState } from '@subwallet/extension-base/background/KoniTypes'; +import { _BalanceMetadata, APIItemState } from '@subwallet/extension-base/background/KoniTypes'; import { _EvmApi } from '@subwallet/extension-base/services/chain-service/types'; import { ApiPromise } from '@polkadot/api'; @@ -43,6 +43,8 @@ export interface BalanceItem { // substrate fields substrateInfo?: SubstrateBalance; + + metadata?: _BalanceMetadata; } /** Balance info of all tokens on an address */ diff --git a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollectionDetail.tsx b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollectionDetail.tsx index b6f7bc550b..fe2ed03d36 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollectionDetail.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollectionDetail.tsx @@ -6,38 +6,93 @@ import { OrdinalRemarkData } from '@subwallet/extension-base/types'; import { EmptyList, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; import { SHOW_3D_MODELS_CHAIN } from '@subwallet/extension-koni-ui/constants'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; -import { useNavigateOnChangeAccount } from '@subwallet/extension-koni-ui/hooks'; +import { useGetNftByAccount, useNavigateOnChangeAccount } from '@subwallet/extension-koni-ui/hooks'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; +import { loadMoreInscription } from '@subwallet/extension-koni-ui/messaging'; import { InscriptionGalleryWrapper } from '@subwallet/extension-koni-ui/Popup/Home/Nfts/component/InscriptionGalleryWrapper'; import { NftGalleryWrapper } from '@subwallet/extension-koni-ui/Popup/Home/Nfts/component/NftGalleryWrapper'; -import { INftCollectionDetail, INftItemDetail } from '@subwallet/extension-koni-ui/Popup/Home/Nfts/utils'; +import { getTotalCollectionItems, INftCollectionDetail, INftItemDetail } from '@subwallet/extension-koni-ui/Popup/Home/Nfts/utils'; +import { RootState } from '@subwallet/extension-koni-ui/stores'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { SwList } from '@subwallet/react-ui'; +import { Input, SwList } from '@subwallet/react-ui'; import CN from 'classnames'; import { Image } from 'phosphor-react'; -import React, { useCallback, useContext } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -type Props = ThemeProps +type Props = ThemeProps; +type SearchInputProps = { + initValue?: string; + placeholder: string; + onChange: (value: string) => void; +}; + +const SearchInput = ({ initValue = '', onChange, placeholder }: SearchInputProps) => { + const [searchValue, setSearchValue] = useState(initValue); + + const _onChange = useCallback((e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }, []); + + useEffect(() => { + const timeout: NodeJS.Timeout = setTimeout(() => { + onChange(searchValue); + }, 500); + + return () => { + clearTimeout(timeout); + }; + }, [onChange, searchValue]); + + return ( + + ); +}; function Component ({ className = '' }: Props): React.ReactElement { const location = useLocation(); - const { collectionInfo, nftList } = location.state as INftCollectionDetail; + const { collectionInfo } = location.state as INftCollectionDetail; + const { nftItems } = useGetNftByAccount(); + + const nftList = useMemo(() => { + const result: NftItem[] = []; + + nftItems.forEach((nftItem) => { + if (nftItem.collectionId === collectionInfo.collectionId && nftItem.chain === collectionInfo.chain) { + result.push(nftItem); + } + }); + + return result; + }, [collectionInfo.chain, collectionInfo.collectionId, nftItems]); + + const balanceMap = useSelector((state: RootState) => state.balance.balanceMap); + const currentAccountProxy = useSelector((state: RootState) => state.accountState.currentAccountProxy); const { t } = useTranslation(); const navigate = useNavigate(); const dataContext = useContext(DataContext); + const [searchValue, setSearchValue] = useState(''); useNavigateOnChangeAccount('/home/nfts/collections'); - const searchNft = useCallback((nftItem: NftItem, searchText: string) => { - const searchTextLowerCase = searchText.toLowerCase(); - return ( - nftItem.name?.toLowerCase().includes(searchTextLowerCase) || - nftItem.id.toLowerCase().includes(searchTextLowerCase) - ); - }, []); + const searchNft = useCallback( + (nftItem: NftItem) => { + const searchTextLowerCase = searchValue.toLowerCase(); + + return ( + nftItem.name?.toLowerCase().includes(searchTextLowerCase) || + nftItem.id.toLowerCase().includes(searchTextLowerCase) + ); + }, + [searchValue] + ); // todo: `need optimize child router for nfts` const handleOnClickNft = useCallback((state: INftItemDetail) => { @@ -90,10 +145,56 @@ function Component ({ className = '' }: Props): React.ReactElement { ); }, [t]); + const onChangeSearchInput = useCallback((value: string) => { + setSearchValue(value); + }, []); + + const totalItems = useMemo(() => { + return getTotalCollectionItems(collectionInfo.collectionId, currentAccountProxy, balanceMap, nftList.length); + }, [balanceMap, collectionInfo.collectionId, currentAccountProxy, nftList.length]); + + const hasMoreItems = useMemo(() => { + if (searchValue.length >= 2) { + return false; + } + + return nftList.length < totalItems; + }, [nftList.length, searchValue.length, totalItems]); + + const onLoadMoreItems = useCallback(() => { + if (hasMoreItems) { + loadMoreInscription().catch(console.log); + } + }, [hasMoreItems]); + + // note: memo to hot fix list scroll problem + const listSection = useMemo(() => ( + <> +
+ ('Search collectible name or ID')} + searchTerm={searchValue} + /> +
+ + ), [emptyNft, hasMoreItems, nftList, onLoadMoreItems, renderNft, searchNft, searchValue, t]); + return ( { {collectionInfo.collectionName || collectionInfo.collectionId}
-  ({nftList.length}) +  ({totalItems})
)} > - ('Search collectible name or ID')} - /> +
+
+ ('Search collectible name or ID')} + /> +
+ + {listSection} +
); @@ -154,11 +251,34 @@ const NftCollectionDetail = styled(Component)(({ theme: { token } }: Prop overflow: 'hidden' }, + '.ant-sw-list-section': { + display: 'flex', + overflow: 'hidden', + flexDirection: 'column' + }, + + '.ant-sw-list-search-input': { + padding: token.padding, + paddingTop: 0 + }, + '.collection-name': { overflow: 'hidden', textOverflow: 'ellipsis' }, + '.ant-sw-list-wrapper': { + flex: '1 1 100vh', + overflow: 'hidden' + }, + + '.ant-sw-list': { + overflow: 'auto', + maxHeight: '100%', + paddingLeft: token.padding, + paddingRight: token.padding + }, + '.nft_item_list__container': { flex: 1, height: '100%', diff --git a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollections.tsx b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollections.tsx index 344d67642e..84f3b77f63 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollections.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollections.tsx @@ -7,12 +7,14 @@ import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; import { useGetNftByAccount, useNotification, useSetCurrentPage, useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { reloadCron } from '@subwallet/extension-koni-ui/messaging'; import { NftGalleryWrapper } from '@subwallet/extension-koni-ui/Popup/Home/Nfts/component/NftGalleryWrapper'; -import { INftCollectionDetail } from '@subwallet/extension-koni-ui/Popup/Home/Nfts/utils'; +import { getTotalCollectionItems, INftCollectionDetail } from '@subwallet/extension-koni-ui/Popup/Home/Nfts/utils'; +import { RootState } from '@subwallet/extension-koni-ui/stores'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; import { ActivityIndicator, ButtonProps, Icon, SwList } from '@subwallet/react-ui'; import CN from 'classnames'; import { ArrowClockwise, Image } from 'phosphor-react'; import React, { useCallback, useContext } from 'react'; +import { useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; @@ -31,6 +33,8 @@ function Component ({ className = '' }: Props): React.ReactElement { const dataContext = useContext(DataContext); const { nftCollections, nftItems } = useGetNftByAccount(); const [loading, setLoading] = React.useState(false); + const balanceMap = useSelector((state: RootState) => state.balance.balanceMap); + const currentAccountProxy = useSelector((state: RootState) => state.accountState.currentAccountProxy); const notify = useNotification(); const subHeaderButton: ButtonProps[] = [ @@ -95,20 +99,20 @@ function Component ({ className = '' }: Props): React.ReactElement { } } - const state: INftCollectionDetail = { collectionInfo: nftCollection, nftList }; + const state: INftCollectionDetail = { collectionInfo: nftCollection }; return ( ); - }, [getNftsByCollection, handleOnClickCollection]); + }, [balanceMap, currentAccountProxy, getNftsByCollection, handleOnClickCollection]); const emptyNft = useCallback(() => { return ( @@ -123,7 +127,7 @@ function Component ({ className = '' }: Props): React.ReactElement { return ( , isTestnet = false): number | undefined { + if (!isTestnet) { + return (balanceMap[address]?.['bitcoin-NATIVE-BTC']?.metadata as BitcoinBalanceMetadata)?.inscriptionCount; + } + + return (balanceMap[address]?.['bitcoinTestnet-NATIVE-BTC']?.metadata as BitcoinBalanceMetadata)?.inscriptionCount; +} + +export function getTotalCollectionItems ( + collectionId: string, + accountProxy: AccountProxy | null, + balanceMap: Record, + itemsLength: number +): number { + if (!accountProxy || ![ORDINAL_COLLECTION_INFO.collectionId, ORDINAL_COLLECTION_INFO_TEST.collectionId].includes(collectionId)) { + return itemsLength; + } + + const isTestnet = collectionId === ORDINAL_COLLECTION_INFO_TEST.collectionId; + + if (accountProxy.proxyId === ALL_ACCOUNT_KEY) { + const totalInscription = getTotalInscriptions(ALL_ACCOUNT_KEY, balanceMap, isTestnet); + + if (totalInscription) { + return totalInscription; + } + } else { + const targetAccountType = isTestnet ? 'bittest-86' : 'bitcoin-86'; + + for (const account of accountProxy.accounts) { + if (account.type !== targetAccountType) { + continue; + } + + const totalInscription = getTotalInscriptions(account.address, balanceMap, isTestnet); + + if (totalInscription) { + return totalInscription; + } + } + } + + return itemsLength; +} diff --git a/packages/extension-koni-ui/src/messaging/transaction/nft.ts b/packages/extension-koni-ui/src/messaging/transaction/nft.ts index f963c382d9..9a4175d3b6 100644 --- a/packages/extension-koni-ui/src/messaging/transaction/nft.ts +++ b/packages/extension-koni-ui/src/messaging/transaction/nft.ts @@ -19,6 +19,10 @@ export async function subscribeNftCollection (callback: (data: NftCollection[]) return sendMessage('pri(nftCollection.getSubscription)', null, callback); } +export async function loadMoreInscription () { + return sendMessage('pri(inscription.loadMoreInscription)'); +} + export async function evmNftSubmitTransaction (request: NftTransactionRequest): Promise { return sendMessage('pri(evmNft.submitTransaction)', request); }