Skip to content

Commit c8b3b28

Browse files
committed
Handle nullable OpenSea image URLs and fallback
Make OpenSea image fields optional and add original_image_url; implement fallback selection for resource and preview URLs in the mapper. Update mapping to use resource_url()/preview_url() helpers that prefer image_url, original_image_url, or display_image_url in a safe order, and replace direct field usage. Add a test asset JSON (asset_null_images.json) and a unit test to verify mapping when image/display URLs are null. Also adjust an existing test to use unwrap for brevity.
1 parent d85e156 commit c8b3b28

3 files changed

Lines changed: 102 additions & 5 deletions

File tree

crates/nft/src/providers/opensea/mapper.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub fn map_asset(response: NftResponse, asset_id: NFTAssetId) -> Option<NFTAsset
1818
impl Nft {
1919
pub fn as_primitive(&self, asset: NFTAssetId) -> Option<NFTAsset> {
2020
let traits = self.traits.clone().unwrap_or_default();
21+
let resource_url = self.resource_url();
22+
let preview_url = self.preview_url();
23+
2124
Some(NFTAsset {
2225
id: asset.to_string(),
2326
collection_id: asset.get_collection_id().id(),
@@ -27,14 +30,30 @@ impl Nft {
2730
name: self.name.clone(),
2831
description: Some(self.description.clone()),
2932
chain: asset.chain,
30-
resource: NFTResource::from_url(&self.image_url),
33+
resource: NFTResource::from_url(resource_url),
3134
images: NFTImages {
32-
preview: NFTResource::from_url(&self.display_image_url),
35+
preview: NFTResource::from_url(preview_url),
3336
},
3437
attributes: traits.iter().flat_map(|x| x.as_attribute()).collect(),
3538
})
3639
}
3740

41+
fn resource_url(&self) -> &str {
42+
self.image_url
43+
.as_deref()
44+
.or(self.original_image_url.as_deref())
45+
.or(self.display_image_url.as_deref())
46+
.unwrap_or_default()
47+
}
48+
49+
fn preview_url(&self) -> &str {
50+
self.display_image_url
51+
.as_deref()
52+
.or(self.image_url.as_deref())
53+
.or(self.original_image_url.as_deref())
54+
.unwrap_or_default()
55+
}
56+
3857
fn as_type(&self) -> Option<NFTType> {
3958
match self.token_standard.as_str() {
4059
"erc1155" => Some(NFTType::ERC1155),
@@ -180,7 +199,7 @@ mod tests {
180199

181200
let asset_id = NFTAssetId::new(Chain::Ethereum, "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D", "1");
182201

183-
let nft_asset = response.nft.as_primitive(asset_id).expect("Failed to map asset");
202+
let nft_asset = response.nft.as_primitive(asset_id).unwrap();
184203

185204
assert_eq!(nft_asset.chain, Chain::Ethereum);
186205
assert_eq!(nft_asset.token_id, "1");
@@ -211,4 +230,25 @@ mod tests {
211230
assert!(nft_collection.links.iter().any(|link| link.url.contains("boredapeyachtclub.com")));
212231
assert!(nft_collection.links.iter().any(|link| link.url.contains("discord.gg")));
213232
}
233+
234+
#[test]
235+
fn test_map_asset_with_null_image_urls() {
236+
let response: NftResponse = serde_json::from_str(include_str!("../../../testdata/opensea/asset_null_images.json")).unwrap();
237+
let asset_id = NFTAssetId::new(
238+
Chain::Ethereum,
239+
"0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401",
240+
"66972740172774133895361774757009899712806299063970949277266423600598010529206",
241+
);
242+
243+
let nft_asset = map_asset(response, asset_id).unwrap();
244+
245+
assert_eq!(nft_asset.chain, Chain::Ethereum);
246+
assert_eq!(nft_asset.token_id, "66972740172774133895361774757009899712806299063970949277266423600598010529206");
247+
assert_eq!(nft_asset.name, "gemdev.eth");
248+
assert_eq!(
249+
nft_asset.resource.url,
250+
"https://metadata.ens.domains/mainnet/0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401/0x94113a45c5bedf735911bf707d70a6c05d9d99e76ece7e904c0eeda6591785b6/image"
251+
);
252+
assert_eq!(nft_asset.images.preview.url, nft_asset.resource.url);
253+
}
214254
}

crates/nft/src/providers/opensea/model.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ pub struct Nft {
3737
pub token_standard: TokenStandard,
3838
pub name: String,
3939
pub description: String,
40-
pub image_url: String,
41-
pub display_image_url: String,
40+
pub image_url: Option<String>,
41+
pub display_image_url: Option<String>,
42+
pub original_image_url: Option<String>,
4243
pub traits: Option<Vec<Trait>>,
4344
}
4445

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"nft": {
3+
"identifier": "66972740172774133895361774757009899712806299063970949277266423600598010529206",
4+
"collection": "ens",
5+
"contract": "0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401",
6+
"token_standard": "erc1155",
7+
"name": "gemdev.eth",
8+
"description": "gemdev.eth, an ENS name.",
9+
"image_url": null,
10+
"display_image_url": null,
11+
"display_animation_url": null,
12+
"metadata_url": "https://metadata.ens.domains/mainnet/0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401/66972740172774133895361774757009899712806299063970949277266423600598010529206",
13+
"opensea_url": "https://opensea.io/assets/ethereum/0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401/66972740172774133895361774757009899712806299063970949277266423600598010529206",
14+
"updated_at": "2026-03-24T13:17:04.153143",
15+
"is_disabled": false,
16+
"is_nsfw": false,
17+
"original_image_url": "https://metadata.ens.domains/mainnet/0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401/0x94113a45c5bedf735911bf707d70a6c05d9d99e76ece7e904c0eeda6591785b6/image",
18+
"original_animation_url": null,
19+
"traits": [
20+
{
21+
"trait_type": "Length",
22+
"display_type": "NUMBER",
23+
"max_value": null,
24+
"value": "6"
25+
},
26+
{
27+
"trait_type": "Created Date",
28+
"display_type": null,
29+
"max_value": null,
30+
"value": "1774356395"
31+
},
32+
{
33+
"trait_type": "Character Set",
34+
"display_type": "STRING",
35+
"max_value": null,
36+
"value": "letter"
37+
},
38+
{
39+
"trait_type": "Segment Length",
40+
"display_type": "NUMBER",
41+
"max_value": null,
42+
"value": "6"
43+
}
44+
],
45+
"animation_url": null,
46+
"is_suspicious": false,
47+
"creator": "",
48+
"owners": [
49+
{
50+
"address": "0x8d7460e51bcf4ed26877cb77e56f3ce7e9f5eb8f",
51+
"quantity": 1
52+
}
53+
],
54+
"rarity": null
55+
}
56+
}

0 commit comments

Comments
 (0)