energy-attestation-sdk
v1.1.1
Published
TypeScript SDK for the Energy Attestation Service — on-chain energy reporting via EAS
Maintainers
Readme
energy-attestation-sdk
TypeScript SDK for the Energy Attestation Service — an on-chain public good that enables any energy project to publicly attest energy data using the Ethereum Attestation Service (EAS).
Website: attest.energy
Table of Contents
- What is this?
- Installation
- Quick Start
- Usage Examples
- Configuration
- API Reference
- Gas Estimation
- Error Handling
- Energy Types
- Utilities
- Architecture
- License
What is this?
The Energy Attestation Service provides a trustless, immutable ledger for energy generation and consumption data. Energy projects — solar farms, wind turbines, grid consumers, IoT devices — submit periodic readings that are permanently recorded on-chain via EAS attestations.
This SDK wraps the on-chain smart contracts so you can interact with the protocol from Node.js backends, IoT devices, and browser dApps without dealing with ABI encoding, contract addresses, or EAS internals.
How it works
Your App / IoT Device
│
▼
EnergySDK ← this package
│
▼
EAS (attest) ← Ethereum Attestation Service
│
▼
Resolver ← validates readings, checks authorization
│
▼
EnergyRegistry ← permanent state: projects, watchers, energy totals- You initialize the SDK with a private key and an RPC endpoint.
- You call
sdk.attestations.attest(...)with your energy readings. - The SDK ABI-encodes the data and submits it to EAS on-chain.
- EAS triggers the Resolver, which validates the data (authorization, timestamps, reading format).
- If valid, the Resolver writes the results to the EnergyRegistry (permanent state).
- The SDK parses the transaction receipt and returns the attestation UID.
Key concepts
| Concept | Description |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Watcher | An organization or entity that owns energy projects (e.g., a utility company, a solar farm operator). Anyone can register as a watcher — it's permissionless. |
| Project | A specific energy generation site or consumption point under a watcher (e.g., "Solar Farm Alpha", "Building 7 Grid Import"). Each project has a fixed energy type. |
| Attester | A wallet authorized to submit energy readings for a project. Can be an IoT device, an auditor, or the project operator. Whitelisted per-project or per-watcher. |
| Attestation | A set of energy readings for a time period, permanently recorded on-chain. Attestations form a sequential chain — each starts exactly where the previous one ended. |
| Replacement | Corrections are done by submitting a new attestation that references the old one (via refUID). The original stays on-chain for audit; totals update atomically. |
Data model
Each attestation contains:
| Field | Type | Description |
| ------------------------ | ----------- | ----------------------------------------------------------- |
| projectId | uint64 | The project these readings belong to |
| readings | uint256[] | Energy per interval in watt-hours (Wh, not kWh) |
| readingIntervalMinutes | uint32 | Minutes between each reading (e.g., Interval.Hourly = 60) |
| fromTimestamp | uint64 | Start of the reporting period (Unix seconds) |
| method | string | How data was collected: "iot", "manual", "estimated" |
| metadataURI | string | Optional IPFS/HTTPS link to supporting evidence |
Note: The
toTimestampis derived on-chain:fromTimestamp + readings.length * readingIntervalMinutes * 60.Energy is always expressed in watt-hours (Wh) as
uint256to avoid floating-point precision issues on-chain.
Installation
npm install energy-attestation-sdk ethersRequirements: Node.js >= 18, ethers.js v6
Graph API key
EnergyQuery queries the Energy Attestation subgraphs hosted on The Graph. Querying requires a free API key from The Graph Studio:
- Go to thegraph.com/studio and sign in
- Navigate to API Keys → Create API key
- Pass it to
EnergyQueryvia theapiKeyoption
The subgraphs are deployed and maintained by the Energy Attestation Service — you only need your own key, not your own subgraph.
Quick Start
Node.js / IoT / Scripts (private key)
import { EnergySDK, Network, Interval, EnergyType } from "energy-attestation-sdk";
// --- Section 1: Watcher wallet (owner) registers watcher + project ---
//
// This wallet will OWN the watcher and project.
const watcherSdk = await EnergySDK.fromPrivateKey({
privateKey: process.env.WATCHER_PRIVATE_KEY!,
network: Network.AMOY,
});
// 1. Register a watcher (organization)
const { watcherId } = await watcherSdk.watchers.createWatcher("My Energy Company");
// 2. Register a solar generation project
const { projectId } = await watcherSdk.projects.createProject(
watcherId,
"Solar Farm Alpha",
EnergyType.SOLAR_PV,
);
// --- Section 2: Separate attester wallet submits readings ---
//
// This wallet is DIFFERENT from the watcher owner wallet.
const attesterSdk = await EnergySDK.fromPrivateKey({
privateKey: process.env.ATTESTER_PRIVATE_KEY!,
network: Network.AMOY,
});
// 3. Watcher owner authorizes the attester wallet for this project
await watcherSdk.attesters.addAttester(projectId, attesterSdk.address);
// 4. Attester submits 4 hourly readings (Wh per hour)
const { uid, txHash } = await attesterSdk.attestations.attest({
projectId: Number(projectId),
readings: [1500n, 1800n, 2100n, 1900n],
readingIntervalMinutes: Interval.Hourly, // or a custom number in minutes
fromTimestamp: 1700000000,
method: "iot",
metadataURI: "ipfs://Qm...",
});
console.log(`Attestation UID: ${uid}`);
console.log(`Transaction: ${txHash}`);Browser dApp (MetaMask, WalletConnect, etc.)
import { EnergySDK, Network } from "energy-attestation-sdk";
import { BrowserProvider } from "ethers";
// Connect to the user's browser wallet
const browserProvider = new BrowserProvider(window.ethereum);
const signer = await browserProvider.getSigner();
const sdk = await EnergySDK.fromSigner({
signer,
network: Network.CELO,
});
// All SDK methods work identically — the signer handles transaction signing
const { watcherId } = await sdk.watchers.createWatcher("My Organization");
console.log(`Connected as: ${sdk.address}`);Usage Examples
Sequential attestations (IoT device loop)
Attestations must form a continuous chain with no gaps. Use getProjectLastTimestamp() to determine where the next one starts:
// Get the chain tip — next attestation must start here
const lastTimestamp = await sdk.read.getProjectLastTimestamp(projectId);
const fromTimestamp =
lastTimestamp === 0n
? Math.floor(Date.now() / 1000) - 3600 // First attestation: start 1 hour ago
: Number(lastTimestamp); // Continue the chain
await sdk.attestations.attest({
projectId: 1,
readings: [2500n],
readingIntervalMinutes: Interval.Hourly,
fromTimestamp,
method: "iot",
});Recording a zero period (downtime, gap fill)
When a device is offline or data is unavailable, use attestZeroPeriod to keep the chain continuous. fromTimestamp is automatically fetched from the project's last attested timestamp — no manual bookkeeping needed.
Note: You can also record zeros with the regular
attest()method by passingreadings: [0n]directly.attestZeroPeriodis just a convenience wrapper that handles the timestamp lookup and zero-filling for you.
// Record a full day of zero output (e.g., device was offline)
const { uid } = await sdk.attestations.attestZeroPeriod({
projectId: 1,
interval: Interval.Daily, // covers exactly 1 day
method: "downtime", // optional — defaults to "0 report"
metadataURI: "ipfs://Qm...", // optional — link to incident report
});The zero period is fully replaceable later with real readings at any granularity, as long as the replacement covers the same total time window:
// Replace the daily zero with 24 hourly real readings
const lastTimestamp = await sdk.read.getProjectLastTimestamp(1);
await sdk.attestations.overwriteAttestation({
projectId: 1,
readings: [1500n, 1800n, 2100n /* ...21 more hourly readings */],
readingIntervalMinutes: Interval.Hourly,
fromTimestamp: Number(lastTimestamp) - Interval.Daily * 60, // start of the zero period
method: "iot",
refUID: uid,
});Correcting a previous attestation
If readings were wrong, submit a replacement. This is a two-transaction flow handled automatically by the SDK — first the replacement attestation is submitted (the resolver's onAttest hook detects the non-zero refUID, validates the period is identical, and records the replacement atomically), then the old attestation is revoked on EAS so it appears as revoked on EAS explorer. The original attestation is marked as replaced on-chain; totals are updated in the same first transaction.
const { uid: newUid } = await sdk.attestations.overwriteAttestation({
projectId: 1,
readings: [2800n], // Corrected readings
readingIntervalMinutes: Interval.Hourly,
fromTimestamp: 1700000000, // Must match original period exactly
method: "manual",
metadataURI: "ipfs://Qm...", // Link to correction justification
refUID: "0xabc...def", // UID of the attestation being replaced
});Important: The replacement must cover the exact same time period (same
fromTimestampand derivedtoTimestamp). Only the readings, method, and metadata can change.
The SDK pre-validates the period match by fetching the original attestation before sending the transaction, surfacing mismatches as a ConfigurationError without spending gas. The resolver enforces the same rules on-chain as a final guarantee.
Querying project state
All read methods are gas-free view calls:
// Project info
const project = await sdk.read.getProject(projectId);
console.log(`${project.name} — type: ${project.energyType}, active: ${project.registered}`);
// Energy totals (in Wh)
const generated = await sdk.read.getTotalGeneratedEnergy(projectId);
console.log(`Total generated: ${generated} Wh (${generated / 1000n} kWh)`);
// Watcher-level aggregates
const watcherTotal = await sdk.read.getTotalGeneratedEnergyByWatcher(watcherId);
// Check if an address is authorized to submit readings
const canAttest = await sdk.read.isProjectAttester(projectId, "0x...");Querying indexed data (EnergyQuery)
EnergyQuery is a standalone class — no private key needed — for querying the indexed subgraph. It enables listing, filtering, pagination, and time-series queries that aren't possible with on-chain calls alone:
import { EnergyQuery, Network } from "energy-attestation-sdk";
const query = new EnergyQuery({
network: Network.AMOY,
apiKey: process.env.GRAPH_API_KEY,
});
// Global protocol stats
const protocol = await query.getProtocol();
console.log(`Total projects: ${protocol.totalProjects}`);
console.log(`Total generated: ${protocol.totalGeneratedWh} Wh`);
// List all registered watchers
const watchers = await query.getWatchers({ registered: true });
// Get a single watcher with its projects, attesters, and ownership history
const watcher = await query.getWatcher("1");
// List projects for a watcher, ordered by total generation
const projects = await query.getProjects({
watcherId: "1",
orderBy: "totalGeneratedWh",
orderDirection: "desc",
});
// All active attestations for a project within a time range
const attestations = await query.getAttestations({
projectId: "42",
replaced: false,
fromTimestamp_gte: "1700000000",
fromTimestamp_lte: "1710000000",
});
// Daily energy snapshots for charting
const snapshots = await query.getDailySnapshots({
projectId: "42",
dateFrom: "2025-01-01",
dateTo: "2025-01-31",
});
// => [{ date: "2025-01-01", generatedWh: "1250000", consumedWh: "0", attestationCount: 24 }, ...]
// List all authorized attesters for a project
const attesters = await query.getProjectAttesters("42", { active: true });Note:
EnergyQueryrequires a deployed subgraph for the target network. Amoy is supported out of the box. For Polygon and Celo pass an explicitsubgraphUrlonce the subgraph is deployed on those networks.
Managing attesters
Watcher owners control who can submit readings. Authorization works at two levels:
// --- Per-project authorization ---
// Authorize specific devices/wallets for a single project
await sdk.attesters.addAttester(projectId, "0xIoTDevice1...");
await sdk.attesters.addAttester(projectId, "0xAuditor...");
// Batch operations (single transaction)
await sdk.attesters.addAttesters(projectId, ["0xDevice1...", "0xDevice2...", "0xDevice3..."]);
// --- Watcher-wide authorization ---
// Authorize a wallet across ALL projects under a watcher
await sdk.attesters.addWatcherAttester(watcherId, "0xCompanyBackend...");
// --- Revoke access ---
await sdk.attesters.removeAttester(projectId, "0xOldDevice...");Configuration
The SDK supports two initialization methods:
EnergySDK.fromPrivateKey(config)— for Node.js scripts, IoT devices, and backendsEnergySDK.fromSigner(config)— for browser wallets (MetaMask, WalletConnect) and multisig signers
PrivateKeySDKConfig
| Field | Type | Required | Description |
| ----------------- | ------------- | -------- | ---------------------------------------------------------------- |
| privateKey | string | Yes | Hex-encoded private key (with or without 0x) |
| network | Network | Yes | Target network — determines all default addresses and RPC |
| rpcUrl | string | No | JSON-RPC endpoint URL. Defaults to the network's public RPC. |
| registryAddress | string | No | EnergyRegistry proxy address. Auto-resolved if available. |
| schemaUID | string | No | EAS schema UID (bytes32). Auto-resolved if available. |
| easAddress | string | No | EAS core contract address. Auto-resolved if available. |
| tx | TxFeeConfig | No | Optional fee policy for write transactions (EIP-1559 overrides). |
Note:
registryAddressmust always point to the proxy address (the permanent address printed bydeploy.ts), never the implementation address.
SignerSDKConfig
| Field | Type | Required | Description |
| ----------------- | ---------------- | -------- | ---------------------------------------------------------------- |
| signer | AbstractSigner | Yes | An ethers.js signer with an attached provider |
| network | Network | Yes | Target network — determines all default addresses |
| registryAddress | string | No | EnergyRegistry proxy address. Auto-resolved if available. |
| schemaUID | string | No | EAS schema UID (bytes32). Auto-resolved if available. |
| easAddress | string | No | EAS core contract address. Auto-resolved if available. |
| tx | TxFeeConfig | No | Optional fee policy for write transactions (EIP-1559 overrides). |
rpcUrlis not needed forfromSigner— the signer carries its own provider.Requirements: The signer must have an attached provider (
signer.provider !== null). If using a bareWalletinstance without a provider, attach one first:wallet.connect(provider). Additionally, the signer's chain ID must match the configurednetwork— if they differ,fromSignerthrows aConfigurationErrorimmediately.
Transaction Fee Policy
To avoid low-tip rejections on some RPCs/networks, the SDK applies a safe fee policy to all write calls (create*, attest*, revoke*, etc.). All supported networks use EIP-1559 fee pricing — the SDK fetches the current base fee directly via eth_getBlockByNumber (not eth_maxPriorityFeePerGas) to avoid compatibility warnings with certain wallet providers (e.g. MetaMask on Celo).
Default behavior:
minPriorityFeeGwei: 25maxFeeMultiplier: 2
Optional config type:
type TxFeeConfig = {
minPriorityFeeGwei?: number; // minimum priority fee (tip) in gwei — EIP-1559 networks only
maxFeeMultiplier?: number; // multiplier applied to the current base fee
retryCount?: number; // retry attempts for send failures (default: 0)
retryDelayMs?: number; // initial delay in ms; doubles each retry (default: 1000)
};Example override:
const sdk = await EnergySDK.fromSigner({
signer,
network: Network.CELO,
tx: {
minPriorityFeeGwei: 30,
maxFeeMultiplier: 2,
},
});EnergyQueryConfig
| Field | Type | Required | Description |
| ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| network | Network | Yes | Target network — determines the default subgraph URL |
| apiKey | string | Yes* | Your personal API key from The Graph Studio. Required to query the hosted subgraphs. |
| subgraphUrl | string | No | Override the default subgraph URL (e.g. for self-hosted deployments) |
* Each developer brings their own free API key. The subgraphs are shared infrastructure — you don't need to deploy your own.
const query = new EnergyQuery({
network: Network.AMOY,
apiKey: process.env.GRAPH_API_KEY,
});
const query = new EnergyQuery({
network: Network.POLYGON,
apiKey: process.env.GRAPH_API_KEY,
});
const query = new EnergyQuery({
network: Network.CELO,
apiKey: process.env.GRAPH_API_KEY,
});Supported Networks
| Network | Enum | Registry | Schema UID | Subgraph |
| ---------------------- | ----------------- | -------------------------------------------- | -------------------------------------------------------------------- | -------- |
| Celo Mainnet | Network.CELO | 0xA5B5f895091d79d1f099531cDB8cb896F17ec4C1 | 0xb9c136082a935b39c6e276ea137ac489bdc090aac17a116347c7ea90442ef7e0 | ✅ Live |
| Polygon Mainnet | Network.POLYGON | 0xA5B5f895091d79d1f099531cDB8cb896F17ec4C1 | 0xb9c136082a935b39c6e276ea137ac489bdc090aac17a116347c7ea90442ef7e0 | ✅ Live |
| Polygon Amoy (testnet) | Network.AMOY | 0x059D4655941204cf6aaC1cF578Aa9dc5D3ed6B39 | 0x4673141c77c3d54962edf6ef7f25a0c62656f9bd08138b4c4f9561413c235435 | ✅ Live |
All three networks are fully supported — all addresses and subgraph URLs are auto-resolved, zero config needed.
Init examples
// --- Private key (scripts, IoT, backends) ---
// Simplest — everything auto-resolved (Amoy)
const sdk = await EnergySDK.fromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
network: Network.AMOY,
});
// Production — custom RPC to avoid rate limits
const sdk = await EnergySDK.fromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
network: Network.CELO,
rpcUrl: "https://my-alchemy-endpoint.com",
});
// --- Browser wallet (MetaMask, WalletConnect, etc.) ---
import { BrowserProvider } from "ethers";
const signer = await new BrowserProvider(window.ethereum).getSigner();
const sdk = await EnergySDK.fromSigner({
signer,
network: Network.CELO,
});API Reference
Modules
The SDK is organized into five focused modules, each accessible as a property on the SDK instance:
sdk.watchers; // WatcherModule — create and manage watchers (organizations)
sdk.projects; // ProjectModule — create and manage energy projects
sdk.attesters; // AttesterModule — manage attester authorization
sdk.attestations; // AttestationModule — submit and correct energy readings
sdk.energyTypes; // EnergyTypeModule — register and manage energy types (admin only)
sdk.read; // ReadModule — query on-chain state (view functions, no gas)Additionally, the SDK exposes the underlying ethers.js objects for advanced use:
sdk.address; // The signer's wallet address (string)
sdk.signer; // The ethers.js AbstractSigner instance
sdk.provider; // The ethers.js Provider instance
sdk.network; // Resolved SDK network enum valuesdk.watchers
| Method | Returns | Description |
| ---------------------------------------------------------- | ----------------------------- | ------------------------------------------- |
| createWatcher(name) | { watcherId, name, txHash } | Register a new watcher (permissionless) |
| transferWatcherOwnership(watcherId, newOwner) | { txHash } | Transfer watcher to a new owner |
| estimateCreateWatcherGas(name) | bigint | Estimate gas for createWatcher |
| estimateTransferWatcherOwnershipGas(watcherId, newOwner) | bigint | Estimate gas for transferWatcherOwnership |
sdk.projects
| Method | Returns | Description |
| -------------------------------------------- | ----------------------------------------- | -------------------------------------------------- |
| createProject(watcherId, name, energyType) | { projectId, name, energyType, txHash } | Register a project under a watcher |
| deregisterProject(projectId) | { txHash } | Deactivate a project (no new attestations allowed) |
| transferProject(projectId, toWatcherId) | { txHash } | Move a project to a different watcher |
| setProjectMetadataURI(projectId, uri) | { txHash } | Set/update IPFS or HTTPS metadata link |
sdk.attesters
| Method | Returns | Description |
| -------------------------------------------- | ------------ | --------------------------------------------- |
| addAttester(projectId, attester) | { txHash } | Authorize a wallet for a specific project |
| removeAttester(projectId, attester) | { txHash } | Revoke project-level authorization |
| addAttesters(projectId, attesters[]) | { txHash } | Batch authorize multiple wallets |
| removeAttesters(projectId, attesters[]) | { txHash } | Batch revoke multiple wallets |
| addWatcherAttester(watcherId, attester) | { txHash } | Authorize across all projects under a watcher |
| removeWatcherAttester(watcherId, attester) | { txHash } | Revoke watcher-wide authorization |
sdk.energyTypes
Admin-only operations for managing the energy type registry. The caller must be the current energy type admin (see sdk.read.getEnergyTypeAdmin()).
| Method | Returns | Description |
| --------------------------------------------- | ------------ | ------------------------------------------ |
| registerEnergyType(id, name) | { txHash } | Register a new energy type (id 1–255) |
| removeEnergyType(id) | { txHash } | Deactivate an existing energy type |
| transferEnergyTypeAdmin(newAdmin) | { txHash } | Transfer the admin role to a new address |
| estimateRegisterEnergyTypeGas(id, name) | bigint | Estimate gas for registerEnergyType |
| estimateRemoveEnergyTypeGas(id) | bigint | Estimate gas for removeEnergyType |
| estimateTransferEnergyTypeAdminGas(address) | bigint | Estimate gas for transferEnergyTypeAdmin |
Energy type ID
0is permanently reserved for consumer projects and cannot be registered.
sdk.attestations
| Method | Returns | Description |
| ------------------------------ | -------------------- | ----------------------------------------------------------------- |
| attest(params) | { uid, txHash } | Submit new energy readings for a project |
| overwriteAttestation(params) | { uid, txHash } | Replace a previous attestation (correction flow) |
| attestBatch(params[]) | { uids[], txHash } | Submit multiple attestations in a single transaction |
| attestZeroPeriod(params) | { uid, txHash } | Record a zero-energy period; replaceable later with real readings |
| revokeAttestation(uid) | { txHash } | Invalidate an attestation without a replacement |
AttestParams
{
projectId: number; // The project to submit readings for
readings: bigint[]; // Energy in Wh per interval (must be non-negative)
readingIntervalMinutes: Interval | number; // Minutes between readings (e.g., Interval.Hourly or 60)
fromTimestamp: number | bigint; // Start of period (Unix seconds)
method: string; // "manual" | "iot" | "estimated"
metadataURI?: string; // Optional IPFS/HTTPS link to evidence
}OverwriteAttestParams
Extends AttestParams with:
refUID: string— the bytes32 UID of the attestation being replaced.
The replacement must cover the exact same time period as the original (same
fromTimestampand derivedtoTimestamp).
ZeroPeriodParams
{
projectId: number; // The project to record the zero period for
interval: Interval; // Length of the period (e.g., Interval.Daily)
method?: string; // Defaults to "0 report"
metadataURI?: string;
}fromTimestamp is auto-fetched from the project's last attested timestamp. The replacement can use any interval combination that covers the same total duration — for example, a Interval.Daily zero period can later be replaced with 24 hourly readings.
Prerequisite: The project must have at least one prior attestation. If
getProjectLastTimestamp()returns0(no prior attestations),attestZeroPeriodthrows aConfigurationError. Submit a regularattest()call first to establish the chain.
BatchAttestResult
{
uids: string[]; // EAS UIDs for each submitted attestation (order matches input params)
txHash: string; // Transaction hash
}Revoking an attestation
Use revokeAttestation to permanently invalidate a specific attestation without providing a replacement. Once revoked, the EAS UID is marked as revoked on-chain.
Important: The
EnergyAttestationResolverblocks direct revocations (DirectRevocationBlocked) to preserve the sequential attestation chain.revokeAttestationwill revert on-chain if a resolver is attached to the schema. UseoverwriteAttestationwhenever you have corrected data to submit. Only callrevokeAttestationin cases where no replacement data exists — for example, to invalidate a fraudulent or test attestation on a schema without a blocking resolver.
const { txHash } = await sdk.attestations.revokeAttestation(
"0xabc...def", // EAS UID of the attestation to revoke
);sdk.read
All methods are gas-free view calls — they query on-chain state directly without sending transactions:
For listing, filtering, pagination, time-series, and aggregated queries, use
EnergyQueryinstead.
| Method | Returns | Description |
| --------------------------------------------- | ----------------- | ----------------------------------------------------------------------- |
| getWatcher(watcherId) | Watcher | Watcher metadata (owner, name, active) |
| getProject(projectId) | Project | Project metadata (watcher, name, energy type) |
| isProjectRegistered(projectId) | boolean | Whether a project is active |
| isWatcherRegistered(watcherId) | boolean | Whether a watcher is active |
| getProjectLastTimestamp(projectId) | bigint | Chain tip — where next attestation must start |
| getTotalGeneratedEnergy(projectId) | bigint | Cumulative Wh generated by a project |
| getTotalConsumedEnergy(projectId) | bigint | Cumulative Wh consumed by a project |
| getTotalGeneratedEnergyByWatcher(watcherId) | bigint | Cumulative Wh generated across all projects |
| getTotalConsumedEnergyByWatcher(watcherId) | bigint | Cumulative Wh consumed across all projects |
| getWatcherProjects(watcherId) | bigint[] | All project IDs under a watcher |
| isProjectAttester(projectId, attester) | boolean | Check project-level authorization |
| isWatcherAttester(watcherId, attester) | boolean | Check watcher-level authorization |
| getProjectMetadataURI(projectId) | string | IPFS/HTTPS metadata link |
| getProjectEnergyType(projectId) | number | Energy type ID (0 = consumer, 1+ = generator) |
| getReplacementUID(uid) | string | Follow the replacement chain for audit |
| getAttestedPeriodUID(projectId, from, to) | string | Look up which attestation covers a period |
| getAttestedPeriodStartUID(projectId, from) | string | Look up attestation by start timestamp only |
| getNextProjectId() | bigint | Next project ID that will be assigned |
| getNextWatcherId() | bigint | Next watcher ID that will be assigned |
| getProjectWatcherId(projectId) | bigint | Which watcher owns a project |
| getProjectType(projectId) | number | Raw energy type ID for a project |
| isAuthorizedResolver(resolver) | boolean | Check if an address is an authorized resolver |
| isEnergyTypeRegistered(id) | boolean | Check if an energy type ID is registered |
| getEnergyTypeName(id) | string | Human-readable name for an energy type ID |
| getEnergyTypeAdmin() | string | Address of the energy type admin |
| getWatcherProjectsWithDetails(watcherId) | Project[] | All projects under a watcher with full metadata |
| getAttestationData(uid) | AttestationData | Fetch and decode attestation data by EAS UID |
| getProjectStats(projectId) | ProjectStats | Project metadata + energy totals + last timestamp in one call |
| getWatcherStats(watcherId) | WatcherStats | Watcher metadata + generated and consumed totals in one call |
| getOwner() | string | Current owner of the EnergyRegistry contract |
| getPendingOwner() | string | Pending owner during a 2-step ownership transfer (zero address if none) |
AttestationData
The decoded attestation data returned by getAttestationData(uid):
{
projectId: bigint;
readingCount: number;
readingIntervalMinutes: number;
readings: bigint[]; // Individual Wh readings
fromTimestamp: bigint; // Period start (Unix seconds)
method: string; // "manual" | "iot" | "estimated"
metadataURI: string; // "" if not set
}// Fetch and decode an attestation directly from the chain
const data = await sdk.read.getAttestationData("0xabc...def");
console.log(data.projectId); // 1n
console.log(data.readings); // [1500n, 1800n, 2100n]
console.log(data.fromTimestamp); // 1700000000nProjectStats
{
project: Project; // Metadata (name, watcherId, energyType, registered)
totalGenerated: bigint; // Cumulative Wh generated
totalConsumed: bigint; // Cumulative Wh consumed
lastTimestamp: bigint; // Chain tip — where next attestation must start
metadataURI: string; // IPFS/HTTPS metadata link
}WatcherStats
{
watcher: Watcher; // Metadata (owner, name, registered)
totalGenerated: bigint; // Cumulative Wh generated across all projects
totalConsumed: bigint; // Cumulative Wh consumed across all projects
}EnergyQuery
EnergyQuery is a standalone class — independent of EnergySDK, no private key required — for querying the indexed subgraph. All methods return typed entities and support filtering and pagination.
BigInt fields (energy totals, timestamps, block numbers) are returned as
stringto avoid JavaScript precision loss. Convert withBigInt(value)when arithmetic is needed.
Protocol
| Method | Returns | Description |
| --------------- | -------------------------- | ---------------------------------------------- |
| getProtocol() | SubgraphProtocol \| null | Global stats: total watchers, projects, energy |
Energy Types
| Method | Returns | Description |
| ------------------ | ---------------------- | --------------------------------------- |
| getEnergyTypes() | SubgraphEnergyType[] | All registered energy types with totals |
Watchers
| Method | Returns | Description |
| --------------------------------------- | ------------------------------------ | -------------------------------------------------------------- |
| getWatcher(id) | SubgraphWatcherDetail \| null | Single watcher with projects, attesters, and ownership history |
| getWatchers(filters?) | PageResult<SubgraphWatcher> | Paged watcher list with items and hasMore |
| iterateWatchers(filters?) | AsyncGenerator<SubgraphWatcher> | Async iterator across all pages matching filters |
| getWatcherOwnershipHistory(watcherId) | SubgraphWatcherOwnershipTransfer[] | Full ownership transfer history for a watcher |
WatcherFilters
| Field | Type | Default |
| ---------------- | -------------------------------------------------------------------------- | ------------- |
| first | number | 100 |
| skip | number | 0 |
| orderBy | "createdAt" \| "totalGeneratedWh" \| "totalConsumedWh" \| "projectCount" | "createdAt" |
| orderDirection | "asc" \| "desc" | "desc" |
| registered | boolean | — |
| owner | string | — |
Projects
| Method | Returns | Description |
| --------------------------- | --------------------------------- | ------------------------------------------------ |
| getProject(id) | SubgraphProject \| null | Single project with watcher and energy type info |
| getProjects(filters?) | PageResult<SubgraphProject> | Paged project list with items and hasMore |
| iterateProjects(filters?) | AsyncGenerator<SubgraphProject> | Async iterator across all pages matching filters |
ProjectFilters
| Field | Type | Default |
| ---------------- | ------------------------------------------------------------------------------ | ------------- |
| first | number | 100 |
| skip | number | 0 |
| orderBy | "createdAt" \| "totalGeneratedWh" \| "totalConsumedWh" \| "attestationCount" | "createdAt" |
| orderDirection | "asc" \| "desc" | "desc" |
| watcherId | string | — |
| energyTypeId | string — use "0" for consumers, "1"–"13" for generators | — |
| registered | boolean | — |
Attestations
| Method | Returns | Description |
| ------------------------------- | ------------------------------------------- | ------------------------------------------------- |
| getAttestation(uid) | SubgraphEnergyAttestation \| null | Single attestation by EAS UID (bytes32 hex) |
| getAttestations(filters?) | PageResult<SubgraphEnergyAttestation> | Paged attestation list with items and hasMore |
| iterateAttestations(filters?) | AsyncGenerator<SubgraphEnergyAttestation> | Async iterator across all pages matching filters |
AttestationFilters
| Field | Type | Default |
| ------------------- | -------------------------------------------------------------- | ----------------- |
| first | number | 100 |
| skip | number | 0 |
| orderBy | "fromTimestamp" \| "blockTimestamp" \| "energyWh" | "fromTimestamp" |
| orderDirection | "asc" \| "desc" | "asc" |
| projectId | string | — |
| attester | string | — |
| replaced | boolean — false returns only active attestations | — |
| fromTimestamp_gte | string — Unix seconds | — |
| fromTimestamp_lte | string — Unix seconds | — |
| toTimestamp_gte | string — Unix seconds | — |
| toTimestamp_lte | string — Unix seconds | — |
| energyTypeId | string — use "0" for consumer, "1"–"13" for generators | — |
Daily Snapshots
| Method | Returns | Description |
| -------------------------------- | --------------------------------------- | --------------------------------------------- |
| getDailySnapshots(filters) | SubgraphDailySnapshot[] | Day-bucketed energy totals, useful for charts |
| iterateDailySnapshots(filters) | AsyncGenerator<SubgraphDailySnapshot> | Async iterator for large date ranges |
DailySnapshotFilters
| Field | Type | Default | Required |
| ---------------- | --------------------- | ------- | -------- |
| projectId | string | — | Yes |
| dateFrom | string (YYYY-MM-DD) | — | No |
| dateTo | string (YYYY-MM-DD) | — | No |
| first | number | 365 | No |
| skip | number | 0 | No |
| orderDirection | "asc" \| "desc" | "asc" | No |
Attesters
| Method | Returns | Description |
| ------------------------------------------ | --------------------------- | ------------------------------------------- |
| getProjectAttesters(projectId, filters?) | SubgraphProjectAttester[] | Attesters authorized for a specific project |
| getWatcherAttesters(watcherId, filters?) | SubgraphWatcherAttester[] | Attesters authorized at the watcher level |
Both accept an optional AttesterFilters with { first?, skip?, active? }.
Gas Estimation
Every write method has a corresponding estimate*Gas variant that performs a dry-run simulation and returns the estimated gas units as a bigint — without sending a transaction. This is useful for fee budgeting, preflight checks, or surfacing authorization errors before committing gas.
// Estimate gas before attesting
const gas = await sdk.attestations.estimateAttestGas({
projectId: 1,
readings: [1500n, 1800n],
readingIntervalMinutes: 60,
fromTimestamp: 1700000000,
method: "iot",
});
console.log(`Estimated gas: ${gas}`); // e.g. 120000n
// Estimate gas for a correction
const gas = await sdk.attestations.estimateOverwriteAttestationGas({
...params,
refUID: "0xabc...def",
});Gas estimation methods apply the same input validation as their transaction counterparts and decode contract reverts into typed errors — so calling estimateAttestGas for an unauthorized attester throws a ContractRevertError with errorName: "UnauthorizedAttester", the same as attest would.
Available estimate methods
sdk.watchers
| Method | Estimates gas for |
| ---------------------------------------------------------- | ------------------------------- |
| estimateCreateWatcherGas(name) | createWatcher(name) |
| estimateTransferWatcherOwnershipGas(watcherId, newOwner) | transferWatcherOwnership(...) |
sdk.attestations
| Method | Estimates gas for |
| ----------------------------------------- | ------------------------------ |
| estimateAttestGas(params) | attest(params) |
| estimateOverwriteAttestationGas(params) | overwriteAttestation(params) |
| estimateAttestBatchGas(params[]) | attestBatch(params[]) |
| estimateAttestZeroPeriodGas(params) | attestZeroPeriod(params) |
| estimateRevokeAttestationGas(uid) | revokeAttestation(uid) |
sdk.projects
| Method | Estimates gas for |
| -------------------------------------------------- | ------------------------------ |
| estimateCreateProjectGas(watcherId, name, type) | createProject(...) |
| estimateDeregisterProjectGas(projectId) | deregisterProject(projectId) |
| estimateTransferProjectGas(projectId, watcherId) | transferProject(...) |
| estimateSetProjectMetadataURIGas(projectId, uri) | setProjectMetadataURI(...) |
sdk.attesters
| Method | Estimates gas for |
| ------------------------------------------------------- | ---------------------------- |
| estimateAddAttesterGas(projectId, attester) | addAttester(...) |
| estimateRemoveAttesterGas(projectId, attester) | removeAttester(...) |
| estimateAddAttestersGas(projectId, attesters[]) | addAttesters(...) |
| estimateRemoveAttestersGas(projectId, attesters[]) | removeAttesters(...) |
| estimateAddWatcherAttesterGas(watcherId, attester) | addWatcherAttester(...) |
| estimateRemoveWatcherAttesterGas(watcherId, attester) | removeWatcherAttester(...) |
sdk.energyTypes
| Method | Estimates gas for |
| --------------------------------------------- | ------------------------------ |
| estimateRegisterEnergyTypeGas(id, name) | registerEnergyType(...) |
| estimateRemoveEnergyTypeGas(id) | removeEnergyType(id) |
| estimateTransferEnergyTypeAdminGas(address) | transferEnergyTypeAdmin(...) |
Error Handling
The SDK decodes on-chain revert reasons into typed errors. Contract errors from both the resolver (validation layer) and the registry (state layer) are automatically identified and surfaced:
import { ContractRevertError, ConfigurationError, ContractErrorCode } from "energy-attestation-sdk";
try {
await sdk.attestations.attest(params);
} catch (error) {
if (error instanceof ContractRevertError) {
console.error(`Contract error: ${error.errorName}`);
console.error(`Source: ${error.source}`); // "resolver" | "registry" | "unknown"
console.error(`Args: ${JSON.stringify(error.errorArgs)}`);
console.error(`Raw data: ${error.rawData}`);
}
}ContractErrorCode
Use ContractErrorCode for safe, refactor-proof error matching instead of comparing raw strings:
import { ContractRevertError, ContractErrorCode } from "energy-attestation-sdk";
try {
await sdk.attestations.attest(params);
} catch (error) {
if (error instanceof ContractRevertError) {
switch (error.errorName) {
case ContractErrorCode.ProjectNotRegistered:
console.error("Project does not exist or was deregistered");
break;
case ContractErrorCode.NonSequentialAttestation:
console.error("Gap in attestation sequence — fromTimestamp must follow last attestation");
break;
case ContractErrorCode.UnauthorizedAttester:
console.error("This wallet is not authorized to attest for this project");
break;
case ContractErrorCode.PeriodAlreadyAttested:
console.error("This time period already has an attestation — use overwriteAttestation");
break;
}
}
}Error types
| Error | When |
| --------------------- | ------------------------------------------------------------------ |
| ConfigurationError | Invalid SDK config or method params (caught before sending tx) |
| ContractRevertError | On-chain revert with decoded error name, args, and source contract |
| TransactionError | Transaction failure without decodable revert data |
ConfigurationError conditions
ConfigurationError is thrown before any transaction is sent. Common triggers:
| Scenario | Method(s) | Message |
| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| Invalid Ethereum address passed as attester | addAttester, removeAttester, addAttesters, removeAttesters, addWatcherAttester, removeWatcherAttester | "Invalid address: ..." |
| Invalid newOwner address | transferWatcherOwnership | "Invalid address: ..." |
| fromSigner called with a signer that has no attached provider | EnergySDK.fromSigner | "Signer must have an attached provider" |
| fromSigner signer's chain ID doesn't match the configured network | EnergySDK.fromSigner | "Signer chain ID ... does not match expected ..." |
| attestZeroPeriod called when the project has no prior attestation (chain tip is 0) | attestZeroPeriod, estimateAttestZeroPeriodGas | "No prior attestation found for project ..." |
| overwriteAttestation refUID doesn't exist on-chain | overwriteAttestation, estimateOverwriteAttestationGas | "Original attestation not found: ..." |
| overwriteAttestation original was already replaced | overwriteAttestation, estimateOverwriteAttestationGas | "Original attestation has already been replaced" |
| overwriteAttestation replacement period doesn't match original | overwriteAttestation, estimateOverwriteAttestationGas | "Period mismatch: ..." |
| Any value in readings is negative | attest, overwriteAttestation, attestBatch, and their estimate* variants | "readings[N] is negative ..." |
| Computed toTimestamp would overflow uint64 (fromTimestamp + count × interval × 60 > uint64 max) | attest, overwriteAttestation, attestBatch, and their estimate* variants | "computed toTimestamp (...) exceeds uint64 max" |
Common contract errors
| Error | Source | Meaning |
| ----------------------------- | -------- | -------------------------------------------------------------------- |
| UnauthorizedAttester | Resolver | Wallet is not whitelisted for this project |
| ProjectNotRegistered | Both | Project ID doesn't exist or was deregistered |
| NonSequentialAttestation | Registry | fromTimestamp doesn't match the previous toTimestamp |
| PeriodAlreadyAttested | Registry | This exact time period already has an attestation |
| PeriodStartAlreadyAttested | Registry | An attestation already starts at this fromTimestamp |
| InvalidReadingsLength | Resolver | readings.length doesn't match readingCount |
| InvalidReadingCount | Resolver | Reading count is zero |
| InvalidReadingInterval | Resolver | readingIntervalMinutes is zero |
| InvalidMethod | Resolver | Empty method string |
| InvalidTimestamps | Resolver | Derived toTimestamp is not after fromTimestamp |
| ReplacementPeriodMismatch | Both | Replacement attestation covers a different period than original |
| ReplacementProjectMismatch | Resolver | Replacement references an attestation from a different project |
| AttestationAlreadyReplaced | Registry | The referenced attestation was already replaced once |
| AttestationNotFound | Registry | The referenced refUID does not exist on-chain |
| DirectRevocationBlocked | Registry | Direct revocation is blocked; use overwriteAttestation instead |
| WatcherNotRegistered | Registry | Watcher ID doesn't exist |
| UnauthorizedWatcherOwner | Registry | Caller is not the watcher's owner |
| AttesterAlreadyAuthorized | Registry | Attester is already whitelisted for this project |
| AttesterNotAuthorized | Registry | Attester is not whitelisted; cannot remove a wallet that isn't added |
| EmptyAttesterArray | Registry | Batch add/remove called with an empty array |
| EnergyTypeNotRegistered | Registry | The energy type ID passed to createProject is not registered |
| InvalidEnergyType | Registry | Energy type ID is out of the valid range |
| UnauthorizedResolver | Registry | Caller is not an authorized resolver (registry-level guard) |
| UnauthorizedEnergyTypeAdmin | Registry | Caller is not the energy type admin |
| OwnableUnauthorizedAccount | Registry | Caller is not the contract owner (e.g. calling upgradeToAndCall) |
| InsufficientValue | Resolver | Resolver requires ETH payment but none was sent |
| InvalidEAS | Resolver | Invalid EAS address passed to resolver constructor |
| InvalidLength | Resolver | Encoded attestation data has wrong length |
| AccessDenied | Resolver | Resolver access control check failed |
| EnforcedPause | Resolver | Resolver is paused; attestations temporarily blocked |
| TimestampOverflow | Resolver | Timestamp arithmetic overflowed uint64 |
Energy Types
Projects are classified by their energy source at registration. This classification is permanent and determines how readings are accumulated:
- Consumer (
energyType = 0) — readings flow into the consumed accumulator - Generator (
energyType = 1–13) — readings flow into the generated accumulator
import { EnergyType } from "energy-attestation-sdk";
// Consumer
EnergyType.CONSUMER; // 0 — grid import, operational load
// Generators
EnergyType.SOLAR_PV; // 1
EnergyType.WIND_ONSHORE; // 2
EnergyType.WIND_OFFSHORE; // 3
EnergyType.HYDRO; // 4
EnergyType.BIOMASS; // 5
EnergyType.GEOTHERMAL; // 6
EnergyType.OCEAN_TIDAL; // 7
EnergyType.NUCLEAR; // 8
EnergyType.NATURAL_GAS; // 9
EnergyType.COAL; // 10
EnergyType.OIL; // 11
EnergyType.STORAGE_DISCHARGE; // 12
EnergyType.HYDROGEN_FUEL_CELL; // 13New energy types can be added on-chain by the energy type admin without redeploying contracts.
Interval presets
You can use the exported Interval enum to avoid minute-conversion mistakes:
import { Interval } from "energy-attestation-sdk";
Interval.Hourly; // 60
Interval.FourHours; // 240
Interval.EightHours; // 480
Interval.TwelveHours; // 720
Interval.Daily; // 1440
Interval.Weekly; // 10080
Interval.Biweekly; // 20160
Interval.FourWeeks; // 40320readingIntervalMinutes still accepts custom minute values when needed.
Utilities
The SDK exports low-level helpers for advanced use cases such as custom integrations, off-chain verification, or building your own transaction flow:
import {
encodeAttestationData, // ABI-encode attestation params into bytes
decodeAttestationData, // Decode bytes back into structured params
computeToTimestamp, // Derive toTimestamp from from + count * interval
sumReadings, // Sum a bigint[] of readings into total Wh
ATTESTATION_SCHEMA, // The EAS schema string
ENERGY_TYPE_NAMES, // Map of energy type IDs to human-readable names
CHAIN_IDS, // EVM chain IDs for all supported networks
} from "energy-attestation-sdk";
// Example: compute the end timestamp of a 24-hour hourly report
const toTimestamp = computeToTimestamp(1700000000, 24, 60);
// => 1700086400 (1700000000 + 24 * 60 * 60)
// Example: sum readings to get total energy
const total = sumReadings([1500n, 1800n, 2100n, 1900n]);
// => 7300n Wh
// Example: switch MetaMask to the correct network before signing
import { CHAIN_IDS, Network } from "energy-attestation-sdk";
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: "0x" + CHAIN_IDS[Network.CELO].toString(16) }],
});Architecture
The SDK exposes two independent entry points:
┌─────────────────────────────────────────────────────────────┐
│ EnergySDK │
│ (requires private key) │
│ │
│ sdk.attestations.attest() │
│ │ │
│ ▼ │
│ ABI-encode data ──► EAS.attest() ──► on-chain tx │
│ │
│ sdk.watchers / sdk.projects / sdk.attesters │
│ │ │
│ ▼ │
│ EnergyRegistry.method() ──► on-chain tx │
│ │
│ sdk.read.* │
│ │ │
│ ▼ │
│ EnergyRegistry.view() ──► no gas, read-only │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ EnergyQuery │
│ (no private key required) │
│ │
│ query.getAttestations() / getProjects() / getDailySnapshots│
│ │ │
│ ▼ │
│ GraphQL ──► The Graph subgraph ──► indexed on-chain data │
└─────────────────────────────────────────────────────────────┘On-chain contracts:
- EnergyRegistry — Permanent state contract deployed behind a UUPS proxy. The proxy address never changes — always use the proxy address as
registryAddressin SDK config. Stores watchers, projects, attester whitelists, and cumulative energy totals. Persists across both resolver and registry implementation upgrades. - EnergyAttestationResolver — EAS resolver that validates attestation data (readings, timestamps, authorization) and delegates state writes to the registry. Stateless and replaceable without data migration.
Key design decisions:
- Attestations are submitted through EAS's
attest()function, which triggers the resolver'sonAttesthook for validation before data is recorded. - Direct revocations are blocked to preserve the sequential attestation chain. Corrections are always done via the replacement mechanism (
overwriteAttestation). - The resolver can be upgraded independently — just deploy a new one, authorize it on the registry, and pause the old one. No data migration needed.
EnergyQueryis fully independent — frontends, dashboards, and analytics tools can query indexed data without a wallet or RPC connection.
License
MIT
