add moderation controls

Moderation view of all requests: /moderate

Allows request details page to be opened in two separate tabs: one with moderation enabled, and the other without (for the projector). Moderating the image has its state synced to the other (non-moderated) tab
This commit is contained in:
Eric 2024-06-19 16:43:41 +10:00
parent 3fb5a59069
commit 12702d4e59
No known key found for this signature in database
12 changed files with 506 additions and 312 deletions

View File

@ -15,6 +15,7 @@
"ethers": "^6.12.1",
"flowbite": "^2.3.0",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"vue-unique-id": "^3.2.1"

View File

@ -1,5 +1,5 @@
<script setup>
import { onBeforeMount, onMounted, ref } from 'vue'
import { onBeforeMount, onMounted, ref, onScopeDispose } from 'vue'
import { useRequestsStore } from '@/stores/requests'
import { RouterView } from 'vue-router'
import Balance from '@/components/Balance.vue'
@ -116,6 +116,18 @@ onMounted(async () => {
onSlotFreed,
onSlotFilled
)
function handleStorageEvent(event) {
if(event.key === 'requests') {
requestsStore.$hydrate()
}
}
window.addEventListener('storage', handleStorageEvent)
onScopeDispose(() => {
window.removeEventListener('storage', handleStorageEvent)
})
})
</script>

View File

@ -16,9 +16,20 @@ const props = defineProps({
type: Boolean,
default: false
},
alt: String
alt: String,
moderated: {
type: [String],
default() {
return 'pending'
},
validator(value, props) {
// The value must match one of these strings
return ['pending', 'approved', 'banned'].includes(value)
}
}
})
const hidden = computed(() => props.cid === undefined)
const blurred = computed(() => ['pending', 'banned'].includes(props.moderated))
onMounted(async () => {
if (hidden.value) {
@ -66,6 +77,19 @@ onMounted(async () => {
<span class="sr-only">{{ error }}</span>
</div>
<img v-else-if="imgSrc" :src="imgSrc" class="rounded-full" :alt="props.alt" />
<img
v-else-if="imgSrc"
:src="imgSrc"
class="rounded-lg"
:alt="props.alt"
:class="{ 'blur-xxl': blurred }"
/>
</div>
</template>
<style scoped>
.blur-xxl {
--tw-blur: blur(76px);
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale)
var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
</style>

View File

@ -1,7 +1,9 @@
<script setup>
import { onMounted, computed } from 'vue'
import { onMounted, onUnmounted, computed } from 'vue'
import { initTooltips } from 'flowbite'
import { getStateColour, price } from '@/utils/requests'
import { storeToRefs } from 'pinia'
import { useRequestsStore } from '@/stores/requests'
import { getStateColour, moderatedState, price } from '@/utils/requests'
import { autoPluralize } from '@/utils/strings'
import CodexImage from '@/components/CodexImage.vue'
@ -10,14 +12,15 @@ import RelativeTime from '@/components/RelativeTime.vue'
import ShortenValue from '@/components/ShortenValue.vue'
import Slots from './Slots.vue'
const request = defineModel()
const props = defineProps({
requestId: {
type: String,
required: true
},
request: {
type: Object,
required: true
enableModeration: {
type: Boolean,
default: false
}
})
@ -25,20 +28,45 @@ onMounted(() => {
initTooltips()
})
const totalPrice = computed(() => price(props.request))
const maxSlotLoss = computed(() => autoPluralize(props.request.ask.maxSlotLoss, 'slot'))
const slots = computed(() => autoPluralize(props.request.ask.slots, 'slot'))
const stateColour = computed(() => getStateColour(props.request.state))
const totalPrice = computed(() => price(request.value))
const maxSlotLoss = computed(() => autoPluralize(request.value.ask.maxSlotLoss, 'slot'))
const slots = computed(() => autoPluralize(request.value.ask.slots, 'slot'))
const stateColour = computed(() => getStateColour(request.value.state))
</script>
<template>
<div class="flex flex-wrap">
<CodexImage
class="flex-initial mx-auto my-8 lg:my-16 min-w-sm max-w-md w-full rounded"
:cid="props.request.content.cid"
:cid="request.content.cid"
:local-only="!['New', 'Fulfilled'].includes(request.state)"
:moderated="enableModeration ? 'approved' : request.moderated"
></CodexImage>
<div class="py-8 px-4 ml-4 max-w-2xl lg:py-16 flex-1">
<div v-if="enableModeration === true" class="mb-4 p-5 w-full border border-gray-300 rounded-lg b-1 bg-gray-100">
<label for="moderation" class="block mb-2 text-lg font-medium text-gray-900 dark:text-white"
>Moderation station</label
>
<div class="flex items-center justify-between space-x-4">
<div class="flex-1">
<select
id="moderation"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
v-model="request.moderated"
>
<option v-for="(value, key) in moderatedState" :value="key" :key="key">
{{ value.text }}
</option>
</select>
</div>
<StateIndicator
class="flex-none h-7 inline-block align-middle"
:text="moderatedState[request.moderated].text"
:color="moderatedState[request.moderated].color"
size="lg"
></StateIndicator>
</div>
</div>
<div class="flex justify-between items-center mb-2">
<h2 class="text-xl font-semibold leading-none text-gray-900 md:text-2xl dark:text-white">
Request <ShortenValue :value="requestId" :chars="8"></ShortenValue>
@ -51,7 +79,6 @@ const stateColour = computed(() => getStateColour(props.request.state))
{{ totalPrice }} CDX
</p>
<dl>
<!-- <dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Dataset CID</dt> -->
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
<RelativeTime :timestamp="new Date(request.requestedAt * 1000)"></RelativeTime>
</dd>
@ -79,9 +106,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 mx-auto"
>
<tbody>
<tr
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
>
@ -89,9 +114,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
</td>
<td class="px-6 py-2 font-light">{{ request.expiry }} seconds</td>
</tr>
<tr
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
>
@ -99,9 +122,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
</td>
<td class="px-6 py-2 font-light">{{ request.ask.duration }} seconds</td>
</tr>
<tr
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
>
@ -109,9 +130,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
</td>
<td class="px-6 py-2 font-light">{{ request.ask.slotSize }} bytes</td>
</tr>
<tr
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
>
@ -119,9 +138,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
</td>
<td class="px-6 py-2 font-light">{{ request.ask.proofProbability }}</td>
</tr>
<tr
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
>
@ -129,9 +146,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
</td>
<td class="px-6 py-2 font-light">{{ request.ask.reward }} CDX</td>
</tr>
<tr
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:tbg-gray-600 text-base"
>
<tr class="hover:bg-gray-50 dark:hover:tbg-gray-600 text-base">
<td
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
>
@ -139,9 +154,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
</td>
<td class="px-6 py-2 font-light">{{ request.ask.collateral }} CDX</td>
</tr>
<tr
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:tbg-gray-600 text-base"
>
<tr class="hover:bg-gray-50 dark:hover:tbg-gray-600 text-base">
<td
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
>
@ -149,9 +162,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
</td>
<td class="px-6 py-2 font-light">{{ slots }}</td>
</tr>
<tr
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
<td
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
>

View File

@ -7,17 +7,28 @@ import { useRequestsStore } from '@/stores/requests'
import StateIndicator from '@/components/StateIndicator.vue'
import RelativeTime from '@/components/RelativeTime.vue'
import ShortenValue from '@/components/ShortenValue.vue'
import { getStateColour } from '@/utils/requests'
import {
getStateColour,
moderatedState
} from '@/utils/requests'
const requestsStore = useRequestsStore()
const { requests } = storeToRefs(requestsStore)
const router = useRouter()
const requestsOrdered = computed(() => {
const sorted = [...requests.value.entries()].sort(
const sorted = [...Object.entries(requests.value)].sort(
([reqIdA, reqA], [reqIdB, reqB]) => reqB.requestedAt - reqA.requestedAt
)
return sorted
})
const props = defineProps({
enableModeration: {
type: Boolean,
default: false
}
})
onMounted(() => {
initTooltips()
})
@ -133,16 +144,17 @@ onMounted(() => {
>
<tr>
<th scope="col" class="px-6 py-3">RequestID</th>
<th v-if="enableModeration" scope="col" class="px-6 py-3">Moderated</th>
<th scope="col" class="px-6 py-3">State</th>
<th scope="col" class="px-6 py-3">Action</th>
</tr>
</thead>
<tbody>
<tr
v-for="([requestId, { requestedAt, state }], idx) in requestsOrdered"
v-for="([requestId, { requestedAt, moderated, state }], idx) in requestsOrdered"
:key="{ requestId }"
class="cursor-pointer bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-600"
@click="router.push(`/request/${requestId}`)"
class="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-600"
@click="router.push({ path: `/request/${requestId}`, query: { enableModeration } })"
>
<th
scope="row"
@ -157,6 +169,14 @@ onMounted(() => {
</div>
</div>
</th>
<td v-if="enableModeration" class="px-6 py-4">
<div class="flex items-center">
<StateIndicator
:color="moderatedState[moderated].color"
:text="moderatedState[moderated].text"
></StateIndicator>
</div>
</td>
<td class="px-6 py-4">
<div class="flex items-center">
<StateIndicator :text="state" :color="getStateColour(state)"></StateIndicator>

View File

@ -2,6 +2,7 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import uniqueId from '@/plugins/UniqueIdPlugin'
import App from '@/App.vue'
@ -15,7 +16,10 @@ import './index.css'
const app = createApp(App)
app.use(createPinia())
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
const environments = {
localhost: {

View File

@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import RequestsView from '../views/RequestsView.vue'
import RequestView from '../views/RequestView.vue'
import NotFoundView from '../views/NotFoundView.vue'
import ModerateView from '../views/ModerateView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -29,6 +30,11 @@ const router = createRouter({
path: '/404',
name: 'NotFound',
component: NotFoundView
},
{
path: '/moderate',
name: 'Moderate',
component: ModerateView
}
]
})

View File

@ -4,286 +4,370 @@ import { slotId } from '../utils/ids'
import { arrayToObject, requestState } from '@/utils/requests'
import { slotState } from '@/utils/slots'
export const useRequestsStore = defineStore('request', () => {
// let fetched = false
const marketplace = inject('marketplace')
const ethProvider = inject('ethProvider')
let {
StorageRequested,
RequestFulfilled,
RequestCancelled,
RequestFailed,
SlotFilled,
SlotFreed
} = marketplace.filters
const requests = ref(new Map()) // key: requestId, val: {request, state, slots: [{slotId, slotIdx, state}]}
// const slots = ref(new Map()) // key: slotId, val: {requestId, slotIdx, state}
// const blockNumbers = ref(new Set()) // includes blocks that had events
// const storageRequestedEvents = ref([]) // {blockNumber, requestId}
// const slotFilledEvents = ref([]) // {blockNumber, requestId, slotIdx, slotId}
// const slotFreedEvents = ref([]) // {blockNumber, requestId, slotIdx, slotId}
// const requestFulfilledEvents = ref([]) // {blockNumber, requestId}
// const requestCancelledEvents = ref([]) // {blockNumber, requestId}
// const requestFailedEvents = ref([]) // {blockNumber, requestId}
// const requestFinishedEvents = ref([]) // {blockNumber, requestId}
const loading = ref(false)
const blocks = ref(new Map())
// const request = computed(() => count.value * 2)
class RequestNotFoundError extends Error {
constructor(requestId, ...params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(...params)
// onStorageRequested => add request to requests ref, along with slots
// => add to storageRequestedEvents {blockNumber, requestId}
// => add blockNumber to blockNumbers
// onRequestFulfilled => update requests[requestId].state with marketplace.getRequestState(requestId)
// => add to requestStartedEvents {blockNumber, requestId}
// => add blockNumber to blockNumbers
// onRequestCancelled => update requests[requestId].state with marketplace.getRequestState(requestId)
// => add to requestCancelledEvents {blockNumber, requestId}
// => add blockNumber to blockNumbers
// onRequestFailed => update requests[requestId].state with marketplace.getRequestState(requestId)
// => add to requestFailedEvents {blockNumber, requestId}
// => add blockNumber to blockNumbers
// onRequestFinished => update requests[requestId].state with marketplace.getRequestState(requestId)
// => add to requestFinishedEvents {blockNumber, requestId}
// => add blockNumber to blockNumbers
// onSlotFilled => update request.slots[slotId].state with getSlotState
// => add to slotFilledEvents {blockNumber, slotId, slotIdx}
// => add blockNumber to blockNumbers
// onSlotFreed => update slots[slotId].state with getSlotState
// => add to slotFreedEvents {blockNumber, slotId, slotIdx}
// => add blockNumber to blockNumbers
const getRequestState = async (requestId) => {
let stateIdx = await marketplace.requestState(requestId)
return requestState[stateIdx]
}
const getSlotState = async (slotId) => {
let stateIdx = await marketplace.slotState(slotId)
return slotState[stateIdx]
}
const getSlots = async (requestId, numSlots) => {
console.log(`fetching ${numSlots} slots`)
let start = Date.now()
let slots = []
for (let slotIdx = 0; slotIdx < numSlots; slotIdx++) {
let id = slotId(requestId, slotIdx)
const startSlotState = Date.now()
let state = await getSlotState(id)
console.log(`fetched slot state in ${(Date.now() - startSlotState) / 1000}s`)
const startGetHost = Date.now()
let provider = await marketplace.getHost(id)
console.log(`fetched slot provider in ${(Date.now() - startGetHost) / 1000}s`)
slots.push({ slotId: id, slotIdx, state, provider })
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, RequestNotFoundError)
}
console.log(`fetched ${numSlots} slots in ${(Date.now() - start) / 1000}s`)
return slots
// blockNumbers.value.add(blockNumber)
}
const getBlock = async (blockHash) => {
if (blocks.value.has(blockHash)) {
return blocks.value.get(blockHash)
} else {
let block = await ethProvider.getBlock(blockHash)
blocks.value.set(blockHash, block)
return block
this.name = 'RequestNotFoundError'
// Custom debugging information
this.requestId = requestId
}
}
export const useRequestsStore = defineStore(
'requests',
() => {
// let fetched = false
const marketplace = inject('marketplace')
const ethProvider = inject('ethProvider')
let {
StorageRequested,
RequestFulfilled,
RequestCancelled,
RequestFailed,
SlotFilled,
SlotFreed
} = marketplace.filters
const requests = ref({}) // key: requestId, val: {request, state, slots: [{slotId, slotIdx, state}]}
// const slots = ref(new Map()) // key: slotId, val: {requestId, slotIdx, state}
// const blockNumbers = ref(new Set()) // includes blocks that had events
// const storageRequestedEvents = ref([]) // {blockNumber, requestId}
// const slotFilledEvents = ref([]) // {blockNumber, requestId, slotIdx, slotId}
// const slotFreedEvents = ref([]) // {blockNumber, requestId, slotIdx, slotId}
// const requestFulfilledEvents = ref([]) // {blockNumber, requestId}
// const requestCancelledEvents = ref([]) // {blockNumber, requestId}
// const requestFailedEvents = ref([]) // {blockNumber, requestId}
// const requestFinishedEvents = ref([]) // {blockNumber, requestId}
const loading = ref(false)
const fetched = ref(false) // indicates if past events were fetched
const blocks = ref({})
// const request = computed(() => count.value * 2)
// onStorageRequested => add request to requests ref, along with slots
// => add to storageRequestedEvents {blockNumber, requestId}
// => add blockNumber to blockNumbers
// onRequestFulfilled => update requests[requestId].state with marketplace.getRequestState(requestId)
// => add to requestStartedEvents {blockNumber, requestId}
// => add blockNumber to blockNumbers
// onRequestCancelled => update requests[requestId].state with marketplace.getRequestState(requestId)
// => add to requestCancelledEvents {blockNumber, requestId}
// => add blockNumber to blockNumbers
// onRequestFailed => update requests[requestId].state with marketplace.getRequestState(requestId)
// => add to requestFailedEvents {blockNumber, requestId}
// => add blockNumber to blockNumbers
// onRequestFinished => update requests[requestId].state with marketplace.getRequestState(requestId)
// => add to requestFinishedEvents {blockNumber, requestId}
// => add blockNumber to blockNumbers
// onSlotFilled => update request.slots[slotId].state with getSlotState
// => add to slotFilledEvents {blockNumber, slotId, slotIdx}
// => add blockNumber to blockNumbers
// onSlotFreed => update slots[slotId].state with getSlotState
// => add to slotFreedEvents {blockNumber, slotId, slotIdx}
// => add blockNumber to blockNumbers
const getRequestState = async (requestId) => {
let stateIdx = await marketplace.requestState(requestId)
return requestState[stateIdx]
}
}
async function addRequest(requestId, blockHash) {
let state = await getRequestState(requestId)
let block = await getBlock(blockHash)
let reqExisting = requests.value.get(requestId) // just in case it already exists
let request = {
...reqExisting,
state,
requestedAt: block.timestamp,
requestFinishedId: null,
detailsFetched: false
const getSlotState = async (slotId) => {
let stateIdx = await marketplace.slotState(slotId)
return slotState[stateIdx]
}
requests.value.set(requestId, request)
return request
}
async function fetchPastRequests() {
// query past events
console.log('fetching past requests')
loading.value = true
try {
let events = await marketplace.queryFilter(StorageRequested)
console.log('got ', events.length, ' StorageRequested events')
events.forEach(async (event, i) => {
console.log('getting details for StorageRequested event ', i)
let start = Date.now()
let { requestId, ask, expiry } = event.args
let { blockHash, blockNumber } = event
await addRequest(requestId, blockHash)
console.log(`got details for ${i} in ${(Date.now() - start) / 1000} seconds`)
if (i === events.length - 1) {
const getSlots = async (requestId, numSlots) => {
console.log(`fetching ${numSlots} slots`)
let start = Date.now()
let slots = []
for (let slotIdx = 0; slotIdx < numSlots; slotIdx++) {
let id = slotId(requestId, slotIdx)
const startSlotState = Date.now()
let state = await getSlotState(id)
console.log(`fetched slot state in ${(Date.now() - startSlotState) / 1000}s`)
const startGetHost = Date.now()
let provider = await marketplace.getHost(id)
console.log(`fetched slot provider in ${(Date.now() - startGetHost) / 1000}s`)
slots.push({ slotId: id, slotIdx, state, provider })
}
console.log(`fetched ${numSlots} slots in ${(Date.now() - start) / 1000}s`)
return slots
// blockNumbers.value.add(blockNumber)
}
const getBlock = async (blockHash) => {
if (Object.keys(blocks.value).includes(blockHash)) {
return blocks.value[blockHash]
} else {
let block = await ethProvider.getBlock(blockHash)
blocks.value[blockHash] = block
return block
}
}
async function addRequest(requestId, blockHash) {
let state = await getRequestState(requestId)
let block = await getBlock(blockHash)
let reqExisting = requests.value[requestId] || {} // just in case it already exists
let request = {
...reqExisting,
state,
requestedAt: block.timestamp,
requestFinishedId: null,
detailsFetched: false,
moderated: 'pending'
}
requests.value[requestId] = request
return request
}
async function fetchPastRequests() {
// query past events
if (fetched.value) {
console.log('skipping fetching past requests, already fetched')
return
}
console.log('fetching past requests')
loading.value = true
try {
let events = await marketplace.queryFilter(StorageRequested)
console.log('got ', events.length, ' StorageRequested events')
events.forEach(async (event, i) => {
console.log('getting details for StorageRequested event ', i)
let start = Date.now()
let { requestId, ask, expiry } = event.args
let { blockHash, blockNumber } = event
await addRequest(requestId, blockHash)
console.log(`got details for ${i} in ${(Date.now() - start) / 1000} seconds`)
if (i === events.length - 1) {
loading.value = false
fetched.value = true
}
})
if (events.length === 0) {
loading.value = false
fetched.value = true
}
})
if (events.length === 0) {
} catch (error) {
console.error(`failed to load past contract events: ${error.message}`)
}
}
async function fetchRequest(requestId) {
let start = Date.now()
console.log('fetching request ', requestId)
const preFetched = requests.value[requestId] || {}
if (preFetched?.detailsFetched) {
return
}
loading.value = true
try {
let arrRequest = await marketplace.getRequest(requestId)
let request = arrayToObject(arrRequest)
let slots = await getSlots(requestId, request.ask.slots)
const reqExisting = requests.value[requestId] || {}
requests.value[requestId] = {
...reqExisting, // state, requestedAt, requestFinishedId (null)
...request,
slots,
detailsFetched: true
}
} catch (error) {
console.error(`failed to load slots for request ${requestId}: ${error}`)
throw error
} finally {
console.log(`fetched request in ${(Date.now() - start) / 1000}s`)
loading.value = false
}
} catch (error) {
console.error(`failed to load past contract events: ${error.message}`)
}
}
async function fetchRequest(requestId) {
let start = Date.now()
console.log('fetching request ', requestId)
const preFetched = requests.value.get(requestId)
if (preFetched?.detailsFetched) {
return
function updateRequestState(requestId, newState) {
if (!Object.keys(requests.value).includes(requestId)) {
throw new RequestNotFoundError(requestId, `Request not found`)
}
let { state, ...rest } = requests.value[requestId]
state = newState
requests.value[requestId] = { state, ...rest }
}
loading.value = true
try {
let arrRequest = await marketplace.getRequest(requestId)
let request = arrayToObject(arrRequest)
let slots = await getSlots(requestId, request.ask.slots)
const reqExisting = requests.value.get(requestId)
requests.value.set(requestId, {
...reqExisting, // state, requestedAt, requestFinishedId (null)
...request,
slots,
detailsFetched: true
function updateRequestFinishedId(requestId, newRequestFinishedId) {
if (!Object.keys(requests.value).includes(requestId)) {
throw new RequestNotFoundError(requestId, `Request not found`)
}
let { requestFinishedId, ...rest } = requests.value[requestId]
requestFinishedId = newRequestFinishedId
requests.value[requestId] = { requestFinishedId, ...rest }
}
function updateRequestSlotState(requestId, slotIdx, newState) {
if (!Object.keys(requests.value).includes(requestId)) {
throw new RequestNotFoundError(requestId, `Request not found`)
}
let { slots, ...rest } = requests.value[requestId]
slots = slots.map((slot) => {
if (slot.slotIdx == slotIdx) {
slot.state = newState
}
})
} catch (error) {
console.error(`failed to load slots for request ${requestId}: ${error}`)
throw error
} finally {
console.log(`fetched request in ${(Date.now() - start) / 1000}s`)
loading.value = false
requests.value[requestId] = { slots, ...rest }
}
function waitForRequestFinished(requestId, duration, onRequestFinished) {
return setTimeout(async () => {
try {
updateRequestState(requestId, 'Finished')
updateRequestFinishedId(requestId, null)
} catch (error) {
if (error instanceof RequestNotFoundError) {
await fetchRequest(requestId)
}
}
if (onRequestFinished) {
let blockNumber = await ethProvider.getBlockNumber()
onRequestFinished(blockNumber, requestId)
}
}, duration)
}
function cancelWaitForRequestFinished(requestId) {
if (!Object.keys(requests.value).includes(requestId)) {
throw new RequestNotFoundError(requestId, `Request not found`)
}
let { requestFinishedId } = requests.value[requestId]
if (requestFinishedId) {
clearTimeout(requestFinishedId)
}
}
async function listenForNewEvents(
onStorageRequested,
onRequestFulfilled,
onRequestCancelled,
onRequestFailed,
onRequestFinished,
onSlotFreed,
onSlotFilled
) {
marketplace.on(StorageRequested, async (requestId, ask, expiry, event) => {
let { blockNumber, blockHash } = event.log
const request = addRequest(requestId, blockHash)
// callback
if (onStorageRequested) {
onStorageRequested(blockNumber, requestId, request.state)
}
})
marketplace.on(RequestFulfilled, async (requestId, event) => {
let requestOnChain = await marketplace.getRequest(requestId)
let ask = requestOnChain[1]
let duration = ask[1]
// set request state to finished at the end of the request -- there's no
// other way to know when a request finishes
console.log('request ', requestOnChain)
let requestFinishedId = waitForRequestFinished(requestId, duration, onRequestFinished)
updateRequestState(requestId, 'Fulfilled')
updateRequestFinishedId(requestId, requestFinishedId)
let { blockNumber } = event.log
if (onRequestFulfilled) {
onRequestFulfilled(blockNumber, requestId)
}
})
marketplace.on(RequestCancelled, async (requestId, event) => {
try {
updateRequestState(requestId, 'Cancelled')
} catch (error) {
if (error instanceof RequestNotFoundError) {
await fetchRequest(requestId)
}
}
let { blockNumber } = event.log
if (onRequestCancelled) {
onRequestCancelled(blockNumber, requestId)
}
})
marketplace.on(RequestFailed, async (requestId, event) => {
try {
updateRequestState(requestId, 'Failed')
cancelWaitForRequestFinished(requestId)
} catch (error) {
if (error instanceof RequestNotFoundError) {
await fetchRequest(requestId)
}
}
let { blockNumber } = event.log
if (onRequestFailed) {
onRequestFailed(blockNumber, requestId)
}
})
marketplace.on(SlotFreed, async (requestId, slotIdx, event) => {
try {
updateRequestSlotState(requestId, slotIdx, 'Freed')
} catch (error) {
if (error instanceof RequestNotFoundError) {
await fetchRequest(requestId)
}
}
let { blockNumber } = event.log
if (onSlotFreed) {
onSlotFreed(blockNumber, requestId, slotIdx)
}
})
marketplace.on(SlotFilled, async (requestId, slotIdx, event) => {
try {
updateRequestSlotState(requestId, slotIdx, 'Filled')
} catch (error) {
if (error instanceof RequestNotFoundError) {
await fetchRequest(requestId)
}
}
let { blockNumber } = event.log
if (onSlotFilled) {
onSlotFilled(blockNumber, requestId, slotIdx)
}
})
}
return {
requests,
// slots,
// blockNumbers,
// storageRequestedEvents,
// slotFilledEvents,
// slotFreedEvents,
// requestStartedEvents,
// requestCancelledEvents,
// requestFailedEvents,
// requestFinishedEvents,
fetchPastRequests,
fetchRequest,
listenForNewEvents,
loading,
fetched
}
},
{
persist: {
serializer: {
serialize: (state) => {
return JSON.stringify(state, (_, v) => (typeof v === 'bigint' ? v.toString() : v))
},
deserialize: (serialized) => {
// TODO: deserialize bigints properly
return JSON.parse(serialized)
}
}
}
}
function updateRequestState(requestId, newState) {
let { state, ...rest } = requests.value.get(requestId)
state = newState
requests.value.set(requestId, { state, ...rest })
}
function updateRequestFinishedId(requestId, newRequestFinishedId) {
let { requestFinishedId, ...rest } = requests.value.get(requestId)
requestFinishedId = newRequestFinishedId
requests.value.set(requestId, { requestFinishedId, ...rest })
}
function updateRequestSlotState(requestId, slotIdx, newState) {
let { slots, ...rest } = requests.value.get(requestId)
slots = slots.map((slot) => {
if (slot.slotIdx == slotIdx) {
slot.state = newState
}
})
requests.value.set(requestId, { slots, ...rest })
}
function waitForRequestFinished(requestId, duration, onRequestFinished) {
return setTimeout(async () => {
updateRequestState(requestId, 'Finished')
updateRequestFinishedId(requestId, null)
if (onRequestFinished) {
let blockNumber = await ethProvider.getBlockNumber()
onRequestFinished(blockNumber, requestId)
}
}, duration)
}
function cancelWaitForRequestFinished(requestId) {
let { requestFinishedId } = requests.value.get(requestId)
if (requestFinishedId) {
clearTimeout(requestFinishedId)
}
}
async function listenForNewEvents(
onStorageRequested,
onRequestFulfilled,
onRequestCancelled,
onRequestFailed,
onRequestFinished,
onSlotFreed,
onSlotFilled
) {
marketplace.on(StorageRequested, async (requestId, ask, expiry, event) => {
let { blockNumber, blockHash } = event.log
const request = addRequest(requestId, blockHash)
// callback
if (onStorageRequested) {
onStorageRequested(blockNumber, requestId, request.state)
}
})
marketplace.on(RequestFulfilled, async (requestId, event) => {
let requestOnChain = await marketplace.getRequest(requestId)
let ask = requestOnChain[1]
let duration = ask[1]
// set request state to finished at the end of the request -- there's no
// other way to know when a request finishes
console.log('request ', requestOnChain)
let requestFinishedId = waitForRequestFinished(requestId, duration, onRequestFinished)
updateRequestState(requestId, 'Fulfilled')
updateRequestFinishedId(requestId, requestFinishedId)
let { blockNumber } = event.log
if (onRequestFulfilled) {
onRequestFulfilled(blockNumber, requestId)
}
})
marketplace.on(RequestCancelled, async (requestId, event) => {
updateRequestState(requestId, 'Cancelled')
let { blockNumber } = event.log
if (onRequestCancelled) {
onRequestCancelled(blockNumber, requestId)
}
})
marketplace.on(RequestFailed, async (requestId, event) => {
updateRequestState(requestId, 'Failed')
cancelWaitForRequestFinished(requestId)
let { blockNumber } = event.log
if (onRequestFailed) {
onRequestFailed(blockNumber, requestId)
}
})
marketplace.on(SlotFreed, async (requestId, slotIdx, event) => {
updateRequestSlotState(requestId, slotIdx, 'Freed')
let { blockNumber } = event.log
if (onSlotFreed) {
onSlotFreed(blockNumber, requestId, slotIdx)
}
})
marketplace.on(SlotFilled, async (requestId, slotIdx, event) => {
updateRequestSlotState(requestId, slotIdx, 'Filled')
let { blockNumber } = event.log
if (onSlotFilled) {
onSlotFilled(blockNumber, requestId, slotIdx)
}
})
}
return {
requests,
// slots,
// blockNumbers,
// storageRequestedEvents,
// slotFilledEvents,
// slotFreedEvents,
// requestStartedEvents,
// requestCancelledEvents,
// requestFailedEvents,
// requestFinishedEvents,
fetchPastRequests,
fetchRequest,
listenForNewEvents,
loading
}
})
)

View File

@ -41,6 +41,12 @@ export function getStateColour(state) {
}
}
export const moderatedState = {
pending: { text: 'Pending 🤷‍♂️', color: 'gray' },
approved: { text: 'Approved ✅', color: 'green' },
banned: { text: 'NSFW! 🫣', color: 'red' }
}
export const requestState = [
'New', // [default] waiting to fill slots
'Fulfilled', // all slots filled, accepting regular proofs

View File

@ -0,0 +1,16 @@
<script setup>
import { storeToRefs } from 'pinia'
import { useRequestsStore } from '@/stores/requests'
import StorageRequests from '@/components/StorageRequests.vue'
import SkeletonLoading from '@/components/SkeletonLoading.vue'
const requestsStore = useRequestsStore()
const { loading } = storeToRefs(requestsStore)
</script>
<template>
<div>
<SkeletonLoading v-if="loading" type="text" />
<StorageRequests :enable-moderation="true" v-else />
</div>
</template>

View File

@ -20,7 +20,7 @@ async function fetch(requestId) {
router.push({ name: 'NotFound' })
}
}
request.value = requests.value.get(requestId)
request.value = requests.value[requestId]
}
const hasRequest = computed(() => {
@ -47,6 +47,11 @@ if (loading.value) {
<template>
<div>
<SkeletonLoading v-if="loading" type="image" />
<StorageRequest v-else-if="hasRequest" :requestId="route.params.requestId" :request="request" />
<StorageRequest
v-else-if="hasRequest"
:requestId="route.params.requestId"
v-model="request"
:enableModeration="route.query.enableModeration === 'true'"
/>
</div>
</template>

View File

@ -2076,6 +2076,11 @@ pify@^2.3.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
pinia-plugin-persistedstate@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz#66780602aecd6c7b152dd7e3ddc249a1f7a13fe5"
integrity sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==
pinia@^2.1.7:
version "2.1.7"
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.1.7.tgz#4cf5420d9324ca00b7b4984d3fbf693222115bbc"