@metamask/assets-controllers
Advanced tools
Comparing version 21.0.0 to 22.0.0
@@ -9,2 +9,8 @@ # Changelog | ||
## [22.0.0] | ||
### Changed | ||
- **BREAKING:** OpenSea V2 API is used instead of V1 ([#3654](https://github.com/MetaMask/core/pull/3654)) | ||
- `NftDetectionController` constructor now requires the `NftController.getNftApi` function. | ||
- NFT controllers will no longer return `last_sale` information for NFTs fetched after the OpenSea V2 update | ||
## [21.0.0] | ||
@@ -484,3 +490,4 @@ ### Added | ||
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@21.0.0...HEAD | ||
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@22.0.0...HEAD | ||
[22.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@21.0.0...@metamask/assets-controllers@22.0.0 | ||
[21.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@20.0.0...@metamask/assets-controllers@21.0.0 | ||
@@ -487,0 +494,0 @@ [20.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@19.0.0...@metamask/assets-controllers@20.0.0 |
@@ -5,3 +5,4 @@ /// <reference types="bn.js" /> | ||
import { BN } from 'ethereumjs-util'; | ||
import type { Nft, NftMetadata } from './NftController'; | ||
import type { Nft, NftMetadata, OpenSeaV2Collection, OpenSeaV2Contract, OpenSeaV2DetailedNft, OpenSeaV2Nft } from './NftController'; | ||
import type { ApiNft, ApiNftContract } from './NftDetectionController'; | ||
/** | ||
@@ -142,2 +143,21 @@ * Compares nft metadata entries to any nft entry. | ||
}): Promise<Result>; | ||
/** | ||
* Maps an OpenSea V2 NFT to the V1 schema. | ||
* @param nft - The V2 NFT to map. | ||
* @returns The NFT in the V1 schema. | ||
*/ | ||
export declare function mapOpenSeaNftV2ToV1(nft: OpenSeaV2Nft): ApiNft; | ||
/** | ||
* Maps an OpenSea V2 detailed NFT to the V1 schema. | ||
* @param nft - The V2 detailed NFT to map. | ||
* @returns The NFT in the V1 schema. | ||
*/ | ||
export declare function mapOpenSeaDetailedNftV2ToV1(nft: OpenSeaV2DetailedNft): ApiNft; | ||
/** | ||
* Maps an OpenSea V2 contract to the V1 schema. | ||
* @param contract - The v2 contract data. | ||
* @param collection - The v2 collection data. | ||
* @returns The contract in the v1 schema. | ||
*/ | ||
export declare function mapOpenSeaContractV2ToV1(contract: OpenSeaV2Contract, collection?: OpenSeaV2Collection): ApiNftContract; | ||
//# sourceMappingURL=assetsUtil.d.ts.map |
@@ -12,3 +12,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.reduceInBatchesSerially = exports.divideIntoBatches = exports.ethersBigNumberToBN = exports.addUrlProtocolPrefix = exports.getFormattedIpfsUrl = exports.getIpfsCIDv1AndPath = exports.removeIpfsProtocolPrefix = exports.isTokenListSupportedForNetwork = exports.isTokenDetectionSupportedForNetwork = exports.SupportedTokenDetectionNetworks = exports.formatIconUrlWithProxy = exports.formatAggregatorNames = exports.compareNftMetadata = void 0; | ||
exports.mapOpenSeaContractV2ToV1 = exports.mapOpenSeaDetailedNftV2ToV1 = exports.mapOpenSeaNftV2ToV1 = exports.reduceInBatchesSerially = exports.divideIntoBatches = exports.ethersBigNumberToBN = exports.addUrlProtocolPrefix = exports.getFormattedIpfsUrl = exports.getIpfsCIDv1AndPath = exports.removeIpfsProtocolPrefix = exports.isTokenListSupportedForNetwork = exports.isTokenDetectionSupportedForNetwork = exports.SupportedTokenDetectionNetworks = exports.formatIconUrlWithProxy = exports.formatAggregatorNames = exports.compareNftMetadata = void 0; | ||
const controller_utils_1 = require("@metamask/controller-utils"); | ||
@@ -263,2 +263,80 @@ const ethereumjs_util_1 = require("ethereumjs-util"); | ||
exports.reduceInBatchesSerially = reduceInBatchesSerially; | ||
/** | ||
* Maps an OpenSea V2 NFT to the V1 schema. | ||
* @param nft - The V2 NFT to map. | ||
* @returns The NFT in the V1 schema. | ||
*/ | ||
function mapOpenSeaNftV2ToV1(nft) { | ||
var _a; | ||
return { | ||
token_id: nft.identifier, | ||
num_sales: null, | ||
background_color: null, | ||
image_url: (_a = nft.image_url) !== null && _a !== void 0 ? _a : null, | ||
image_preview_url: null, | ||
image_thumbnail_url: null, | ||
image_original_url: null, | ||
animation_url: null, | ||
animation_original_url: null, | ||
name: nft.name, | ||
description: nft.description, | ||
external_link: null, | ||
asset_contract: { | ||
address: nft.contract, | ||
asset_contract_type: null, | ||
created_date: null, | ||
schema_name: nft.token_standard.toUpperCase(), | ||
symbol: null, | ||
total_supply: null, | ||
description: nft.description, | ||
external_link: null, | ||
collection: { | ||
name: nft.collection, | ||
image_url: null, | ||
}, | ||
}, | ||
creator: { | ||
user: { username: '' }, | ||
profile_img_url: '', | ||
address: '', | ||
}, | ||
last_sale: null, | ||
}; | ||
} | ||
exports.mapOpenSeaNftV2ToV1 = mapOpenSeaNftV2ToV1; | ||
/** | ||
* Maps an OpenSea V2 detailed NFT to the V1 schema. | ||
* @param nft - The V2 detailed NFT to map. | ||
* @returns The NFT in the V1 schema. | ||
*/ | ||
function mapOpenSeaDetailedNftV2ToV1(nft) { | ||
var _a; | ||
const mapped = mapOpenSeaNftV2ToV1(nft); | ||
return Object.assign(Object.assign({}, mapped), { animation_url: (_a = nft.animation_url) !== null && _a !== void 0 ? _a : null, creator: Object.assign(Object.assign({}, mapped.creator), { address: nft.creator }) }); | ||
} | ||
exports.mapOpenSeaDetailedNftV2ToV1 = mapOpenSeaDetailedNftV2ToV1; | ||
/** | ||
* Maps an OpenSea V2 contract to the V1 schema. | ||
* @param contract - The v2 contract data. | ||
* @param collection - The v2 collection data. | ||
* @returns The contract in the v1 schema. | ||
*/ | ||
function mapOpenSeaContractV2ToV1(contract, collection) { | ||
var _a, _b, _c; | ||
return { | ||
address: contract.address, | ||
asset_contract_type: null, | ||
created_date: null, | ||
schema_name: contract.contract_standard.toUpperCase(), | ||
symbol: null, | ||
total_supply: contract.supply.toString(), | ||
description: (_a = collection === null || collection === void 0 ? void 0 : collection.description) !== null && _a !== void 0 ? _a : null, | ||
external_link: (_b = collection === null || collection === void 0 ? void 0 : collection.project_url) !== null && _b !== void 0 ? _b : null, | ||
collection: { | ||
name: (_c = collection === null || collection === void 0 ? void 0 : collection.name) !== null && _c !== void 0 ? _c : contract.name, | ||
image_url: collection === null || collection === void 0 ? void 0 : collection.image_url, | ||
}, | ||
}; | ||
} | ||
exports.mapOpenSeaContractV2ToV1 = mapOpenSeaContractV2ToV1; | ||
//# sourceMappingURL=assetsUtil.js.map |
@@ -24,2 +24,70 @@ /// <reference types="node" /> | ||
}; | ||
export declare enum OpenSeaV2ChainIds { | ||
ethereum = "ethereum" | ||
} | ||
export declare type OpenSeaV2GetNftResponse = { | ||
nft: OpenSeaV2DetailedNft; | ||
}; | ||
export declare type OpenSeaV2Nft = { | ||
identifier: string; | ||
collection: string; | ||
contract: string; | ||
token_standard: string; | ||
name: string; | ||
description: string; | ||
image_url?: string; | ||
metadata_url?: string; | ||
updated_at: string; | ||
is_disabled: boolean; | ||
is_nsfw: boolean; | ||
}; | ||
export declare type OpenSeaV2DetailedNft = OpenSeaV2Nft & { | ||
animation_url?: string; | ||
is_suspicious: boolean; | ||
creator: string; | ||
traits: { | ||
trait_type: string; | ||
display_type?: string; | ||
max_value: string; | ||
trait_count?: number; | ||
value: number | string; | ||
}[]; | ||
owners: { | ||
address: string; | ||
quantity: number; | ||
}[]; | ||
rarity: { | ||
rank: number; | ||
}; | ||
}; | ||
export declare type OpenSeaV2ListNftsResponse = { | ||
nfts: OpenSeaV2Nft[]; | ||
next?: string; | ||
}; | ||
export declare type OpenSeaV2Contract = { | ||
address: string; | ||
chain: string; | ||
collection: string; | ||
contract_standard: string; | ||
name: string; | ||
supply: number; | ||
}; | ||
export declare type OpenSeaV2Collection = { | ||
collection: string; | ||
name: string; | ||
description?: string; | ||
image_url?: string; | ||
owner: string; | ||
category: string; | ||
is_disabled: boolean; | ||
is_nsfw: boolean; | ||
trait_offers_enabled: boolean; | ||
opensea_url: string; | ||
project_url?: string; | ||
wiki_url?: string; | ||
discord_url?: string; | ||
telegram_url?: string; | ||
twitter_username?: string; | ||
instagram_username?: string; | ||
}; | ||
/** | ||
@@ -172,4 +240,8 @@ * @type Nft | ||
private readonly messagingSystem; | ||
private getNftApi; | ||
getNftApi({ contractAddress, tokenId, }: { | ||
contractAddress: string; | ||
tokenId: string; | ||
}): string; | ||
private getNftContractInformationApi; | ||
private getNftCollectionInformationApi; | ||
/** | ||
@@ -176,0 +248,0 @@ * Helper method to update nested state for allNfts and allNftContracts. |
@@ -12,3 +12,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.NftController = void 0; | ||
exports.NftController = exports.OpenSeaV2ChainIds = void 0; | ||
const address_1 = require("@ethersproject/address"); | ||
@@ -24,2 +24,6 @@ const base_controller_1 = require("@metamask/base-controller"); | ||
const constants_1 = require("./constants"); | ||
var OpenSeaV2ChainIds; | ||
(function (OpenSeaV2ChainIds) { | ||
OpenSeaV2ChainIds["ethereum"] = "ethereum"; | ||
})(OpenSeaV2ChainIds = exports.OpenSeaV2ChainIds || (exports.OpenSeaV2ChainIds = {})); | ||
const ALL_NFTS_STATE_KEY = 'allNfts'; | ||
@@ -103,7 +107,10 @@ const ALL_NFTS_CONTRACTS_STATE_KEY = 'allNftContracts'; | ||
getNftApi({ contractAddress, tokenId, }) { | ||
return `${controller_utils_1.OPENSEA_PROXY_URL}/asset/${contractAddress}/${tokenId}`; | ||
return `${controller_utils_1.OPENSEA_PROXY_URL}/chain/${OpenSeaV2ChainIds.ethereum}/contract/${contractAddress}/nfts/${tokenId}`; | ||
} | ||
getNftContractInformationApi({ contractAddress, }) { | ||
return `${controller_utils_1.OPENSEA_PROXY_URL}/asset_contract/${contractAddress}`; | ||
return `${controller_utils_1.OPENSEA_PROXY_URL}/chain/${OpenSeaV2ChainIds.ethereum}/contract/${contractAddress}`; | ||
} | ||
getNftCollectionInformationApi({ collectionSlug, }) { | ||
return `${controller_utils_1.OPENSEA_PROXY_URL}/collections/${collectionSlug}`; | ||
} | ||
/** | ||
@@ -145,3 +152,3 @@ * Helper method to update nested state for allNfts and allNftContracts. | ||
// if we were still unable to fetch the data we return out the default/null of `NftMetadata` | ||
if (!nftInformation) { | ||
if (!(nftInformation === null || nftInformation === void 0 ? void 0 : nftInformation.nft)) { | ||
return { | ||
@@ -156,3 +163,3 @@ name: null, | ||
// now we reconfigure the data to conform to the `NftMetadata` type for storage. | ||
const { num_sales, background_color, image_url, image_preview_url, image_thumbnail_url, image_original_url, animation_url, animation_original_url, name, description, external_link, creator, last_sale, asset_contract: { schema_name }, } = nftInformation; | ||
const { num_sales, background_color, image_url, image_preview_url, image_thumbnail_url, image_original_url, animation_url, animation_original_url, name, description, external_link, creator, last_sale, asset_contract: { schema_name }, } = (0, assetsUtil_1.mapOpenSeaDetailedNftV2ToV1)(nftInformation.nft); | ||
/* istanbul ignore next */ | ||
@@ -308,5 +315,11 @@ const nftMetadata = Object.assign({}, { name: name || null }, { description: description || null }, { image: image_url || null }, creator && { creator }, num_sales && { numberOfSales: num_sales }, background_color && { backgroundColor: background_color }, image_preview_url && { imagePreview: image_preview_url }, image_thumbnail_url && { imageThumbnail: image_thumbnail_url }, image_original_url && { imageOriginal: image_original_url }, animation_url && { animation: animation_url }, animation_original_url && { | ||
}); | ||
// if we successfully fetched return the fetched data immediately | ||
// If we successfully fetched the contract | ||
if (apiNftContractObject) { | ||
return apiNftContractObject; | ||
// Then fetch some additional details from the collection | ||
const collection = yield (0, controller_utils_1.fetchWithErrorHandling)({ | ||
url: this.getNftCollectionInformationApi({ | ||
collectionSlug: apiNftContractObject.collection, | ||
}), | ||
}); | ||
return (0, assetsUtil_1.mapOpenSeaContractV2ToV1)(apiNftContractObject, collection); | ||
} | ||
@@ -313,0 +326,0 @@ // If we've reached this point we were unable to fetch data from either the proxy or opensea so we return |
@@ -6,3 +6,3 @@ import type { BaseConfig, BaseState } from '@metamask/base-controller'; | ||
import type { Hex } from '@metamask/utils'; | ||
import type { NftController, NftState } from './NftController'; | ||
import { type NftController, type NftState } from './NftController'; | ||
/** | ||
@@ -130,2 +130,3 @@ * @type ApiNft | ||
private readonly addNft; | ||
private readonly getNftApi; | ||
private readonly getNftState; | ||
@@ -143,2 +144,3 @@ private readonly getNetworkClientById; | ||
* @param options.addNft - Add an NFT. | ||
* @param options.getNftApi - Gets the URL to fetch an NFT from OpenSea. | ||
* @param options.getNftState - Gets the current state of the Assets controller. | ||
@@ -149,3 +151,3 @@ * @param options.getNetworkClientById - Gets the network client by ID, from the NetworkController. | ||
*/ | ||
constructor({ chainId: initialChainId, getNetworkClientById, onPreferencesStateChange, onNetworkStateChange, getOpenSeaApiKey, addNft, getNftState, }: { | ||
constructor({ chainId: initialChainId, getNetworkClientById, onPreferencesStateChange, onNetworkStateChange, getOpenSeaApiKey, addNft, getNftApi, getNftState, }: { | ||
chainId: Hex; | ||
@@ -158,2 +160,3 @@ getNetworkClientById: NetworkController['getNetworkClientById']; | ||
addNft: NftController['addNft']; | ||
getNftApi: NftController['getNftApi']; | ||
getNftState: () => NftState; | ||
@@ -160,0 +163,0 @@ }, config?: Partial<NftDetectionConfig>, state?: Partial<BaseState>); |
@@ -15,3 +15,5 @@ "use strict"; | ||
const polling_controller_1 = require("@metamask/polling-controller"); | ||
const assetsUtil_1 = require("./assetsUtil"); | ||
const constants_1 = require("./constants"); | ||
const NftController_1 = require("./NftController"); | ||
const DEFAULT_INTERVAL = 180000; | ||
@@ -32,2 +34,3 @@ /** | ||
* @param options.addNft - Add an NFT. | ||
* @param options.getNftApi - Gets the URL to fetch an NFT from OpenSea. | ||
* @param options.getNftState - Gets the current state of the Assets controller. | ||
@@ -38,3 +41,3 @@ * @param options.getNetworkClientById - Gets the network client by ID, from the NetworkController. | ||
*/ | ||
constructor({ chainId: initialChainId, getNetworkClientById, onPreferencesStateChange, onNetworkStateChange, getOpenSeaApiKey, addNft, getNftState, }, config, state) { | ||
constructor({ chainId: initialChainId, getNetworkClientById, onPreferencesStateChange, onNetworkStateChange, getOpenSeaApiKey, addNft, getNftApi, getNftState, }, config, state) { | ||
super(config, state); | ||
@@ -85,18 +88,16 @@ /** | ||
this.addNft = addNft; | ||
this.getNftApi = getNftApi; | ||
this.setIntervalLength(this.config.interval); | ||
} | ||
getOwnerNftApi({ address, offset, }) { | ||
return `${controller_utils_1.OPENSEA_PROXY_URL}/assets?owner=${address}&offset=${offset}&limit=50`; | ||
getOwnerNftApi({ address, next, }) { | ||
return `${controller_utils_1.OPENSEA_PROXY_URL}/chain/${NftController_1.OpenSeaV2ChainIds.ethereum}/account/${address}/nfts?limit=200&next=${next !== null && next !== void 0 ? next : ''}`; | ||
} | ||
getOwnerNfts(address) { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
let nftApiResponse; | ||
let nfts = []; | ||
let offset = 0; | ||
let pagingFinish = false; | ||
/* istanbul ignore if */ | ||
let next; | ||
do { | ||
nftApiResponse = yield (0, controller_utils_1.fetchWithErrorHandling)({ | ||
url: this.getOwnerNftApi({ address, offset }), | ||
url: this.getOwnerNftApi({ address, next }), | ||
timeout: 15000, | ||
@@ -107,7 +108,18 @@ }); | ||
} | ||
((_a = nftApiResponse === null || nftApiResponse === void 0 ? void 0 : nftApiResponse.assets) === null || _a === void 0 ? void 0 : _a.length) !== 0 | ||
? (nfts = [...nfts, ...nftApiResponse.assets]) | ||
: (pagingFinish = true); | ||
offset += 50; | ||
} while (!pagingFinish); | ||
const newNfts = yield Promise.all(nftApiResponse.nfts.map((nftV2) => __awaiter(this, void 0, void 0, function* () { | ||
var _a, _b; | ||
const nftV1 = (0, assetsUtil_1.mapOpenSeaNftV2ToV1)(nftV2); | ||
// If the image hasn't been processed into OpenSea's CDN, the image_url will be null. | ||
// Try fetching the NFT individually, which returns the original image url from metadata if available. | ||
if (!nftV1.image_url && nftV2.metadata_url) { | ||
const nftDetails = yield (0, controller_utils_1.safelyExecute)(() => (0, controller_utils_1.timeoutFetch)(this.getNftApi({ | ||
contractAddress: nftV2.contract, | ||
tokenId: nftV2.identifier, | ||
}), undefined, 1000).then((r) => r.json())); | ||
nftV1.image_original_url = (_b = (_a = nftDetails === null || nftDetails === void 0 ? void 0 : nftDetails.nft) === null || _a === void 0 ? void 0 : _a.image_url) !== null && _b !== void 0 ? _b : null; | ||
} | ||
return nftV1; | ||
}))); | ||
nfts = [...nfts, ...newNfts]; | ||
} while ((next = nftApiResponse.next)); | ||
return nfts; | ||
@@ -114,0 +126,0 @@ }); |
{ | ||
"name": "@metamask/assets-controllers", | ||
"version": "21.0.0", | ||
"version": "22.0.0", | ||
"description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)", | ||
@@ -41,3 +41,3 @@ "keywords": [ | ||
"@metamask/contract-metadata": "^2.4.0", | ||
"@metamask/controller-utils": "^7.0.0", | ||
"@metamask/controller-utils": "^8.0.0", | ||
"@metamask/eth-query": "^4.0.0", | ||
@@ -44,0 +44,0 @@ "@metamask/metamask-eth-abis": "3.0.0", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
804979
7566