@ceresv2/sdk
v0.3.2
Published
Ceres SDK — React hooks, API client, and invite flow for the Ceres on-chain social graph protocol
Maintainers
Readme
@ceresv2/sdk
TypeScript SDK for the Ceres protocol — address-level invite, on-chain social graph, DID identity, and B-side integration on Sepolia testnet.
Features
- React hooks for wallet-connected read & write operations (invite, profile, inviter, descendants)
- CeresApiClient — HTTP client for Ceres Relayer API (millisecond reads, no RPC)
- CeresInviteFlow — end-to-end invite binding (sign → submit → wait), one function call
- V2 Address-Level Invites —
bindInviterBySigwith EIP-712 signature (zero gas for end users) - B-Side Integration — deploy a FeeContract + get whitelisted for automatic fee channel
- Pure utility functions that work with any viem
PublicClient(no React required) - Pre-built contract ABIs and on-chain contract addresses
- CeresProvider to wire up Wagmi + React Query in one line
Installation
npm install @ceresv2/sdkPeer dependencies
Make sure your project has these installed:
npm install wagmi viem @tanstack/react-query reactQuick Start
Wrap your app with CeresProvider:
import { CeresProvider } from '@ceresv2/sdk'
function App() {
return (
<CeresProvider>
<YourApp />
</CeresProvider>
)
}That's it — CeresProvider internally sets up WagmiProvider (with the Sepolia RPC config) and QueryClientProvider.
Custom Wagmi Config
If you need a different chain or RPC endpoint, pass your own wagmi config:
import { CeresProvider } from '@ceresv2/sdk'
import { http, createConfig } from 'wagmi'
import { sepolia } from 'wagmi/chains'
const myConfig = createConfig({
chains: [sepolia],
transports: {
[sepolia.id]: http('https://your-custom-rpc.example.com'),
},
})
function App() {
return (
<CeresProvider wagmiConfig={myConfig}>
<YourApp />
</CeresProvider>
)
}CeresApiClient (No React Required)
The API client talks to the Ceres Relayer's REST API for fast database reads. No wallet, no RPC — just HTTP. Works in browser, Node.js, and serverless.
import { CeresApiClient } from '@ceresv2/sdk'
const ceres = new CeresApiClient({ baseUrl: 'http://43.156.99.215:5000' })
// Batch check up to 200 addresses (millisecond response)
const { profiles } = await ceres.batchCheck([
'0xd5Ec02F0f4e5CD7122472e7C31549b6a48c6F5C8',
'0x361f92e8d41b305b5cb0ef63a8768e79f7e18b65',
])
// → [{ address, invited, inviter, chainId, inviteeCount, descendantCount }]
// Get global stats
const stats = await ceres.getStats()
// → { edgeCount: 308, profiles: 274, chains: [...] }
// Paginated edges with incremental sync
const { edges, hasMore } = await ceres.getEdges({ since: 1783548174, order: 'asc' })
// → { edges: [...], page: 1, total: 308, hasMore: false }
// Get full graph around an address
const graph = await ceres.getAddressGraph('0x...')
// Get full network graph (cached 5 min)
const network = await ceres.getNetworkGraph()Incremental Sync Pattern
// First run: full sync
const { edges, hasMore } = await ceres.getEdges({ limit: 1000 })
let lastTimestamp = edges[edges.length - 1]?.timestamp ?? 0
// Every 15 seconds: get only new edges
setInterval(async () => {
const delta = await ceres.getEdges({ since: lastTimestamp, order: 'asc' })
for (const edge of delta.edges) {
console.log('New:', edge.invitee, 'invited by', edge.inviter)
lastTimestamp = edge.timestamp
}
}, 15000)CeresInviteFlow — One-Click Invite
Encapsulates the entire invite journey into a single function call. The caller does NOT need to know about EIP-712, contract addresses, or Relayer endpoints.
import { CeresInviteFlow } from '@ceresv2/sdk'
// Frontend button handler
async function handleInvite(inviterAddress: string) {
const flow = new CeresInviteFlow({ chainId: 56 }) // BSC
const result = await flow.invite({
walletClient, // from useWalletClient() or wagmi
inviterAddress: inviterAddress as `0x${string}`,
})
if (result.success) {
console.log('✅ Invited! Tx:', result.txHash)
console.log(' Inviter:', result.inviter)
console.log(' Invitee:', result.invitee)
console.log(' Chain:', result.chainId)
} else {
if (result.code === 'ALREADY_INVITED') {
console.warn('Already invited:', result.error)
} else {
console.error('Failed:', result.error)
}
}
}InviteFlow steps (handled internally)
- Check if invitee already has an inviter (API first → contract fallback)
- Sign EIP-712
Invitetyped data with the user's wallet - Submit signature to Ceres Relayer
- Wait for on-chain confirmation (polling API + RPC)
- Return
{ success: true, txHash, receipt }or{ success: false, error, code }
Supported Chains
| Chain | chainId | InviteFlow | |-------|---------|------------| | ETH | 1 | ✅ | | BSC | 56 | ✅ | | BASE | 8453 | ✅ | | Sepolia | 11155111 | ✅ (testnet) |
All hooks require a WagmiProvider + QueryClientProvider context (provided automatically by <CeresProvider>).
React Hooks (V2 — One-Click Integration)
These hooks are the recommended way to integrate Ceres. Zero debugging — just wrap with <CeresProvider>, then use hooks.
useCeresInvite — 一键邀请
import { useCeresInvite } from '@ceresv2/sdk'
function InviteButton({ inviterAddress }: { inviterAddress: string }) {
const { invite, isPending, error } = useCeresInvite()
return (
<button onClick={() => invite(inviterAddress as `0x${string}`)} disabled={isPending}>
{isPending ? '邀请中...' : '邀请'}
</button>
)
}useCeresUserStatus — 用户所有状态(一站式)
import { useCeresUserStatus } from '@ceresv2/sdk'
function Dashboard() {
const { address, isConnected, inviteProfile, didProfile, invitees, isLoading } = useCeresUserStatus()
if (!isConnected) return <p>请先连接钱包</p>
if (isLoading) return <p>加载中...</p>
return (
<div>
<p>地址: {address}</p>
{inviteProfile?.invited && <p>邀请人: {inviteProfile.inviter}</p>}
{didProfile && <p>DID: {didProfile.name}</p>}
<p>邀请了 {invitees.length} 个地址</p>
</div>
)
}useCeresMintDID — 铸造 DID
import { useCeresMintDID } from '@ceresv2/sdk'
function MintButton() {
const { mint, isPending, mintFee } = useCeresMintDID()
return (
<button onClick={() => mint({ name: 'My DID', bio: 'Hello Ceres', avatar: '', urls: [] })}>
{isPending ? '铸造中...' : `铸造 DID (${mintFee ? '需支付' : '免费'})`}
</button>
)
}Hooks 一览
| Hook | 用途 | 数据源 |
|------|------|--------|
| useCeresInvite() | 一键邀请 | EIP-712 签名 → Relayer → 链 |
| useCeresUserStatus() | 用户全部状态(邀请+DID+邀请列表) | API + 链 |
| useCeresMintDID() | 铸造 DID Profile | 钱包交互 → 合约 |
| useCeresDIDProfile(addr) | DID 信息(by address) | 合约 |
| useCeresDIDStats() | DID 合约全局统计 | 合约 |
| useCeresRecentDIDs() | 最新 DID profiles | API |
| useCeresProfilesByOwner(addr) | 某地址拥有的所有 DID | API |
| useCeresBatchCheck(addresses) | 批量查邀请 | API → 合约 fallback |
| useCeresProfile(addr) | 单地址邀请状态 | API |
| useCeresInvitees() | 当前钱包邀请列表 | API |
| useCeresNetwork() | 全局统计 | API |
| useCeresEdges() | 分页边查询 | API |
| useCeresGraph() | 全局图谱 | API |
接入方只需要
// 1. 包裹 CeresProvider
import { CeresProvider } from '@ceresv2/sdk'
<CeresProvider>
<App />
</CeresProvider>
// 2. 用 hooks
import { useCeresInvite, useCeresUserStatus } from '@ceresv2/sdk'
// 搞定React Hooks (V1 — DID NFT, Legacy)
useProfile
Read a full DID profile by token ID (name, bio, avatar, owner, level):
import { useProfile } from '@ceresv2/sdk'
function ProfileCard({ tokenId }: { tokenId: bigint }) {
const { data: profile, isLoading } = useProfile(tokenId)
if (isLoading) return <div>Loading…</div>
if (!profile) return <div>Profile not found</div>
return (
<div>
<h2>{profile.name}</h2>
<p>{profile.bio}</p>
<img src={profile.avatar} alt={profile.name} />
<span>Level: {profile.levelName} ({profile.level})</span>
<span>Owner: {profile.owner}</span>
</div>
)
}useInviter
Look up the inviter (parent) token ID in the invitation tree:
const { data: inviterTokenId } = useInviter(tokenId)
// → bigint (0n = root node / no inviter)useDescendantCount
Count all descendants (direct + indirect invitees):
const { data: count } = useDescendantCount(tokenId)
// → bigintuseBalanceOf
Get the ERC-721 balance for an address:
const { data: balance } = useBalanceOf(address)
// → bigintuseTotalProfiles
Total number of profiles ever created:
const { data: totalProfiles } = useTotalProfiles()
// → bigintuseUserTokenId
Auto-detect the first token ID owned by the connected wallet:
const tokenId = useUserTokenId() // → bigint | undefinedPure Utility Functions (no React)
Use these in server-side code, scripts, or any non-React context. Each function takes a viem PublicClient as the last argument.
import { createPublicClient, http } from 'viem'
import { sepolia } from 'viem/chains'
import { getProfile, getInviter, getDescendantCount, getBalanceOf } from '@ceresv2/sdk'
const client = createPublicClient({
chain: sepolia,
transport: http('https://sepolia.gateway.tenderly.co'),
})
// Fetch a full profile
const profile = await getProfile(1n, client)
// Look up invitation relationship
const inviterId = await getInviter(5n, client)
// Count descendants
const count = await getDescendantCount(3n, client)
// Get token balance of an address
const balance = await getBalanceOf('0x...', client)
// Get total profiles
const total = await getTotalProfiles(client)
// Look up token ID by address
const tokenId = await getUserTokenId('0x...', client)Contract ABIs & Addresses
Access the raw ABIs and addresses for direct integration:
import {
CeresDID_ABI,
CeresRegistry_ABI,
CERES_DID_ADDRESS,
CERES_REGISTRY_ADDRESS,
didContract,
registryContract,
} from '@ceresv2/sdk'- CeresDID: ERC-721 NFT at
0x159f4001C8692A777A842f3F0A76f268aF1A8F39 - CeresRegistry: Registry & invitation tree at
0x9043489CFFe56C1C5b5E1b8Fb1E4bc384B575116
The didContract / registryContract exports are pre-built config objects with both address and abi, ready for wagmi useReadContract or viem readContract.
Types
import type { Profile, ProfileWithId, UseProfileResult } from '@ceresv2/sdk'
interface Profile {
name: string
bio: string
avatar: string
updatedAt: bigint
}
interface ProfileWithId extends Profile {
tokenId: bigint
owner: `0x${string}`
level: number
levelName: string
}Level System
| Level | Name | Color | |-------|---------|---------| | 0 | Seed | Gray | | 1 | Bronze | Bronze | | 2 | Silver | Silver | | 3 | Gold | Gold | | 4 | Crystal | Purple | | 5 | Diamond | Blue |
import { LEVEL_NAMES, LEVEL_COLORS } from '@ceresv2/sdk'
LEVEL_NAMES[3] // → 'Gold'
LEVEL_COLORS[5] // → '#3B82F6'V2 Address-Level Hooks
The V2 protocol introduces address-level invite binding (no DID mint required).
useCeresInvite
Create an invite relationship via EIP-712 signature:
import { useCeresInvite } from '@ceresv2/sdk'
function InviteButton({ inviter, invitee }: { inviter: Address; invitee: Address }) {
const { invite, isPending, error } = useCeresInvite()
return (
<button onClick={() => invite({ inviter, invitee })} disabled={isPending}>
{isPending ? 'Signing...' : `Invite ${invitee.slice(0,6)}...`}
</button>
)
}Under the hood: SDK generates EIP-712 typed data → user signs with wallet → signature sent to Relayer → Relayer calls Core.bindInviterBySig() → invite created on-chain.
useCeresHasInviter
Check if an address has been invited:
const { data: hasInviter } = useCeresHasInviter(address)
// → booleanuseCeresRead
Batch read invitation data for an address:
const { data } = useCeresRead(address)
// → { hasInviter, inviter, descendantCount, level }B-Side Integration
For projects that want to offer Ceres invites to their own users with automatic fee collection:
- Deploy a
CeresFeeContract(or use the standard version) - Request whitelist registration from the Ceres team (
Core.addFeeContract(yourAddress)) - Use
useCeresInvite()from this SDK — the Relayer routes through your FeeContract
See docs/B端接入流程.md for the full integration guide.
Chain Support
| Network | Core-A | Core-B | Relayer |
|---------|--------|--------|---------|
| Sepolia (11155111) | 0xCD142BDD...533D | 0xeE05846...7333 | 101.33.109.117:5003 |
License
MIT
