Since Curate was first conceptualized, demand for ethereum block space has skyrocketed. This put a high price floor on the usecases that Curate can be with. Furthermore, we learned that there are is a lot more demand for user facing data than for contract to contract queries.
Light Curate is a new version of the contract that significantly decreases costs of deployment and operation of Curate lists by leveraging new technologies and changing the strategy for data storage.
1- Light Curate does not use contract storage to store item data. Instead, we only store the item's IPFS multihash in the contract. This means other contracts can't query the TCR with field values, but storage costs are roughly O(1) vs Classic Curate's O(n). 2- The Graph ipfs api means we can also store the item fields in the subgraph. This comes with several benefits, among them: - No need to use @kleros/gtcr-encoder to encode and decode items. Just query the subgraph and you have the item. - Faster, scalable search: Classic Curate needs to sync with the client by downloading every single item and decoding it. With subgraphs we do not need to do this and can query the fields directly. - New need for complex solidity code searching fields on-chain. 3- EIP-1167: Light Curate uses the minimal proxy for new deployments. This means the cost of deploying a new TCR dropped from roughly 7 million gas to 700k. Ten times cheaper!
Development
This section will be devided into 3 sections:
1- Fetching Parameters: Your UI needs to display some important information to the users such as, what is the bounty for successfuly challenges and how long do items stay in the challenge period. 1- Item Submission: Here you will learn how to build a button to submit an item to the UI. 2- Fetching Items: How to view items and item details. 3- Item Interaction: This includes challenging, submitting evidence and crowdfunding appeals.
Fetching Parameters
We use a view contract to fetch all the relevant information at once. Deployments:
Kovan: 0x2dba4b729cb5f73bf85e7012ea99aa477a210dd6
Note: If you are using react, you can take the hook we built here or use it as an example.
Item Submission.
With light Curate, item submission consists of first uploading the item to IPFS and then submitting a transaction with the required deposit.
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.
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.
// ipfs-publish.js
import deepEqual from 'fast-deep-equal/es6'
const mirroredExtensions = ['.json']
/**
* Send file to IPFS network.
* @param {string} fileName - The name that will be used to store the file. This is useful to preserve extension type.
* @param {ArrayBuffer} data - The raw data from the file to upload.
* @returns {object} ipfs response. Should include the hash and path of the stored item.
*/
export default async function ipfsPublish(fileName, data) {
if (!mirroredExtensions.some(ext => fileName.endsWith(ext)))
return publishToKlerosNode(fileName, data)
const [klerosResult, theGraphResult] = await Promise.all([
publishToKlerosNode(fileName, data),
publishToTheGraphNode(fileName, data),
// Pin to your own ipfs node here as well.
])
if (!deepEqual(klerosResult, theGraphResult)) {
console.warn('IPFS upload result is different:', {
kleros: klerosResult,
theGraph: theGraphResult
})
throw new Error('IPFS upload result is different.')
}
return klerosResult
}
/**
* Send file to IPFS network via the Kleros IPFS node
* @param {string} fileName - The name that will be used to store the file. This is useful to preserve extension type.
* @param {ArrayBuffer} data - The raw data from the file to upload.
* @returns {object} ipfs response. Should include the hash and path of the stored item.
*/
async function publishToKlerosNode(fileName, data) {
const buffer = await Buffer.from(data)
const url = `${process.env.REACT_APP_IPFS_GATEWAY}/add`
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify({
fileName,
buffer
}),
headers: {
'content-type': 'application/json'
}
})
const body = await response.json()
return body.data
}
/**
* Send file to IPFS network via The Graph hosted IPFS node
* @param {string} fileName - The name that will be used to store the file. This is useful to preserve extension type.
* @param {ArrayBuffer} data - The raw data from the file to upload.
* @returns {object} ipfs response. Should include the hash and path of the stored item.
*/
async function publishToTheGraphNode(fileName, data) {
const url = `${process.env.REACT_APP_HOSTED_GRAPH_IPFS_ENDPOINT}/api/v0/add?wrap-with-directory=true`
const payload = new FormData()
payload.append('file', new Blob([data]), fileName)
const response = await fetch(url, {
method: 'POST',
body: payload
})
const result = await jsonStreamToPromise(response.body)
return result.map(({ Name, Hash }) => ({
hash: Hash,
path: `/${Name}`
}))
}
/**
* Accumulates a JSON stream body into an array of JSON objects.
* @param {ReadableStream} stream The stream to read from.
* @returns {Promise<any>} An array of all JSON objects emitted by the stream.
*/
async function jsonStreamToPromise(stream) {
const reader = stream.getReader()
const decoder = new TextDecoder('utf-8')
const deferred = {
resolve: undefined,
reject: undefined
}
const result = new Promise((resolve, reject) => {
deferred.resolve = resolve
deferred.reject = reject
})
const acc = []
const start = async () => {
reader
.read()
.then(({ done, value }) => {
if (done) return deferred.resolve(acc)
// Each `read` can produce one or more lines...
const lines = decoder.decode(value).split(/\n/)
const objects = lines
.filter(line => line.trim() !== '')
.map(line => JSON.parse(line))
acc.push(...objects)
return start()
})
.catch(err => deferred.reject(err))
}
start()
return result
}
The JSON file for the object is composed of the its metadata and fields.
Metadata (columns): An array describing each of the items columns (what's its type, name, description, etc.)
Values (values): An object mapping the column name to the value.
The metadata is available inside the meta evidence file, which is returned by the useTCRView hook. The Values are input by the user.
Example of columns used by the TCR at
[
{
"label": "Logo",
"description": "The token's logo.",
"type": "image",
"isIdentifier": false
},
{
"label": "Name",
"description": "The token name.",
"type": "text",
"isIdentifier": true
},
{
"label": "Ticker",
"description": "The token ticker.",
"type": "text",
"isIdentifier": true
},
{
"label": "Address",
"description": "The token address.",
"type": "address",
"isIdentifier": true
},
{
"label": "Chain ID",
"description": "The ID of the chain the token contract was deployed",
"type": "number"
},
{
"label": "Decimals",
"description": "The number of decimal places.",
"type": "number"
}
]
And an example of values. Note that it is required for the keys to match the column names in the columns object.