Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/web/src/app/api/readings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ data: page, next_cursor, total: count ?? 0 })
}

const MetadataSchema = z.object({
firmware_version: z.string().optional(),
hardware_model: z.string().optional(),
location_lat: z.number().optional(),
location_lon: z.number().optional(),
manufacturer: z.string().optional(),
})

const ReadingSchema = z.object({
meter_id: z.string().uuid({ message: 'meter_id must be a valid UUID' }),
kwh: z
Expand Down Expand Up @@ -225,6 +233,9 @@ export async function POST(req: NextRequest) {
signature_hex,
anchored: false,
minted: false,
metadata: metadata ?? null,
metadata_hash: metadataHash ? metadataHash.toString('hex') : null,
metadata_signature_hex: metadata_signature_hex ?? null,
})
.select()
.single()
Expand Down
7 changes: 7 additions & 0 deletions docs/migrations/003_meter_metadata.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Migration 003: add signed meter metadata columns to readings
alter table readings
add column if not exists metadata jsonb,
add column if not exists metadata_hash text, -- SHA-256 hex of canonical metadata JSON
add column if not exists metadata_signature_hex text; -- Ed25519 sig over metadata_hash (128 hex chars)

create index if not exists readings_metadata_hash_idx on readings(metadata_hash);
36 changes: 31 additions & 5 deletions scripts/send-reading.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
/**
* scripts/send-reading.mjs
*
* Simulate a smart meter sending a signed reading to the SolarProof API.
* Simulate a smart meter sending a signed reading (+ optional metadata) to the SolarProof API.
*
* Usage:
* node scripts/send-reading.mjs \
* --meter-id <uuid> \
* --kwh 12.5 \
* --key ./meter-key.json \
* --api http://localhost:3000
* --api http://localhost:3000 \
* [--firmware 1.2.3] \
* [--model "SolarEdge-SE7600H"] \
* [--manufacturer "SolarEdge"]
*/

import { createSign, createHash } from 'crypto'
Expand All @@ -22,6 +25,9 @@ const meterId = get('--meter-id') ?? 'test-meter-id'
const kwh = parseFloat(get('--kwh') ?? '10')
const keyFile = get('--key') ?? './meter-key.json'
const api = get('--api') ?? 'http://localhost:3000'
const firmware = get('--firmware')
const model = get('--model')
const manufacturer = get('--manufacturer')

const { private_key_hex } = JSON.parse(readFileSync(keyFile, 'utf8'))
const timestamp = Math.floor(Date.now() / 1000)
Expand All @@ -38,9 +44,14 @@ const privKeyDer = Buffer.concat([
Buffer.from('302e020100300506032b657004220420', 'hex'),
Buffer.from(private_key_hex, 'hex'),
])
const sign = createSign('ed25519')
sign.update(readingHash)
const signature = sign.sign({ key: privKeyDer, format: 'der', type: 'pkcs8' })

function signHash(hash) {
const sign = createSign('ed25519')
sign.update(hash)
return sign.sign({ key: privKeyDer, format: 'der', type: 'pkcs8' })
}

const signature = signHash(readingHash)

const body = {
meter_id: meterId,
Expand All @@ -50,6 +61,21 @@ const body = {
nonce: `sim-${meterId}-${timestamp}-${Math.floor(Math.random() * 1000000)}`,
}

// Attach optional signed metadata
const metadata = {}
if (firmware) metadata.firmware_version = firmware
if (model) metadata.hardware_model = model
if (manufacturer) metadata.manufacturer = manufacturer

if (Object.keys(metadata).length > 0) {
const canonical = JSON.stringify(metadata, Object.keys(metadata).sort())
const metadataHash = createHash('sha256').update(Buffer.from(canonical, 'utf8')).digest()
const metadataSig = signHash(metadataHash)
body.metadata = metadata
body.metadata_signature_hex = metadataSig.toString('hex')
console.log('Metadata hash:', metadataHash.toString('hex'))
}

console.log('Sending reading:', { meterId, kwh, timestamp })
console.log('Reading hash:', readingHash.toString('hex'))

Expand Down
Loading