Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/extension-base/src/koni/background/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export class KoniCron {
return;
}

this.state.nftDetectionService.fetchEvmCollectionsWithPreview(addresses)
this.state.nftService.syncPreview(address)
.catch((err) => console.warn(`[Cron] NFT detection failed for ${address}:`, err));
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1323,7 +1323,7 @@ export default class KoniExtension {
}

private async handleGetNftFullList (request: NftFullListRequest): Promise <boolean> {
return this.#koniState.nftDetectionService.getFullNftInstancesByCollection(request);
return this.#koniState.nftService.syncFull(request);
}

private getStakingReward (): Promise<StakingRewardJson> {
Expand Down
6 changes: 3 additions & 3 deletions packages/extension-base/src/koni/background/handlers/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { KeyringService } from '@subwallet/extension-base/services/keyring-servi
import MigrationService from '@subwallet/extension-base/services/migration-service';
import MintCampaignService from '@subwallet/extension-base/services/mint-campaign-service';
import MktCampaignService from '@subwallet/extension-base/services/mkt-campaign-service';
import NftService from '@subwallet/extension-base/services/nft-service';
import { NftService } from '@subwallet/extension-base/services/nft-service';
import NotificationService from '@subwallet/extension-base/services/notification-service/NotificationService';
import { PriceService } from '@subwallet/extension-base/services/price-service';
import RequestService from '@subwallet/extension-base/services/request-service';
Expand Down Expand Up @@ -136,7 +136,7 @@ export default class KoniState {
readonly mintCampaignService: MintCampaignService;
readonly campaignService: CampaignService;
readonly mktCampaignService: MktCampaignService;
readonly nftDetectionService: NftService;
readonly nftService: NftService;
readonly buyService: BuyService;
readonly earningService: EarningService;
readonly feeService: FeeService;
Expand Down Expand Up @@ -176,7 +176,7 @@ export default class KoniState {

this.campaignService = new CampaignService(this);
this.mktCampaignService = new MktCampaignService(this);
this.nftDetectionService = new NftService(this);
this.nftService = new NftService(this);
this.buyService = new BuyService(this);
this.earningService = new EarningService(this);
this.swapService = new SwapService(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2019-2022 @subwallet/extension-koni authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { NftCollection, NftFullListRequest, NftItem } from '@subwallet/extension-base/background/KoniTypes';
import KoniState from '@subwallet/extension-base/koni/background/handlers/State';

export interface FetchCollectionsResult {
items: NftItem[];
collections: NftCollection[];
}

export abstract class AbstractNftService {
protected state: KoniState;

constructor (state: KoniState) {
this.state = state;
}

abstract detectPreview(addresses: string[]): Promise<void>;
// abstract fetchFull(address: string): Promise<FetchCollectionsResult>;
abstract getFullNftInstances(request: NftFullListRequest): Promise<boolean>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright 2019-2022 @subwallet/extension-koni authors & contributors
// SPDX-License-Identifier: Apache-2.0

// Utils function
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright 2019-2022 @subwallet/extension-koni authors & contributors
// SPDX-License-Identifier: Apache-2.0

// Utils function
260 changes: 30 additions & 230 deletions packages/extension-base/src/services/nft-service/index.ts
Original file line number Diff line number Diff line change
@@ -1,249 +1,49 @@
// Copyright 2019-2022 @subwallet/extension-base authors & contributors
// Copyright 2019-2022 @subwallet/extension-koni authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { _AssetType } from '@subwallet/chain-list/types';
import { NftCollection, NftFullListRequest, NftItem } from '@subwallet/extension-base/background/KoniTypes';
import KoniState from '@subwallet/extension-base/koni/background/handlers/State';
import { _getEvmChainId } from '@subwallet/extension-base/services/chain-service/utils';
import { baseParseIPFSUrl } from '@subwallet/extension-base/utils';
import { getKeypairTypeByAddress } from '@subwallet/keyring';
import { EthereumKeypairTypes } from '@subwallet/keyring/types';
import subwalletApiSdk from '@subwallet-monorepos/subwallet-services-sdk';
import { BlockscoutNftInstanceRaw } from '@subwallet-monorepos/subwallet-services-sdk/services/blockscout/types';

/**
* NFT detection service
* Responsible for managing NFT detection jobs per address
*/

interface SdkToken {
address_hash: string;
name?: string | null;
symbol?: string | null;
type?: _AssetType.ERC721;
icon_url?: string | null;
}

interface SdkCollection {
amount: string;
token: SdkToken;
token_instances: BlockscoutNftInstanceRaw[];
}

type SdkCollectionsByChain = Record<string, SdkCollection[]>;

function mapSdkToNftItem (
rawInstance: BlockscoutNftInstanceRaw,
chain: string,
collectionId: string,
owner: string
): NftItem | null {
const metadata = rawInstance.metadata || {};

const image = metadata.image || rawInstance.image_url || rawInstance.media_url || '';

const attributes = Array.isArray(metadata.attributes) ? metadata.attributes : [];

let rarity: string | undefined;
const properties: Record<string, string | number | boolean | null> = {};

for (const attr of attributes) {
try {
const key = attr.trait_type?.trim();

if (!key) {
continue;
}

let value = attr.value as unknown as string | number | boolean | null;

if (typeof value === 'string') {
const lower = value.toLowerCase();

if (lower === 'true') {
value = true;
} else if (lower === 'false') {
value = false;
}
}

properties[key] = value;

if (key.toLowerCase() === 'rarity') {
rarity = String(value);
}
} catch {
}
import EvmNftService from '@subwallet/extension-base/services/nft-service/multichain/evm/evm-nft-service';
import { UniqueNftService } from '@subwallet/extension-base/services/nft-service/multichain/unique';
import { NftServiceRegistry } from '@subwallet/extension-base/services/nft-service/registry/nft-service-registry';

export class NftService {
private registry: NftServiceRegistry;
constructor (private state: KoniState) {
this.registry = new NftServiceRegistry();
this.registerDefaultServices();
}

const hasProperties = Object.keys(properties).length > 0;
const normalizedType = rawInstance.token_type?.replace('-', '')?.toUpperCase();

// Only support ERC721
if (normalizedType !== 'ERC721') {
return null;
private registerDefaultServices (): void {
this.registry.register('evm', new EvmNftService(this.state));
this.registry.register('unique', new UniqueNftService(this.state));
}

return {
id: rawInstance.id?.toString(),
chain,
collectionId,
owner: rawInstance.owner || owner,

originAsset: undefined,
name: metadata.name || `#${rawInstance.id}`,
image: baseParseIPFSUrl(image),
externalUrl: rawInstance.external_app_url || undefined,
rarity,
description: metadata.description || undefined,
properties: hasProperties ? properties : null,
// Cron trigger (detect collection and preview nft)
async syncPreview (address: string) {
const chains = this.registry.getAllSupportedChains();

type: normalizedType === 'ERC721' ? _AssetType.ERC721 : _AssetType.ERC721, // currently only support ERC721
rmrk_ver: undefined,
onChainOption: undefined,
assetHubType: undefined
};
}
for (const chain of chains) {
const service = this.registry.getService(chain);

function mapSdkToCollection (raw: SdkCollection, chain: string): NftCollection {
const token = raw.token || {};

return {
// must-have
collectionId: token.address_hash,
chain,
originAsset: undefined,

// optional
collectionName: token.name || token.symbol || 'Unknown Collection',
image: token.icon_url || undefined,
itemCount: Number(raw.amount) || raw.token_instances?.length || 0,
externalUrl: undefined
};
}

export default class NftService {
private inProgress = new Set<string>();
private state: KoniState;

constructor (state: KoniState) {
this.state = state;
}
const { collections, items } = await service.detectPreview(address);

async fetchEvmCollectionsWithPreview (addresses: string[]) {
for (const address of addresses) {
const type = getKeypairTypeByAddress(address);
const typeValid = [...EthereumKeypairTypes].includes(type);

if (typeValid) {
if (this.inProgress.has(address)) {
console.log(`[NftService] ${address} already running`);

continue;
}

this.inProgress.add(address);

try {
const nftDetectionApi = subwalletApiSdk.nftDetectionApi;

if (!nftDetectionApi?.getEvmNftCollectionsByAddress) {
console.warn('[NftService] NftDetectionApi not available');

continue;
}

const rawData: SdkCollectionsByChain = await nftDetectionApi.getEvmNftCollectionsByAddress(address);

const allItems: NftItem[] = [];
const allCollections: NftCollection[] = [];

for (const [chain, collections] of Object.entries(rawData)) {
if (!Array.isArray(collections)) {
continue;
}

for (const col of collections) {
const mappedCollection = mapSdkToCollection(col, chain);

allCollections.push(mappedCollection);

if (Array.isArray(col.token_instances)) {
const items = col.token_instances.map((inst) =>
mapSdkToNftItem(inst, chain, mappedCollection.collectionId, address)
).filter((i): i is NftItem => Boolean(i));

allItems.push(...items);
}
}
}

await this.state.handleDetectedNftCollections(allCollections);
await this.state.handleDetectedNfts(address, allItems);
} catch (err) {
console.warn(`[NftService] detect error for ${address}`, err);
} finally {
this.inProgress.delete(address);
}
}
await this.state.handleDetectedNftCollections(collections);
await this.state.handleDetectedNfts(address, items);
}
}

async getFullNftInstancesByCollection (request: NftFullListRequest): Promise<boolean> {
const { chainInfo, contractAddress, owners } = request;
const chainId = _getEvmChainId(chainInfo);

if (!contractAddress || !owners || !chainId) {
console.warn('[NftService] missing params for getFullNftInstancesByCollection');

return false;
}

try {
const nftDetectionApi = subwalletApiSdk.nftDetectionApi;

if (!nftDetectionApi?.getAllNftInstances) {
console.warn('[NftService] getAllNftInstances not available');

return false;
}

const ownerList = Array.isArray(owners) ? owners : [owners];

for (const eachOwner of ownerList) {
try {
const instances = await nftDetectionApi.getAllNftInstances(
contractAddress,
eachOwner,
chainId.toString()
);

if (!Array.isArray(instances)) {
continue;
}

console.log('FOR TESTER (before)', instances);

const nftList = instances.map((inst) =>
mapSdkToNftItem(inst, chainInfo.slug, contractAddress, eachOwner)
).filter((i): i is NftItem => Boolean(i));

console.log('FOR TESTER (after)', nftList);
// UI trigger gọi hàm này (fetch full list nft)
async syncFull (addresses: string[]) {
const address = addresses[0];
const chains = this.registry.getAllSupportedChains();

await this.state.handleDetectedNfts(eachOwner, nftList);
} catch (innerErr) {
console.warn(`[NftService] getAllNftInstances failed for ${eachOwner}`, innerErr);
}
}
for (const chain of chains) {
const service = this.registry.getService(chain);

return true;
} catch (err) {
console.error(
`[NftDetectionService] getFullNftInstancesByCollection error for ${contractAddress}`,
err
);
const { collections, items } = await service.getFullNftInstances(address);

return false;
await this.state.handleDetectedNftCollections(collections);
await this.state.handleDetectedNfts(address, items);
}
}
}
Loading