Skip to content

Commit 15ecdff

Browse files
committed
docs: add light-curate instructions
1 parent 116e523 commit 15ecdff

File tree

1 file changed

+341
-5
lines changed

1 file changed

+341
-5
lines changed

developer/curate/light-curate.md

Lines changed: 341 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,348 @@ Deployments:
2424

2525
- Kovan: `0x2dba4b729cb5f73bf85e7012ea99aa477a210dd6`
2626

27-
> Note: If you are using react, you can take the hook we built [here](https://github.com/kleros/light-gtcr-example/blob/master/src/hooks/tcr-view.js) or use it as an example.
28-
29-
Copy the contract's ABI from [here](../../.gitbook/assets/light-gtcr-view-abi.json).
30-
31-
Using `ethers`, instantiate the contract.
27+
> Note: If you are using react, you can take the hook we built [here](https://github.com/kleros/gtcr/blob/5e313ced24f5e3fc3a54f812e07fb1f86a6b2621/src/hooks/tcr-view.js) or use it as an example.
3228
3329
### Item Submission.
3430

3531
With light Curate, item submission consists of first uploading the item to IPFS and then submitting a transaction with the required deposit.
32+
33+
Since we use `@graphprotocol/graph-ts` we must submit items to its ipfs endpoint until they allow custom endpoints. In addition, we also upload to kleros ipfs node.
34+
35+
> In addition to Kleros' and The Graph's, we strongly advise pin the data to ipfs nodes you control as well. Update the provided below for this.
36+
37+
Full example [here](https://github.com/kleros/gtcr/blob/5e313ced24f5e3fc3a54f812e07fb1f86a6b2621/src/utils/ipfs-publish.js)
38+
39+
```
40+
REACT_APP_IPFS_GATEWAY=https://ipfs.kleros.io
41+
REACT_APP_HOSTED_GRAPH_IPFS_ENDPOINT=https://api.thegraph.com/ipfs
42+
```
43+
44+
#### Upload and Transaction
45+
46+
```
47+
// ipfs-publish.js
48+
49+
import deepEqual from 'fast-deep-equal/es6'
50+
51+
const mirroredExtensions = ['.json']
52+
53+
/**
54+
* Send file to IPFS network.
55+
* @param {string} fileName - The name that will be used to store the file. This is useful to preserve extension type.
56+
* @param {ArrayBuffer} data - The raw data from the file to upload.
57+
* @returns {object} ipfs response. Should include the hash and path of the stored item.
58+
*/
59+
export default async function ipfsPublish(fileName, data) {
60+
if (!mirroredExtensions.some(ext => fileName.endsWith(ext)))
61+
return publishToKlerosNode(fileName, data)
62+
63+
const [klerosResult, theGraphResult] = await Promise.all([
64+
publishToKlerosNode(fileName, data),
65+
publishToTheGraphNode(fileName, data),
66+
// Pin to your own ipfs node here as well.
67+
])
68+
69+
if (!deepEqual(klerosResult, theGraphResult)) {
70+
console.warn('IPFS upload result is different:', {
71+
kleros: klerosResult,
72+
theGraph: theGraphResult
73+
})
74+
throw new Error('IPFS upload result is different.')
75+
}
76+
77+
return klerosResult
78+
}
79+
80+
/**
81+
* Send file to IPFS network via the Kleros IPFS node
82+
* @param {string} fileName - The name that will be used to store the file. This is useful to preserve extension type.
83+
* @param {ArrayBuffer} data - The raw data from the file to upload.
84+
* @returns {object} ipfs response. Should include the hash and path of the stored item.
85+
*/
86+
async function publishToKlerosNode(fileName, data) {
87+
const buffer = await Buffer.from(data)
88+
const url = `${process.env.REACT_APP_IPFS_GATEWAY}/add`
89+
90+
const response = await fetch(url, {
91+
method: 'POST',
92+
body: JSON.stringify({
93+
fileName,
94+
buffer
95+
}),
96+
headers: {
97+
'content-type': 'application/json'
98+
}
99+
})
100+
101+
const body = await response.json()
102+
103+
return body.data
104+
}
105+
106+
/**
107+
* Send file to IPFS network via The Graph hosted IPFS node
108+
* @param {string} fileName - The name that will be used to store the file. This is useful to preserve extension type.
109+
* @param {ArrayBuffer} data - The raw data from the file to upload.
110+
* @returns {object} ipfs response. Should include the hash and path of the stored item.
111+
*/
112+
async function publishToTheGraphNode(fileName, data) {
113+
const url = `${process.env.REACT_APP_HOSTED_GRAPH_IPFS_ENDPOINT}/api/v0/add?wrap-with-directory=true`
114+
115+
const payload = new FormData()
116+
payload.append('file', new Blob([data]), fileName)
117+
118+
const response = await fetch(url, {
119+
method: 'POST',
120+
body: payload
121+
})
122+
123+
const result = await jsonStreamToPromise(response.body)
124+
125+
return result.map(({ Name, Hash }) => ({
126+
hash: Hash,
127+
path: `/${Name}`
128+
}))
129+
}
130+
131+
/**
132+
* Accumulates a JSON stream body into an array of JSON objects.
133+
* @param {ReadableStream} stream The stream to read from.
134+
* @returns {Promise<any>} An array of all JSON objects emitted by the stream.
135+
*/
136+
async function jsonStreamToPromise(stream) {
137+
const reader = stream.getReader()
138+
const decoder = new TextDecoder('utf-8')
139+
140+
const deferred = {
141+
resolve: undefined,
142+
reject: undefined
143+
}
144+
145+
const result = new Promise((resolve, reject) => {
146+
deferred.resolve = resolve
147+
deferred.reject = reject
148+
})
149+
150+
const acc = []
151+
const start = async () => {
152+
reader
153+
.read()
154+
.then(({ done, value }) => {
155+
if (done) return deferred.resolve(acc)
156+
157+
// Each `read` can produce one or more lines...
158+
const lines = decoder.decode(value).split(/\n/)
159+
const objects = lines
160+
.filter(line => line.trim() !== '')
161+
.map(line => JSON.parse(line))
162+
acc.push(...objects)
163+
164+
return start()
165+
})
166+
.catch(err => deferred.reject(err))
167+
}
168+
169+
start()
170+
171+
return result
172+
}
173+
174+
```
175+
176+
The JSON file for the object is composed of the its metadata and fields.
177+
178+
- Metadata (columns): An array describing each of the items columns (what's its type, name, description, etc.)
179+
- Values (values): An object mapping the column name to the value.
180+
181+
The metadata is available inside the meta evidence file, which is returned by the useTCRView hook.
182+
The Values are input by the user.
183+
184+
Example of columns used by the TCR at
185+
186+
```
187+
[
188+
{
189+
"label": "Logo",
190+
"description": "The token's logo.",
191+
"type": "image",
192+
"isIdentifier": false
193+
},
194+
{
195+
"label": "Name",
196+
"description": "The token name.",
197+
"type": "text",
198+
"isIdentifier": true
199+
},
200+
{
201+
"label": "Ticker",
202+
"description": "The token ticker.",
203+
"type": "text",
204+
"isIdentifier": true
205+
},
206+
{
207+
"label": "Address",
208+
"description": "The token address.",
209+
"type": "address",
210+
"isIdentifier": true
211+
},
212+
{
213+
"label": "Chain ID",
214+
"description": "The ID of the chain the token contract was deployed",
215+
"type": "number"
216+
},
217+
{
218+
"label": "Decimals",
219+
"description": "The number of decimal places.",
220+
"type": "number"
221+
}
222+
]
223+
```
224+
225+
And an example of values. Note that it is required for the keys to match the column names in the columns object.
226+
227+
```
228+
{
229+
"Logo": "/ipfs/QmT4vij3PrGZEQ1zarTrPmkqQWggRQN6VEpewSHJXbkeXh/pnk-logo.png",
230+
"Name": "Pinakion",
231+
"Ticker": "PNK",
232+
"Address": "0x93ED3FBe21207Ec2E8f2d3c3de6e058Cb73Bc04d",
233+
"Chain ID": "1",
234+
"Decimals": "18"
235+
}
236+
```
237+
238+
With this in hand we can submit the item.
239+
240+
```
241+
const gtcr = new ethers.Contract(tcrAddress, _gtcr, signer)
242+
const enc = new TextEncoder()
243+
const fileData = enc.encode(JSON.stringify({ columns, values }))
244+
const ipfsEvidenceObject = await ipfsPublish('item.json', fileData)
245+
const ipfsEvidencePath = `/ipfs/${ipfsEvidenceObject[1].hash +
246+
ipfsEvidenceObject[0].path}`
247+
248+
// Request signature and submit.
249+
const tx = await gtcr.addItem(ipfsEvidencePath, {
250+
value: submissionDeposit
251+
})
252+
```
253+
254+
### Fetching Items
255+
256+
> We break down this section into two as list views and details view have different requirements.
257+
258+
Fetchin items is best done via the subgraph we provide. If you deployed an list using the factory, it already has a subgraph deployed and available (here)[https://thegraph.com/explorer/subgraph/kleros/light-curate-kovan].
259+
260+
#### List
261+
262+
Whenever we want to fetch items, or a specific item, we must pass the TCR address to the subgraph.
263+
264+
See (this react example)[https://github.com/kleros/gtcr/blob/5e313ced24f5e3fc3a54f812e07fb1f86a6b2621/src/pages/items/index.js] for more details.
265+
266+
A standard query for the first page of a given list, ordered by the most recent requests, looks like this.
267+
268+
```
269+
const ITEMS_PER_PAGE = 40
270+
const orderDirection = 'asc'
271+
const page = 1
272+
const itemsWhere = `{ registry: "${tcrAddress.toLowerCase()}" }`
273+
const GTCR_SUBGRAPH_URL='https://api.thegraph.com/subgraphs/name/kleros/light-curate-kovan'
274+
const query = {
275+
query: `
276+
{
277+
items(
278+
skip: ${(Number(page) - 1) * ITEMS_PER_PAGE}
279+
first: ${ITEMS_PER_PAGE}
280+
orderDirection: ${orderDirection}
281+
orderBy: latestRequestSubmissionTime
282+
where: ${itemsWhere}
283+
) {
284+
itemID
285+
status
286+
data
287+
props {
288+
value
289+
}
290+
requests(first: 1, orderBy: submissionTime, orderDirection: desc) {
291+
disputed
292+
disputeID
293+
submissionTime
294+
resolved
295+
requester
296+
challenger
297+
resolutionTime
298+
rounds(first: 1, orderBy: creationTime, orderDirection: desc) {
299+
appealPeriodStart
300+
appealPeriodEnd
301+
ruling
302+
hasPaidRequester
303+
hasPaidChallenger
304+
amountPaidRequester
305+
amountPaidChallenger
306+
}
307+
}
308+
}
309+
}
310+
`
311+
}
312+
const { data, errors } = await (
313+
await fetch(GTCR_SUBGRAPH_URL, {
314+
method: 'POST',
315+
body: JSON.stringify(query)
316+
})
317+
).json()
318+
```
319+
320+
#### Details
321+
322+
If you want, you can also use apollo to cache queries and make the app load faster.
323+
324+
```
325+
const ITEM_DETAILS_QUERY = gql`
326+
query itemDetailsQuery($id: String!) {
327+
item(id: $id) {
328+
data
329+
requests(orderBy: submissionTime, orderDirection: desc) {
330+
requestType
331+
disputed
332+
disputeID
333+
submissionTime
334+
resolved
335+
requester
336+
arbitrator
337+
challenger
338+
evidenceGroupID
339+
creationTx
340+
resolutionTx
341+
rounds(orderBy: creationTime, orderDirection: desc) {
342+
appealPeriodStart
343+
appealPeriodEnd
344+
ruling
345+
hasPaidRequester
346+
hasPaidChallenger
347+
amountPaidRequester
348+
amountPaidChallenger
349+
}
350+
}
351+
}
352+
}
353+
`
354+
355+
// subgraph item entities have id "<itemID>@<listaddress>"
356+
const compoundId = `${itemID}@${tcrAddress.toLowerCase()}`
357+
const detailsViewQuery = useQuery(ITEM_DETAILS_QUERY, {
358+
variables: { id: compoundId }
359+
})
360+
```
361+
362+
### Item Interaction
363+
364+
This is the easiest part of the application. All items are referenced by their ID which is the keccak256 hash of the IPFS URI.
365+
366+
With it you can:
367+
368+
- Execute requests: This is for when a request passed the challenge period without any challenges.
369+
- Challenge requests (registration or removal) via the contract's `challengeRequest` function
370+
- Submit evidence: `submitEvidence` by passing the evidence json file following (ERC-1497)[https://kleros.gitbook.io/docs/developer/erc-1497-evidence-standard] standard.
371+
- Fund appeals: `fundAppeal`

0 commit comments

Comments
 (0)