Querying REVLoans via Bendystraw
Problem
Displaying loan data in revnet UIs requires querying Bendystraw's GraphQL API with the correct queries and understanding how to filter/aggregate loans across chains and projects.
Context / Trigger Conditions
- •Building UI to show a user's outstanding loans
- •Displaying all loans for a specific revnet
- •Checking if a user has permission to borrow
- •Calculating loan headroom (refinanceable amount)
- •Multi-chain loan aggregation
Solution
GraphQL Queries
Get All Loans for a User
graphql
query LoansByAccount($owner: String!, $version: Int!) {
loans(where: { owner: $owner, version: $version }) {
items {
borrowAmount
collateral
prepaidDuration
projectId
terminal
token
chainId
createdAt
id
project {
version
}
}
}
}
Variables:
- •
owner: User's wallet address (lowercase) - •
version: Protocol version (5 for V5)
Get Loans for Specific Project
graphql
query LoansDetailsByAccount($owner: String!, $projectId: Int!, $version: Int!) {
loans(where: { owner: $owner, projectId: $projectId, version: $version }) {
items {
borrowAmount
collateral
prepaidDuration
createdAt
projectId
terminal
token
chainId
id
project {
version
}
}
}
}
Check Borrow Permission
graphql
query HasPermission(
$account: String!
$chainId: Float!
$projectId: Float!
$operator: String!
$version: Float!
) {
permissionHolder(
account: $account
chainId: $chainId
projectId: $projectId
operator: $operator
version: $version
) {
permissions
}
}
Permission ID 1 = Borrow permission. Check if permissions array includes 1.
Loan Entity Fields
typescript
type Loan = {
id: BigInt // Unique loan ID
owner: String // Borrower address
beneficiary: String // Recipient of borrowed funds
borrowAmount: BigInt // Amount borrowed (in base token wei)
collateral: BigInt // Tokens locked as collateral
prepaidDuration: Int // Seconds of prepaid fee time
prepaidFeePercent: Int // Basis points of prepaid fee
projectId: Int // Revnet project ID
chainId: Int // Chain where loan exists
terminal: String // Terminal address
token: String // Base token address (ETH = 0x0...0)
createdAt: Int // Unix timestamp
sourceFeeAmount: BigInt // Total fees charged
tokenUri: String | null // NFT metadata URI (loans are ERC-721)
version: Int // Protocol version
}
React Hook Usage (revnet-app pattern)
typescript
import { useBendystrawQuery } from 'juice-sdk-react'
import { LoansByAccountDocument } from '@/generated/graphql'
const LOAN_POLL_INTERVAL = 3000 // 3 seconds
function useUserLoans(address: string, version: number = 5) {
const { data, loading, error } = useBendystrawQuery(
LoansByAccountDocument,
{ owner: address.toLowerCase(), version },
{ pollInterval: LOAN_POLL_INTERVAL }
)
return {
loans: data?.loans.items ?? [],
loading,
error
}
}
Filter Loans by Revnet
When showing loans for a specific revnet (which may span multiple chains):
typescript
function filterLoansByRevnet(
loans: Loan[],
revnetProjectIds: number[] // projectIds across all chains
): Loan[] {
return loans.filter(loan =>
revnetProjectIds.includes(Number(loan.projectId))
)
}
// Usage: Get projectIds from suckerGroup
const { data: projectData } = useBendystrawQuery(ProjectDocument, { ... })
const revnetProjectIds = projectData.project.suckerGroup?.projects_rel
.map(p => Number(p.projectId)) ?? [Number(projectData.project.projectId)]
const filteredLoans = filterLoansByRevnet(loans, revnetProjectIds)
Calculate Loan Headroom (Refinanceable Amount)
Use contract call to get borrowable amount for existing collateral:
typescript
import { useReadContract } from 'wagmi'
import { revLoansAbi } from '@/abi/revLoans'
function useLoanHeadroom(loan: Loan) {
const { data: borrowableAmount } = useReadContract({
address: REVLOANS_ADDRESS,
abi: revLoansAbi,
functionName: 'borrowableAmountFrom',
args: [
BigInt(loan.projectId),
BigInt(loan.collateral),
18, // decimals
1, // currency (ETH)
],
})
// Headroom = what you could borrow - what you already borrowed
const headroom = borrowableAmount
? borrowableAmount - BigInt(loan.borrowAmount)
: 0n
return headroom
}
Multi-Chain Token Resolution
Loans may use different tokens on different chains. Get token config from suckerGroup:
graphql
query GetSuckerGroup($id: String!) {
suckerGroup(id: $id) {
projects_rel {
projectId
chainId
decimals # 18 for ETH, 6 for USDC
currency # 1 for ETH, 2 for USDC
}
}
}
typescript
function getTokenConfigForLoan(loan: Loan, suckerGroup: SuckerGroup) {
const project = suckerGroup.projects_rel.find(
p => p.chainId === loan.chainId && p.projectId === loan.projectId
)
return {
decimals: project?.decimals ?? 18,
currency: project?.currency ?? 1,
}
}
Verification
Test with known loan data:
- •Query loans for an address known to have loans
- •Verify
borrowAmountmatches on-chainREVLoans.loanOf() - •Check that
prepaidDurationdecreases over time (fee time consumed)
Example
Complete component for displaying user loans:
typescript
function UserLoansTable({ address, revnetProjectIds }) {
const { loans, loading } = useUserLoans(address)
// Filter to this revnet only
const revnetLoans = filterLoansByRevnet(loans, revnetProjectIds)
if (loading) return <Spinner />
if (revnetLoans.length === 0) return <EmptyState />
return (
<Table>
<thead>
<tr>
<th>Chain</th>
<th>Borrowed</th>
<th>Collateral</th>
<th>Fee Time</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{revnetLoans.map(loan => (
<LoanRow key={loan.id} loan={loan} />
))}
</tbody>
</Table>
)
}
Notes
- •Loans are ERC-721 NFTs - each loan has a unique
tokenUri - •
prepaidDurationis in seconds, decreases as time passes - •After 10 years (
LOAN_LIQUIDATION_DURATION), loans can be liquidated - •Permission checking uses Bendystraw, but actual borrow calls use on-chain contracts
- •Poll interval of 3 seconds keeps UI responsive to loan state changes
References
- •loansByAccount.graphql
- •LoansDetailsTable.tsx
- •useHasBorrowPermission.ts
- •REVLoans contract:
/jb-revloansskill for contract mechanics