A Nethermind plugin to index and query Circles protocol events.
The repository contains a docker-compose file to start a Nethermind node with the Circles plugin installed. There are configurations for Gnosis Chain, Chiado and Spaceneth (a local testnet).
The quickstart configurations use lighthouse as consensus engine and spin up a postgres database to store the indexed data. The spaceneth configuration comes with a local blockscout instance.
git clone https://github.com/CirclesUBI/circles-nethermind-plugin.git
cd circles-nethermind-plugin
For the use with Gnosis Chain and Chiado, a shared secret is required to authenticate requests between the execution and consensus engine.
# Gnosis Chain
mkdir -p ./.state/jwtsecret-gnosis
openssl rand -hex 32 > ./.state/jwtsecret-gnosis/jwt.hex
# Chiado
mkdir -p ./.state/jwtsecret-chiado
openssl rand -hex 32 > ./.state/jwtsecret-chiado/jwt.hex
Copy the .env.example
file to .env
and adjust the values to your needs.
cp .env.example .env
Choose if your want to run your node on Gnosis Chain or Chiado.
# Gnosis chain
docker compose -f docker-compose.gnosis.yml up -d
# Chiado
docker compose -f docker-compose.chiado.yml up -d
That's it! The node must be fully synced before you can start querying the Circles events. Once synced you can use the node like any other RPC node, but with the added benefit of querying Circles events directly at the same RPC endpoint.
30303/tcp
(nethermind p2p)30303/udp
(nethermind p2p)8545/tcp
(nethermind rpc)5432/tcp
(postgres)9000/tcp
(consensus p2p)9000/udp
(consensus p2p)5054/tcp
(consensus metrics)
./.state
- Directory containing all host mapped docker volumes./.state/consensus-chiado|consensus-chiado
- Lighthouse consensus engine data./.state/nethermind-chiado|nethermind-gnosis
- Nethermind data./.state/postgres-chiado|postgres-gnosis
- Postgres data./.state/jwtsecret-chiado|jwtsecret-gnosis
- Shared secret between execution and consensus engine
The process of setting up a local only node is a bit more involved. However, by using this approach, you gain the possibility to manipulate the node's time and don't need any xDai, which is useful for testing purposes.
docker compose -f docker-compose.spaceneth.yml up -d
Since a new spaceneth node is empty except for the genesis block, you need to deploy the Circles contracts yourself.
# Clone the Circles contracts submodules
git submodule update --init --recursive
To deploy the contracts, we add a script to the Circles contracts repository that deploys the contracts to the spaceneth node.
# Add the deploy script to the Circles contracts repository
./add-deploy-script-tp-v2-repo.sh
As a last step, we need to replace the tload
and tstore
based reentrancy guards with a more classic approach.
Spaceneth does not support these instructions.
-
Open
circles-contracts-v2/src/hub/Hub.sol
-
Replace this modifier:
modifier nonReentrant(uint8 _code) { assembly { if tload(0) { revert(0, 0) } tstore(0, 1) } _; assembly { tstore(0, 0) } }
with this modifier:
bool private _reentrancyGuard; modifier nonReentrant(uint8 _code) { if (_reentrancyGuard) { revert CirclesReentrancyGuard(_code); } _reentrancyGuard = true; _; _reentrancyGuard = false; }
-
Open
circles-contracts-v2/foundry.toml
-
Remove this line:
evm_version = 'cancun'
Now you can deploy the contracts to the spaceneth node.
# Deploy the contracts
npm install && ./deploy.sh
You can access the blockscout instance at http://localhost:4000
.
You can get a funded account private key by running:
npm install
node createFundedAccount.js
You can fast-forward the time by running:
curl -X POST -H "Content-Type: application/json" -d '{"fake_time": "+1d x1"}' http://localhost:5000/set_time
Explanation:
{
"fake_time": "+1d x1"
}
+1d
means to offset the current time by 1 day. x1
means that the time will pass as real time. If you want to
fast-forward the time, you can increase the number of x
(e.g. x10
).
NOTE: This will restart the nethermind node.
If you want to start over, you can reset the spaceneth node by running:
# Stop the stack
docker compose -f docker-compose.spaceneth.yml down
# Delete all persisted data
sudo rm -rf .state/nethermind-spaceneth
sudo rm -rf .state/postgres-spaceneth
sudo rm -rf .state/postgres2-spaceneth
sudo rm -rf .state/redis-spaceneth
# Start the stack again
docker compose -f docker-compose.spaceneth.yml up
This method allows you to query the total Circles (v1) holdings of an address.
curl -X POST --data '{
"jsonrpc":"2.0",
"method":"circles_getTotalBalance",
"params":["0xde374ece6fa50e781e81aac78e811b33d16912c7"],
"id":1
}' -H "Content-Type: application/json" https://circles-rpc.aboutcircles.com/
{
"jsonrpc": "2.0",
"result": "5444258229585459544466",
"id": 1
}
This method allows you to query all individual Circles (v1) holdings of an address.
curl -X POST --data '{
"jsonrpc":"2.0",
"method":"circles_getTokenBalances",
"params":["0xde374ece6fa50e781e81aac78e811b33d16912c7"],
"id":1
}' -H "Content-Type: application/json" httpS://circles-rpc.aboutcircles.com/
{
"jsonrpc": "2.0",
"result": [
{
"token": "0x057f55e194b94073d2dfa4e86163c2e897086dc7",
"balance": "51429863673363442586"
},
{
"token": "0x09c85ee337f6f7bdd3e5e17213b703c26d9c907d",
"balance": "56223473812572527629"
}
],
"id": 1
}
This method allows you to query all (v1) trust relations of an address.
curl -X POST --data '{
"jsonrpc":"2.0",
"method":"circles_getTrustRelations",
"params":["0xde374ece6fa50e781e81aac78e811b33d16912c7"],
"id":1
}' -H "Content-Type: application/json" https://circles-rpc.aboutcircles.com/
{
"jsonrpc": "2.0",
"result": {
"user": "0xde374ece6fa50e781e81aac78e811b33d16912c7",
"trusts": {
"0xb7c83b840e146f9768a1fdc4ce46c8ad17594720": 100,
"0x5fd8f7464c050ec0fb34223aab544e13510812fa": 50,
"0x83d296691be2c9d7be14378ecbf2d95c3ddb0200": 100,
"0x70551a5862ef2c9baf81596f67be723283b6ccd0": 100
},
"trustedBy": {
"0x965090908dcd0b134802f35c9138a7e987b5182f": 100,
"0x5ce3d708a2b1e8371530754c930fc9b5bad27ab7": 100,
"0x6fae976eb90127b895ceddf8311864cda42ac6ac": 100,
"0x3e93a305d5cd96202c12084414a6622fa7a36c3d": 100
}
},
"id": 1
}
This method allows you to query Circles events. The method takes a single parameter, which is a JSON object with the following properties:
namespace
- The protocol namespace to query (System, CrcV1 or CrcV2).table
- The table to query (e.g.Signup
,Trust
, etc.).columns
- An array of column names to return or[]
to return all columns of the table.filter
- Filters that can be used e.g. for pagination or to search for specific values.order
- A list of columns to order the results by.distinct
- If set totrue
, only distinct rows are returned.limit
- The maximum number of rows to return (defaults to max. 1000).
NOTE: There is no default order, so make sure to always add sensible order columns.
Every table has at least the following columns:
blockNumber
- The block number the event was emitted in.timestamp
- The unix timestamp of the event.transactionIndex
- The index of the transaction in the block.logIndex
- The index of the log in the transaction.
Tables for batch events have an additional batchIndex
column.
The items of a batch are treated like individual events that can only be distinguished by the batchIndex
.
Namespaces and tables:
System
Block
CrcV1
HubTransfer
OrganizationSignup
Signup
Transfer
Trust
CrcV2
ApprovalForAll
DiscountCost
InviteHuman
PersonalMint
RegisterGroup
RegisterHuman
RegisterOrganization
RegisterShortName
Stopped
TransferBatch
TransferSingle
Trust
UpdateMetadataDigest
URI
Equals
NotEquals
GreaterThan
GreaterThanOrEquals
LessThan
LessThanOrEquals
Like
NotLike
In
NotIn
You can use the combination of blockNumber
, transactionIndex
and logIndex
(+ batchIndex
in the case of batch events) together with a limit
to paginate through the results.
Query the last two Circles signups:
curl -X POST --data '{
"jsonrpc": "2.0",
"id": 1,
"method": "circles_query",
"params": [
{
"Namespace": "CrcV1",
"Table": "Signup",
"Limit": 2,
"Columns": [],
"Filter": [],
"Order": [
{
"Column": "blockNumber",
"SortOrder": "ASC"
},
{
"Column": "transactionIndex",
"SortOrder": "ASC"
},
{
"Column": "logIndex",
"SortOrder": "ASC"
}
]
}
]
}' -H "Content-Type: application/json" https://localhost:8545/
{
"jsonrpc": "2.0",
"result": {
"columns": [
"blockNumber",
"timestamp",
"transactionIndex",
"logIndex",
"transactionHash",
"user",
"token"
],
"rows": [
[
"0x597343",
"0x64f5aa5a",
"0x0",
"0x3",
"0xb41462160f73af912b550b27a7ed31e091d5da6c59a6325b367048ea42eef47f",
"0x4bc38a9f15508d19299a45b063556ec4bee853ff",
"0xcc724001786fcf8414747dd598e8e9383882b6d7"
],
[
"0x597343",
"0x64f5aa5a",
"0x0",
"0x3",
"0xb41462160f73af912b550b27a7ed31e091d5da6c59a6325b367048ea42eef47f",
"0x4bc38a9f15508d19299a45b063556ec4bee853ff",
"0xcc724001786fcf8414747dd598e8e9383882b6d7"
]
]
},
"id": 1
}