This repository has been archived by the owner on Oct 4, 2023. It is now read-only.
/
helpers.ts
246 lines (224 loc) · 7.05 KB
/
helpers.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import { OpenSeaAsset, OpenSeaEvent } from 'services/opensea-client/types'
import {
Collectible,
CollectibleType
} from 'containers/collectibles/components/types'
import { gifPreview } from 'utils/imageProcessingUtil'
/**
* extensions based on OpenSea metadata standards
* https://docs.opensea.io/docs/metadata-standards
*/
const OPENSEA_AUDIO_EXTENSIONS = ['mp3', 'wav', 'oga']
const OPENSEA_VIDEO_EXTENSIONS = [
'gltf',
'glb',
'webm',
'mp4',
'm4v',
'ogv',
'ogg',
'mov'
]
const SUPPORTED_VIDEO_EXTENSIONS = ['webm', 'mp4', 'ogv', 'ogg', 'mov']
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'
const isAssetImage = (asset: OpenSeaAsset) => {
const nonImageExtensions = [
...OPENSEA_VIDEO_EXTENSIONS,
...OPENSEA_AUDIO_EXTENSIONS
]
return [
asset.image_url,
asset.image_original_url,
asset.image_preview_url,
asset.image_thumbnail_url
].some(url => url && nonImageExtensions.every(ext => !url.endsWith(ext)))
}
const isAssetVideo = (asset: OpenSeaAsset) => {
const {
animation_url,
animation_original_url,
image_url,
image_original_url,
image_preview_url,
image_thumbnail_url
} = asset
return [
animation_url || '',
animation_original_url || '',
image_url,
image_original_url,
image_preview_url,
image_thumbnail_url
].some(
url => url && SUPPORTED_VIDEO_EXTENSIONS.some(ext => url.endsWith(ext))
)
}
const isAssetGif = (asset: OpenSeaAsset) => {
return !!(
asset.image_url?.endsWith('.gif') ||
asset.image_original_url?.endsWith('.gif') ||
asset.image_preview_url?.endsWith('.gif') ||
asset.image_thumbnail_url?.endsWith('.gif')
)
}
export const isAssetValid = (asset: OpenSeaAsset) => {
return isAssetVideo(asset) || isAssetImage(asset) || isAssetGif(asset)
}
/**
* Returns a collectible given an asset object from the OpenSea API
*
* A lot of the work here is to determine whether a collectible is a gif, a video, or an image
*
* If the collectible is a gif, we set the gifUrl, and we process a frame from the gifUrl which we set as its frameUrl
*
* If the collectible is a video, we set the videoUrl, and we check whether the asset has an image
* - if it has an image, we check whether the image url is an actual image or a video (sometimes OpenSea returns
* videos in the image url properties of the asset)
* - if it's an image, we set it as the frameUrl
* - otherwise, we unset the frameUrl
* - if not, we do not set the frameUrl
* Video collectibles that do not have a frameUrl will use the video paused at the first frame as the thumbnail
* in the collectibles tab
*
* Otherwise, we consider the collectible to be an image, we get the image url and make sure that it is not
* a gif or a video
* - if it's a gif, we follow the above gif logic
* - if it's a video, we unset the frameUrl and follow the above video logic
* - otherwise, we set the frameUrl and the imageUrl
*
* @param asset
*/
export const assetToCollectible = async (
asset: OpenSeaAsset
): Promise<Collectible> => {
let type: CollectibleType
let frameUrl = null
let imageUrl = null
let videoUrl = null
let gifUrl = null
const { animation_url, animation_original_url, name } = asset
const imageUrls = [
asset.image_url,
asset.image_original_url,
asset.image_preview_url,
asset.image_thumbnail_url
]
try {
if (isAssetGif(asset)) {
type = CollectibleType.GIF
const urlForFrame = imageUrls.find(url => url?.endsWith('.gif'))!
frameUrl = await getFrameFromGif(urlForFrame, name || '')
gifUrl = imageUrls.find(url => url?.endsWith('.gif'))!
} else if (isAssetVideo(asset)) {
type = CollectibleType.VIDEO
frameUrl =
imageUrls.find(
url =>
url && SUPPORTED_VIDEO_EXTENSIONS.every(ext => !url.endsWith(ext))
) ?? null
/**
* make sure frame url is not a video
* if it is a video, unset frame url so that component will use a video url instead
*/
if (frameUrl) {
const res = await fetch(frameUrl, { method: 'HEAD' })
const isVideo = res.headers.get('Content-Type')?.includes('video')
if (isVideo) {
frameUrl = null
}
}
videoUrl = [animation_url, animation_original_url, ...imageUrls].find(
url => url && SUPPORTED_VIDEO_EXTENSIONS.some(ext => url.endsWith(ext))
)!
} else {
type = CollectibleType.IMAGE
frameUrl = imageUrls.find(url => !!url)!
const res = await fetch(frameUrl, { method: 'HEAD' })
const isGif = res.headers.get('Content-Type')?.includes('gif')
const isVideo = res.headers.get('Content-Type')?.includes('video')
if (isGif) {
type = CollectibleType.GIF
gifUrl = frameUrl
frameUrl = await getFrameFromGif(frameUrl, name || '')
} else if (isVideo) {
type = CollectibleType.VIDEO
frameUrl = null
videoUrl = imageUrls.find(url => !!url)!
} else {
imageUrl = imageUrls.find(url => !!url)!
}
}
} catch (e) {
console.error('Error processing collectible', e)
type = CollectibleType.IMAGE
frameUrl = imageUrls.find(url => !!url)!
imageUrl = frameUrl
}
return {
id: asset.token_id,
name: asset.name,
description: asset.description,
type,
frameUrl,
imageUrl,
videoUrl,
gifUrl,
isOwned: true,
dateCreated: null,
dateLastTransferred: null,
externalLink: asset.external_link,
permaLink: asset.permalink
}
}
export const creationEventToCollectible = async (
event: OpenSeaEvent
): Promise<Collectible> => {
const { asset, created_date } = event
const collectible = await assetToCollectible(asset)
return {
...collectible,
dateCreated: created_date,
isOwned: false
}
}
export const transferEventToCollectible = async (
event: OpenSeaEvent,
isOwned = true
): Promise<Collectible> => {
const { asset, created_date } = event
const collectible = await assetToCollectible(asset)
return {
...collectible,
isOwned,
dateLastTransferred: created_date
}
}
export const isNotFromNullAddress = (event: OpenSeaEvent) => {
return event.from_account.address !== NULL_ADDRESS
}
const getFrameFromGif = async (url: string, name: string) => {
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1
const isSafariMobile =
navigator.userAgent.match(/iPad/i) || navigator.userAgent.match(/iPhone/i)
let preview
try {
// Firefox does not handle partial gif rendering well
if (isFirefox || isSafariMobile) {
throw new Error('partial gif not supported')
}
const req = await fetch(url, {
headers: {
// Extremely heuristic 200KB. This should contain the first frame
// and then some. Rendering this out into an <img tag won't allow
// animation to play. Some gifs may not load if we do this, so we
// can try-catch it.
Range: 'bytes=0-200000'
}
})
const ab = await req.arrayBuffer()
preview = new Blob([ab])
} catch (e) {
preview = await gifPreview(url)
}
return URL.createObjectURL(preview)
}