requests.js
1 import { ref, inject } from 'vue' 2 import { defineStore } from 'pinia' 3 import { slotId } from '../utils/ids' 4 import { arrayToObject, toRequestState, timestampsFor } from '@/utils/requests' 5 import { toSlotState } from '@/utils/slots' 6 import { RequestState } from '@/utils/requests' 7 import { StorageEvent } from '@/utils/events' 8 import serializer from './serializer' 9 import { useEventsStore } from './events' 10 11 class RequestNotFoundError extends Error { 12 constructor(requestId, ...params) { 13 // Pass remaining arguments (including vendor specific ones) to parent constructor 14 super(...params) 15 16 // Maintains proper stack trace for where our error was thrown (only available on V8) 17 if (Error.captureStackTrace) { 18 Error.captureStackTrace(this, RequestNotFoundError) 19 } 20 21 this.name = 'RequestNotFoundError' 22 // Custom debugging information 23 this.requestId = requestId 24 } 25 } 26 27 export const useRequestsStore = defineStore( 28 'requests', 29 () => { 30 const marketplace = inject('marketplace') 31 const ethProvider = inject('ethProvider') 32 const events = useEventsStore() 33 let { StorageRequested } = marketplace.filters 34 const requests = ref({}) // key: requestId, val: {request, state, slots: [{slotId, slotIdx, state}]} 35 const blocks = ref({}) 36 const loading = ref({ 37 past: false, 38 recent: false, 39 states: false 40 }) 41 const fetched = ref({ 42 past: false 43 }) 44 // Request structure 45 // { 46 // request: { 47 // client, 48 // ask: { 49 // slots, 50 // slotSize, 51 // duration, 52 // proofProbability, 53 // reward, 54 // collateral, 55 // maxSlotLoss 56 // }, 57 // content: { 58 // cid, 59 // merkleRoot 60 // }, 61 // expiry, 62 // nonce, 63 // }, 64 // state, 65 // moderated: false, 66 // requestedAt: Number (timestamp in seconds), 67 // requestFinishedId: Number (setTimeout id for request completion update) 68 // slots: [{slotId, slotIdx, state}], 69 // loading: { 70 // request: false, 71 // slots: false, 72 // state: false 73 // }, 74 // fetched: { 75 // request: false, 76 // slots: false 77 // } 78 // } 79 80 // fetch request details if not previously fetched 81 const getRequest = async (requestId) => { 82 try { 83 let request = requests.value[requestId] 84 if (exists(requestId) && request.fetched.request === true) { 85 console.log('request', requestId, ' details already fetched') 86 return request 87 } 88 requests.value[requestId] = { 89 loading: { 90 request: true, 91 slots: false, 92 state: false 93 }, 94 fetched: { 95 request: false, 96 slots: false 97 } 98 } 99 request = arrayToObject(await marketplace.getRequest(requestId)) 100 await getRequestState(requestId) 101 requests.value[requestId] = { 102 ...requests.value[requestId], 103 request, 104 moderated: 'pending' 105 // requestedAt: will be set in addFromEvent, as we don't have a block 106 // requestFinishedId: will be set in addFromEvent as we don't have 107 // requestedAt yet 108 // state: state is set in getRequestState 109 } 110 requests.value[requestId].loading.request = false 111 requests.value[requestId].fetched.request = true 112 return requests.value[requestId] 113 } catch (e) { 114 delete requests.value[requestId] 115 throw new Error(`failed to get request for ${requestId}: ${e.message}`) 116 } 117 } 118 119 const getRequestState = async (requestId) => { 120 if (!exists(requestId)) { 121 throw new RequestNotFoundError(requestId, `Request not found`) 122 } 123 try { 124 requests.value[requestId].loading.state = true 125 const stateIdx = await marketplace.requestState(requestId) 126 const state = toRequestState(Number(stateIdx)) 127 requests.value[requestId].state = state 128 } catch (e) { 129 throw new Error(`failed to get request state for ${requestId}: ${e.message}`) 130 } finally { 131 requests.value[requestId].loading.state = false 132 } 133 } 134 135 const getSlotState = async (slotId) => { 136 let stateIdx = await marketplace.slotState(slotId) 137 return toSlotState(Number(stateIdx)) 138 } 139 140 const getSlots = async (requestId, numSlots) => { 141 if (!exists(requestId)) { 142 throw new RequestNotFoundError(requestId, `Request not found`) 143 } 144 requests.value[requestId].loading.slots = true 145 try { 146 console.log(`fetching ${numSlots} slots`) 147 let start = Date.now() 148 let slots = [] 149 for (let slotIdx = 0; slotIdx < numSlots; slotIdx++) { 150 try { 151 let id = slotId(requestId, slotIdx) 152 const startSlotState = Date.now() 153 let state = await getSlotState(id) 154 console.log(`fetched slot state in ${(Date.now() - startSlotState) / 1000}s`) 155 const startGetHost = Date.now() 156 let provider = await marketplace.getHost(id) 157 console.log(`fetched slot provider in ${(Date.now() - startGetHost) / 1000}s`) 158 slots.push({ slotId: id, slotIdx, state, provider }) 159 } catch (e) { 160 console.error('error getting slot details', e) 161 continue 162 } 163 } 164 console.log(`fetched ${numSlots} slots in ${(Date.now() - start) / 1000}s`) 165 requests.value[requestId].slots = slots 166 return slots 167 } catch (e) { 168 throw new Error(`error fetching slots for ${requestId}: ${e.message}`) 169 } finally { 170 requests.value[requestId].loading.slots = false 171 requests.value[requestId].fetched.slots = true 172 } 173 } 174 175 const getBlock = async (blockHash) => { 176 if (blockHash in blocks.value) { 177 return blocks.value[blockHash] 178 } else { 179 let { number, timestamp } = await ethProvider.getBlock(blockHash) 180 blocks.value[blockHash] = { number, timestamp } 181 return { number, timestamp } 182 } 183 } 184 185 const addFromEvent = async (requestId, ask, expiry, blockHash) => { 186 let request = await getRequest(requestId) 187 let { state } = request.request 188 let { timestamp } = await getBlock(blockHash) 189 let requestFinishedId = waitForRequestFinished(requestId, ask, expiry, state, timestamp) 190 191 requests.value[requestId].requestedAt = timestamp 192 requests.value[requestId].requestFinishedId = requestFinishedId 193 194 return request 195 } 196 197 const exists = (requestId) => { 198 return requestId in requests.value 199 } 200 201 // Returns an array of Promises, where each Promise represents the fetching 202 // of one StorageRequested event 203 async function fetchPastRequestsFrom(fromBlock = null) { 204 console.log(`fetching past requests from ${fromBlock ? `block ${fromBlock}` : 'all time'}`) 205 try { 206 let events = await marketplace.queryFilter(StorageRequested, fromBlock) 207 console.log('got ', events.length, ' StorageRequested events') 208 if (events.length === 0) { 209 return [] 210 } 211 212 return events.map(async (event, i) => { 213 let { requestId, ask, expiry } = event.args 214 let { blockHash, blockNumber } = event 215 await addFromEvent(requestId, ask, expiry, blockHash) 216 }) 217 } catch (error) { 218 console.error(`failed to load past contract events: ${error.message}`) 219 return [] 220 } 221 } 222 223 async function refetchRequestStates() { 224 async function refetchRequestState(requestId) { 225 await getRequestState(requestId) 226 let { 227 request: { ask, expiry }, 228 state, 229 requestedAt 230 } = requests.value[requestId] 231 // refetching of requests states happen on page load, so if we're 232 // loading the page, we need to reset any timeouts for RequestFinished 233 // events 234 requests.value[requestId].requestFinishedId = waitForRequestFinished( 235 requestId, 236 ask, 237 expiry, 238 state, 239 requestedAt 240 ) 241 } 242 // array of asynchronously-executed Promises, each requesting a request 243 // state 244 loading.value.states = true 245 try { 246 const fetches = Object.entries(requests.value).map(([requestId, request]) => 247 refetchRequestState(requestId) 248 ) 249 await Promise.all(fetches) 250 } catch (e) { 251 console.error(`failure requesting latest request states:`, e) 252 } finally { 253 loading.value.states = false 254 } 255 } 256 257 async function fetchPastRequests() { 258 // query past events 259 const blocksSorted = Object.values(blocks.value).sort( 260 (blkA, blkB) => blkB.number - blkA.number 261 ) 262 const lastBlockNumber = blocksSorted.length ? blocksSorted[0].number : null 263 264 if (lastBlockNumber) { 265 loading.value.recent = true 266 } else { 267 loading.value.past = true 268 } 269 270 await Promise.all(await fetchPastRequestsFrom(lastBlockNumber + 1)) 271 272 if (lastBlockNumber) { 273 loading.value.recent = false 274 } else { 275 loading.value.past = false 276 fetched.value.past = true 277 } 278 } 279 280 async function fetchRequestDetails(requestId) { 281 try { 282 // fetch request details if not previously fetched 283 const { request } = await getRequest(requestId) 284 285 // always fetch state 286 await getRequestState(requestId) 287 288 // always fetch slots - fire async but don't wait 289 console.log('fetching slots for request', requestId) 290 getSlots(requestId, request.ask.slots) 291 } catch (error) { 292 if ( 293 !error.message.includes('Unknown request') && 294 !error.message.includes('invalid BytesLike value') 295 ) { 296 console.error(`failed to fetch details for request ${requestId}: ${error}`) 297 } 298 throw error 299 } 300 } 301 302 function updateRequestState(requestId, newState) { 303 if (!exists(requestId)) { 304 throw new RequestNotFoundError(requestId, `Request not found`) 305 } 306 requests.value[requestId].state = newState 307 } 308 309 function updateRequestFinishedId(requestId, newRequestFinishedId) { 310 if (!exists(requestId)) { 311 throw new RequestNotFoundError(requestId, `Request not found`) 312 } 313 requests.value[requestId].state = newRequestFinishedId 314 } 315 316 function updateRequestSlotState(requestId, slotIdx, newState) { 317 if (!exists(requestId)) { 318 throw new RequestNotFoundError(requestId, `Request not found`) 319 } 320 let { slots, ...rest } = requests.value[requestId] 321 slots = slots.map((slot) => { 322 if (slot.slotIdx == slotIdx) { 323 slot.state = newState 324 } 325 return slot 326 }) 327 requests.value[requestId] = { slots, ...rest } 328 } 329 330 function updateRequestSlotProvider(requestId, slotIdx, provider) { 331 if (!exists(requestId)) { 332 throw new RequestNotFoundError(requestId, `Request not found`) 333 } 334 let { slots, ...rest } = requests.value[requestId] 335 slots = slots.map((slot) => { 336 if (slot.slotIdx == slotIdx) { 337 slot.provider = provider 338 } 339 return slot 340 }) 341 requests.value[requestId] = { slots, ...rest } 342 } 343 344 function waitForRequestFinished(requestId, ask, expiry, state, requestedAt) { 345 if (!['Fulfilled', 'New'].includes(state)) { 346 return null 347 } 348 // set request state to finished at the end of the request -- there's no 349 // other way to know when a request finishes 350 let { endsAt } = timestampsFor(ask, expiry, requestedAt) 351 let msFromNow = endsAt * 1000 - Date.now() // time remaining until finish, in ms 352 353 return setTimeout(async () => { 354 try { 355 // the state may actually have been cancelled, but RequestCancelled 356 // may not have fired yet, so get the updated state 357 await getRequestState(requestId) 358 updateRequestFinishedId(requestId, null) 359 } catch (error) { 360 if (error instanceof RequestNotFoundError) { 361 await fetchRequestDetails(requestId) 362 } else { 363 throw error 364 } 365 } 366 let blockNumber = await ethProvider.getBlockNumber() 367 events.add({ 368 event: StorageEvent.RequestFinished, 369 blockNumber, 370 requestId, 371 state: RequestState.Finished, 372 timestamp: Date.now() / 1000, 373 moderated: requests.value[requestId]?.moderated 374 }) 375 }, msFromNow + 10000) // add additional 10s to ensure state has changed 376 } 377 378 function cancelWaitForRequestFinished(requestId) { 379 if (!exists(requestId)) { 380 throw new RequestNotFoundError(requestId, `Request not found`) 381 } 382 let { requestFinishedId } = requests.value[requestId] 383 if (requestFinishedId) { 384 clearTimeout(requestFinishedId) 385 } 386 } 387 388 return { 389 requests, 390 blocks, 391 addFromEvent, 392 exists, 393 getBlock, 394 getRequest, 395 fetchPastRequests, 396 refetchRequestStates, 397 fetchRequestDetails, 398 updateRequestState, 399 updateRequestSlotState, 400 updateRequestSlotProvider, 401 updateRequestFinishedId, 402 cancelWaitForRequestFinished, 403 loading, 404 fetched, 405 RequestNotFoundError 406 } 407 }, 408 { 409 persist: { 410 serializer, 411 crossTabSync: true 412 // paths: ['requests', 'blocks', 'fetched', 'loading'] 413 } 414 } 415 )