Documentation

Submitting Energy Records

Energy records (called Attestations in the protocol) are the core data unit of EnergyAS — permanent, verifiable readings that anyone can audit.

Note

For a complete working example including setup steps, see the End-to-End Integration tutorial.

Prepare Your Data

Each record contains interval readings in watt-hours (Wh). For example, 24 hourly readings from a solar farm:

JavaScript
const readings = [
  0n, 0n, 0n, 0n, 0n, 0n,
  15_000n, 45_000n, 120_000n, 280_000n, 410_000n, 520_000n,
  580_000n, 590_000n, 560_000n, 480_000n, 350_000n, 200_000n,
  80_000n, 20_000n, 0n, 0n, 0n, 0n,
];

const fromTimestamp = Math.floor(new Date('2025-01-15T00:00:00Z').getTime() / 1000);

Key rules:

  • Readings are bigint values in watt-hours — not kWh, not floats
  • The period covered is fromTimestamp + readingCount × intervalMinutes × 60 seconds
  • The same period cannot be submitted twice for the same project
  • Each submission must start exactly where the previous one ended (no gaps)

Submit a Single Record

JavaScript
import { EnergySDK, Interval } from 'energy-attestation-sdk';

const sdk = await EnergySDK.fromPrivateKey({
  network: 'amoy',
  privateKey: process.env.DEVICE_PRIVATE_KEY,
});

const { uid, txHash } = await sdk.attestations.attest({
  projectId: 1,
  readings,
  readingIntervalMinutes: Interval.Hourly,  // or just 60
  fromTimestamp,
  method: 'smart-meter-api',
  metadataURI: 'ipfs://QmExportHash',  // optional
});

console.log('Record UID:', uid);

Using the contract directly

The SDK handles ABI encoding internally. If you're not using the SDK, encode the data manually:

JavaScript
import { AbiCoder, ZeroAddress, ZeroHash } from 'ethers';

const data = AbiCoder.defaultAbiCoder().encode(
  ['uint64', 'uint32', 'uint32', 'uint256[]', 'uint64', 'string', 'string'],
  [projectId, readingCount, intervalMinutes, readings, fromTimestamp, 'iot', metadataURI]
);

const tx = await eas.attest({
  schema: schemaUID,
  data: {
    recipient: ZeroAddress,
    expirationTime: 0,
    revocable: true,
    refUID: ZeroHash,
    data,
    value: 0n,
  }
});

const receipt = await tx.wait();

Batch Submissions

Submit multiple records in a single transaction for gas efficiency — useful when catching up on multiple periods.

Using the SDK

JavaScript
const { uids, txHash } = await sdk.attestations.attestBatch([
  {
    projectId: 1,
    readings: [500_000n, 510_000n, 495_000n],
    readingIntervalMinutes: Interval.Hourly,
    fromTimestamp: period1Start,
    method: 'smart-meter-api',
  },
  {
    projectId: 1,
    readings: [488_000n, 502_000n, 497_000n],
    readingIntervalMinutes: Interval.Hourly,
    fromTimestamp: period2Start,
    method: 'smart-meter-api',
  },
]);

console.log('Submitted UIDs:', uids);

Using the contract directly (EAS multiAttest)

JavaScript
const items = reports.map(report => ({
  recipient: ZeroAddress,
  expirationTime: 0n,
  revocable: true,
  refUID: ZeroHash,
  data: encodeReport(report),
  value: 0n,
}));

await eas.multiAttest([{ schema: schemaUID, data: items }]);

Zero-Generation Periods

If a site had no generation during a period (planned maintenance, outage, etc.), you still need to submit a record to keep the attestation chain intact. Future submissions will be blocked if there's a gap.

Using the SDK (easiest)

attestZeroPeriod automatically picks up from the project's last attested timestamp:

JavaScript
const { uid } = await sdk.attestations.attestZeroPeriod({
  projectId: 1,
  interval: Interval.Daily,
  method: 'manual-zero-report',
  metadataURI: 'ipfs://QmMaintenanceLog',
});

Using the contract directly

JavaScript
const lastTs = await registry.getProjectLastTimestamp(projectId);

const data = AbiCoder.defaultAbiCoder().encode(
  ['uint64', 'uint32', 'uint32', 'uint256[]', 'uint64', 'string', 'string'],
  [projectId, 1, 1440, [0n], Number(lastTs), 'manual-zero-report', '']
);

await eas.attest({ schema: schemaUID, data: { ...defaults, data } });

Corrections

To correct an existing record, submit a replacement referencing the original by UID. The contract validates that the replacement covers the same project and time period, then updates the running totals by the delta. Direct revocations are blocked — corrections must always go through this mechanism to preserve the audit trail.

Using the SDK

JavaScript
const { uid: correctedUid } = await sdk.attestations.overwriteAttestation({
  refUID: originalUid,  // UID of the record being corrected
  projectId: 1,
  readings: correctedReadings,
  readingIntervalMinutes: Interval.Hourly,
  fromTimestamp,
  method: 'smart-meter-api-corrected',
  metadataURI: 'ipfs://QmCorrectionAuditReport',
});

Using the contract directly

JavaScript
const tx = await eas.attest({
  schema: schemaUID,
  data: {
    recipient: ZeroAddress,
    expirationTime: 0n,
    revocable: true,
    refUID: originalAttestationUID,  // non-zero = replacement
    data: correctedData,
    value: 0n,
  }
});

After correction, the original record is marked replaced: true in the subgraph and excluded from totals. The replacement links back to the original via replaces, and the original links forward via replacedBy — forming a full correction chain.