/ src / stores / requests.js
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  )