Building a Project with SE-2 | Crowd Fund | Part Six | The Subgraph

WebSculpt
8 min readJan 31, 2024

--

Image from Shubham Dhage on Unsplash

Important Links

If you are here for the first time, then you may want to start at the beginning of this series.
Other posts in the series:
V1’s Smart Contract
V1’s Components
V2’s Smart Contract
V2’s Components

You are currently on the article for V3’s Smart Contract (View code on GitHub).
View Live Site

***Note that the readme file has a demo video to help you visualize what is going on.
******The readme file also contains instructions to run the project against a LOCAL subgraph as well as the DEPLOYED subgraph.

Links that support this article (more important links)

***Note that these ☝️ 3 blogs ☝️could have easily been a part of this one (they are playing a necessary support-role [if you do not use them in conjunction with this one — you may miss something]).

An excerpt from the Multisig Contract article

…this serves as a stepping stone — you’ll see how these proposals work before we start querying them from The Graph in V3. BUT … V2 is not totally practical due to the amount of data we would eventually be storing.
There is simply a better way to do it, but…
I feel strongly that V2 is the Walk-Before-You-Run to V3. V2 also requires a lot of (otherwise unnecessary) solidity to keep all of our Proposals and Fund Runs separated.

In the☝️ previous article☝️ — I stated that V2’s contract was not entirely practical — let’s talk about what’s changing …

New Updates

We are now going to use a Subgraph on The Graph to query our data, but how exactly is it going to know about our data? From our contract’s events! In the support-role blogs (that I mentioned above), you can find more information regarding the specifics. Here’s a bit of that now:

What is a subgraph?

This is — simply put — a way of organizing blockchain data — rules and instructions you give to the indexers around the world so that you can query the data. Here is a great video going over the topic.

Image from video: https://youtu.be/EJ2em_QkQWU?si=zJ_1amXXLHwJ6JCZ
  • Off-chain data: A good example of this would be IPFS
  • subgraph.yaml: Rules/Overview && Shape/Structure
  • mapping.ts: Apply logic for Data Transformation (you could look at this as the “business logic”)
  • schema.graphql: Data goes into “buckets” (AKA: “entities”); you will send GraphQL queries to these “buckets” to get data to your front-end
Example of a query

Back to our contract…

An event will fire off in our contract — let’s say an event for a new Fund Run occurs from within our contract — we want to capture that event and map the data over to an Entity that we will run queries against later.

This means (in the Crowd Fund example), if we want to see a list of Fund Runs or a table of Proposals — we can just query data that is derived from our events; whereas before, we were calling functions on the contract to return structs and lists of structs back to us.

We will still be storing some data on the contract, but we will not have to store nearly as much.
Learn more about The Graph and Subgraphs here.

The most-simplistic way to explain what we want out of this Subgraph: The blockchain is great for writing, but we want a better way to read from it.

When Graph Node is indexing the blockchain (and discovers an event that was emitted from our contract), we’ll provide a Handler Function — which will communicate back the pertinent details of this event; basically, fetching information and storing it in a way that we are defining.

Changes to the code

Structs

struct MultiSigRequest {
uint256 amount;
address to;
address proposedBy;
string reason;
}

struct FundRunValues {
uint256 target;
uint256 amountCollected;
uint256 amountWithdrawn;
}

struct DonorsLog {
address donor;
mapping(uint256 => uint256) donorMoneyLog; //mapping(fundRunId => donationAmount)
}

The old FundRun struct had a lot of bloat that is no longer needed — we just want to be able to check the Ether-Values.
The old MultiSigVault struct is gone entirely.
Note that what we are continuing to store with these structs is simply data that still needs to be maintained within the contract — rather than trusting what is being passed-to it.

Mappings

 mapping(uint16 => uint256) public vaultNonces;
mapping(uint16 => uint256) public fundRunDeadlines;
mapping(uint16 => address) public proposalCreators;
mapping(uint16 => address[]) public fundRunOwners;
mapping(uint16 => address[]) public proposalSigners;
mapping(uint16 => FundRunStatus) public fundRunStatuses;
mapping(uint16 => ProposalStatus) public proposalStatuses;
mapping(uint16 => FundRunValues) public fundRunValues;
mapping(address => DonorsLog) public donorLogs; //a single donor will have all of their logs (across all Fund Runs they donated to) here

In V2, we had old mappings which held entire (lists of) structs; whereas now, we’re storing data in a much-more compartmentalized way. We no longer need to keep up with all of those structs.

Below is an example of how the mapping —fundRunValues— is utilized in the contract to manage funds:

require(
fundRunValues[_id].amountCollected > 0,
"There is nothing to withdraw"
);
require(
fundRunValues[_id].amountCollected >
fundRunValues[_id].amountWithdrawn,
"This Fund Run is empty -- withdrawals may have already occurred."
);
require(
_tx.amount > 0,
"The proposed transaction withdrawal amount must be greater than 0."
);
require(
fundRunValues[_id].amountWithdrawn + _tx.amount <=
fundRunValues[_id].amountCollected,
"This proposal would overdraw this Fund Run."
);

When it comes to pieces of data like a Fund Run’s Title or Description — that is no longer important to our contract; however, we’re still “double-checking” numbers/values in the contract.
If we want to fetch the Title or the Description, we’ll query our subgraph.

Events (some examples)

event FundRun(
uint16 fundRunId,
address[] owners,
string title,
string description,
uint256 target,
uint256 deadline,
uint256 amountCollected,
uint256 amountWithdrawn,
FundRunStatus status
);

event Proposal(
uint16 proposalId,
uint16 fundRunId,
address proposedBy,
uint256 amount,
address to,
string reason,
ProposalStatus status,
uint256 signaturesRequired,
uint16 signaturesCount
);

event MultisigTransfer(
uint16 proposalId,
uint16 fundRunId,
address to,
uint256 netWithdrawAmount,
uint256 grossWithdrawAmount
);

Our subgraph will be complete with a schema and functions that handle our events…

Corresponding Event Handlers

export function handleFundRun(event: FundRunEvent): void {
let entity = new FundRun(
Bytes.fromHexString("fundruns__").concat(Bytes.fromI32(event.params.fundRunId))
)
entity.fundRunId = event.params.fundRunId
entity.owners = changetype<Bytes[]>(event.params.owners)
entity.title = event.params.title
entity.description = event.params.description
entity.target = event.params.target
entity.deadline = event.params.deadline
entity.amountCollected = event.params.amountCollected
entity.amountWithdrawn = event.params.amountWithdrawn
entity.status = event.params.status

entity.blockNumber = event.block.number
entity.blockTimestamp = event.block.timestamp
entity.transactionHash = event.transaction.hash

entity.save()
}

export function handleProposal(event: ProposalEvent): void {
let entity = new Proposal(
Bytes.fromHexString("proposals_").concat(Bytes.fromI32(event.params.proposalId))
)
entity.proposalId = event.params.proposalId
entity.fundRunId = event.params.fundRunId
entity.proposedBy = event.params.proposedBy
entity.amount = event.params.amount
entity.to = event.params.to
entity.reason = event.params.reason
entity.status = event.params.status
entity.signaturesRequired = event.params.signaturesRequired
entity.signaturesCount = event.params.signaturesCount

entity.blockNumber = event.block.number
entity.blockTimestamp = event.block.timestamp
entity.transactionHash = event.transaction.hash

let fundRunEntity = FundRun.load(Bytes.fromHexString("fundruns__").concat(Bytes.fromI32(event.params.fundRunId)));
if(fundRunEntity !== null)
entity.fundRun = fundRunEntity.id;

entity.save()
}

export function handleMultisigTransfer(event: MultisigTransferEvent): void {
let entity = new MultisigTransfer(
event.transaction.hash.concatI32(event.logIndex.toI32())
)
entity.proposalId = event.params.proposalId
entity.fundRunId = event.params.fundRunId
entity.to = event.params.to
entity.netWithdrawAmount = event.params.netWithdrawAmount
entity.grossWithdrawAmount = event.params.grossWithdrawAmount

entity.blockNumber = event.block.number
entity.blockTimestamp = event.block.timestamp
entity.transactionHash = event.transaction.hash

let proposalEntity = Proposal.load(Bytes.fromHexString("proposals_").concat(Bytes.fromI32(event.params.proposalId)))
if(proposalEntity !== null) {
proposalEntity.status = 2;
proposalEntity.save();

let fundRunEntity = FundRun.load(Bytes.fromHexString("fundruns__").concat(Bytes.fromI32(event.params.fundRunId)));
if(fundRunEntity !== null) {
fundRunEntity.amountWithdrawn = fundRunEntity.amountWithdrawn.plus(event.params.grossWithdrawAmount);
fundRunEntity.save();
}
}

entity.save()
}

The need for predictable IDs

Each of your “Entities” will have a unique (Bytes!) ID, that you code like this:

export function handleMultisigTransfer(event: MultisigTransferEvent): void {
let entity = new MultisigTransfer(
event.transaction.hash.concatI32(event.logIndex.toI32())
)
.....
}

But, I wanted to be able to predict what an ID would be, so I did this:

export function handleFundRun(event: FundRunEvent): void {
let entity = new FundRun(
Bytes.fromHexString("fundruns__").concat(Bytes.fromI32(event.params.fundRunId))
)
............
}

PLEASE NOTE: Bytes.fromHexString(“fundruns__”)👈👈 the string needs to have an even amount of characters, or you will have a bug.

I can then (later on), know how to reference this particular Fund Run (when a new Proposal is created):

export function handleProposal(event: ProposalEvent): void {
let entity = new Proposal(
Bytes.fromHexString("proposals_").concat(Bytes.fromI32(event.params.proposalId))
)
entity.proposalId = event.params.proposalId
..............
............
..........

let fundRunEntity = FundRun.load(Bytes.fromHexString("fundruns__").concat(Bytes.fromI32(event.params.fundRunId)));
if(fundRunEntity !== null)
entity.fundRun = fundRunEntity.id;

entity.save()
}

☝️☝️this code is setting the Fund Run of the Proposal “entity”…
Note how the FundRun is being loaded: FundRun.load(Bytes.fromHexString(“fundruns__”)....

Then, we simply set the “fundRun” property of our “proposal” to the (Bytes!) ID of the LOADED “fundRun”.

If you look at the schema, you will see how this one-to-many relationship is set-up:

type FundRun @entity {
id: Bytes!
.......
.......
proposals: [Proposal!] @derivedFrom(field: "fundRun")
}

type Proposal @entity {
id: Bytes!
......
......
fundRun: FundRun
}

Which allows for the query that populates this 3-tier table
( you are seeing the Proposals for a specific Fund Run ):

Displaying the one-to-many relationship

Querying

Graph Protocol uses the GraphQL Query API

Examples⁠
Query for a single Token entity defined in your schema:

{
token(id: "1") {
id
owner
}
}

Note: When querying for a single entity, the id field is required, and it must be a string.

Query all Token entities:

{
tokens {
id
owner
}
}

Example Fund Run Query

Fund Runs have a one-to-many relationship with Proposals

//queries page
//for viewing 3-tier table
//ALL Fund Runs
export const GQL_FUNDRUNS_Three_Tier = () => {
return gql`
query ($limit: Int!, $offset: Int!) {
fundRuns(orderBy: fundRunId, orderDirection: desc, first: $limit, skip: $offset) {
id
fundRunId
owners
title
description
deadline
target
amountCollected
amountWithdrawn
status
proposals {
id
proposalId
fundRunId
proposedBy
amount
to
reason
status
signatures {
id
signer
signature
}
}
}
}
`;
};

You will see that Proposals ALSO have a one-to-many relationship with the Signatures:



type Proposal @entity {
id: Bytes!
.....................
signatures: [ProposalSignature!] @derivedFrom(field: "proposal")
}

type ProposalSignature @entity {
id: Bytes!
...................
proposal: Proposal
}

Which facilitates this drill-down capability within our table:

3-tier table

Quick Review

Moving forward, when we want to read from our contract, we’ll be querying from a (Graph Protocol) Subgraph. Nothing changed about the signing/encoding/packing of these Proposals’ signatures; however, everything changed about how they were being stored and called-into-use.

Links that support this article

--

--

WebSculpt

Blockchain Development, coding on Ethereum. Condensed notes for learning to code in Solidity faster.