Skip to content

Commit cc5b551

Browse files
committed
feat: view the result of attested data
1 parent f049ffe commit cc5b551

File tree

5 files changed

+877
-9
lines changed

5 files changed

+877
-9
lines changed

README.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,60 @@ attestations.forEach(att => {
274274
});
275275
```
276276

277+
#### Parsing Attestation Payloads
278+
279+
The SDK provides utilities to parse and decode signed attestation payloads:
280+
281+
```typescript
282+
import { parseAttestationPayload } from "@trufnetwork/sdk-js";
283+
import { sha256, recoverAddress } from "ethers";
284+
285+
// Get signed attestation
286+
const signedAttestation = await attestationAction.getSignedAttestation({
287+
requestTxId: result.requestTxId,
288+
});
289+
290+
// Separate signature from canonical payload
291+
const payloadBytes = signedAttestation.payload;
292+
const signatureOffset = payloadBytes.length - 65;
293+
const canonicalPayload = payloadBytes.slice(0, signatureOffset);
294+
const signature = payloadBytes.slice(signatureOffset);
295+
296+
// Verify signature and recover validator address
297+
const digest = sha256(canonicalPayload);
298+
const r = "0x" + Buffer.from(signature.slice(0, 32)).toString("hex");
299+
const s = "0x" + Buffer.from(signature.slice(32, 64)).toString("hex");
300+
const v = signature[64];
301+
const validatorAddress = recoverAddress(digest, { r, s, v });
302+
303+
console.log(`Validator: ${validatorAddress}`);
304+
305+
// Parse and decode the payload
306+
const parsed = parseAttestationPayload(canonicalPayload);
307+
308+
console.log(`Block Height: ${parsed.blockHeight}`);
309+
console.log(`Data Provider: ${parsed.dataProvider}`);
310+
console.log(`Stream ID: ${parsed.streamId}`);
311+
console.log(`Query Results: ${parsed.result.length} rows`);
312+
313+
// Access decoded query results
314+
parsed.result.forEach((row, idx) => {
315+
console.log(`Row ${idx + 1}: [timestamp: ${row.values[0]}, value: ${row.values[1]}]`);
316+
});
317+
```
318+
319+
**Parsed Structure:**
320+
- `version`: Protocol version (1 byte)
321+
- `algorithm`: Signature algorithm (0 = secp256k1)
322+
- `blockHeight`: Block height when attested (bigint)
323+
- `dataProvider`: Data provider address (hex string)
324+
- `streamId`: Stream identifier (string)
325+
- `actionId`: Action identifier (number)
326+
- `arguments`: Decoded action arguments (array)
327+
- `result`: Decoded query results as rows with `[timestamp, value]` pairs
328+
329+
**Note:** Query results are ABI-encoded as `(uint256[] timestamps, int256[] values)` where values use 18-decimal fixed-point representation.
330+
277331
#### Attestation Payload Structure
278332

279333
The signed attestation payload is a binary blob containing:
@@ -284,7 +338,7 @@ The signed attestation payload is a binary blob containing:
284338
5. Stream ID (32 bytes, length-prefixed)
285339
6. Action ID (2 bytes)
286340
7. Arguments (variable, length-prefixed)
287-
8. Result (variable, length-prefixed)
341+
8. Result (variable, ABI-encoded, length-prefixed)
288342
9. Signature (65 bytes, secp256k1)
289343

290344
This payload can be passed to EVM smart contracts for on-chain verification using `ecrecover`.
@@ -450,6 +504,7 @@ For other bundlers or serverless platforms, consult their documentation on modul
450504
| Get stream taxonomy | `composedAction.getTaxonomiesForStreams({streams, latestOnly})` |
451505
| Request attestation | `attestationAction.requestAttestation({dataProvider, streamId, actionName, args, encryptSig, maxFee})` |
452506
| Get signed attestation | `attestationAction.getSignedAttestation({requestTxId})` |
507+
| Parse attestation payload | `parseAttestationPayload(canonicalPayload)` |
453508
| List attestations | `attestationAction.listAttestations({requester, limit, offset, orderBy})` |
454509
| Get transaction event | `transactionAction.getTransactionEvent({txId})` |
455510
| List transaction fees | `transactionAction.listTransactionFees({wallet, mode, limit, offset})` |

docs/api-reference.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,134 @@ await primitiveAction.insertRecord({
747747
-**High-throughput data insertion** (independent records)
748748
-**Fire-and-forget operations** (with proper error handling)
749749

750+
## Attestation Payload Parsing
751+
752+
### `parseAttestationPayload(payload: Uint8Array): ParsedAttestationPayload`
753+
Parses and decodes a canonical attestation payload (without the 65-byte signature).
754+
755+
#### Parameters
756+
- `payload: Uint8Array` - Canonical payload bytes (full payload minus last 65 bytes)
757+
758+
#### Returns
759+
- `ParsedAttestationPayload` object with:
760+
- `version: number` - Protocol version (currently 1)
761+
- `algorithm: number` - Signature algorithm (0 = secp256k1)
762+
- `blockHeight: bigint` - Block height when attestation was created
763+
- `dataProvider: string` - Data provider Ethereum address (hex format)
764+
- `streamId: string` - Stream identifier
765+
- `actionId: number` - Action identifier
766+
- `arguments: any[]` - Decoded action arguments
767+
- `result: DecodedRow[]` - Decoded query results as rows
768+
769+
#### Example
770+
```typescript
771+
import { parseAttestationPayload } from "@trufnetwork/sdk-js";
772+
import { sha256, recoverAddress } from "ethers";
773+
774+
// Get signed attestation
775+
const attestationAction = client.loadAttestationAction();
776+
const signedAttestation = await attestationAction.getSignedAttestation({
777+
requestTxId: "0x..."
778+
});
779+
780+
// Extract canonical payload (without signature)
781+
const payloadBytes = signedAttestation.payload;
782+
const canonicalPayload = payloadBytes.slice(0, -65);
783+
const signature = payloadBytes.slice(-65);
784+
785+
// Verify signature
786+
const digest = sha256(canonicalPayload);
787+
const r = "0x" + Buffer.from(signature.slice(0, 32)).toString("hex");
788+
const s = "0x" + Buffer.from(signature.slice(32, 64)).toString("hex");
789+
const v = signature[64];
790+
const validatorAddress = recoverAddress(digest, { r, s, v });
791+
792+
// Parse payload
793+
const parsed = parseAttestationPayload(canonicalPayload);
794+
795+
console.log(`Validator: ${validatorAddress}`);
796+
console.log(`Block: ${parsed.blockHeight}`);
797+
console.log(`Provider: ${parsed.dataProvider}`);
798+
console.log(`Stream: ${parsed.streamId}`);
799+
console.log(`Results: ${parsed.result.length} rows`);
800+
801+
// Access query results
802+
parsed.result.forEach((row, idx) => {
803+
const [timestamp, value] = row.values;
804+
console.log(`Row ${idx + 1}: timestamp=${timestamp}, value=${value}`);
805+
});
806+
```
807+
808+
### Attestation Result Format
809+
810+
Query results in attestations are ABI-encoded as:
811+
```solidity
812+
abi.encode(uint256[] timestamps, int256[] values)
813+
```
814+
815+
Where:
816+
- **timestamps**: Array of Unix timestamps (uint256)
817+
- **values**: Array of 18-decimal fixed-point integers (int256)
818+
819+
Example decoded output:
820+
```javascript
821+
[
822+
{ values: ["1704067200", "77.051806494788211665"] },
823+
{ values: ["1704153600", "78.718654581755352351"] },
824+
// ...
825+
]
826+
```
827+
828+
### Complete Attestation Workflow
829+
830+
```typescript
831+
// 1. Request attestation
832+
const attestationAction = client.loadAttestationAction();
833+
const result = await attestationAction.requestAttestation({
834+
dataProvider: "0x4710a8d8f0d845da110086812a32de6d90d7ff5c",
835+
streamId: "stai0000000000000000000000000000",
836+
actionName: "get_record",
837+
args: [...],
838+
encryptSig: false,
839+
maxFee: 1000000,
840+
});
841+
842+
// 2. Wait for transaction confirmation
843+
await client.waitForTx(result.requestTxId);
844+
845+
// 3. Poll for signature (validators sign asynchronously)
846+
let signedAttestation;
847+
for (let i = 0; i < 15; i++) {
848+
try {
849+
signedAttestation = await attestationAction.getSignedAttestation({
850+
requestTxId: result.requestTxId,
851+
});
852+
if (signedAttestation.payload.length > 65) break;
853+
} catch (e) {
854+
await new Promise(resolve => setTimeout(resolve, 2000));
855+
}
856+
}
857+
858+
// 4. Parse and verify
859+
const canonicalPayload = signedAttestation.payload.slice(0, -65);
860+
const signature = signedAttestation.payload.slice(-65);
861+
862+
const digest = sha256(canonicalPayload);
863+
const validatorAddress = recoverAddress(digest, {
864+
r: "0x" + Buffer.from(signature.slice(0, 32)).toString("hex"),
865+
s: "0x" + Buffer.from(signature.slice(32, 64)).toString("hex"),
866+
v: signature[64],
867+
});
868+
869+
const parsed = parseAttestationPayload(canonicalPayload);
870+
871+
// 5. Use the verified data
872+
console.log(`✅ Verified by: ${validatorAddress}`);
873+
parsed.result.forEach(row => {
874+
console.log(`Data: ${row.values}`);
875+
});
876+
```
877+
750878
## Performance Recommendations
751879
- Use batch record insertions
752880
- Implement client-side caching

examples/attestation/index.ts

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import { NodeTNClient } from "../../src";
1616
import { Wallet, sha256, recoverAddress } from "ethers";
17+
import { parseAttestationPayload } from "../../src/util/AttestationEncoding";
1718

1819
async function main() {
1920
// ===== 1. Setup Client =====
@@ -48,13 +49,14 @@ async function main() {
4849
const dataProvider = "0x4710a8d8f0d845da110086812a32de6d90d7ff5c"; // AI Index data provider
4950
const streamId = "stai0000000000000000000000000000"; // AI Index stream
5051

51-
// Query last 7 days of data
52-
const now = Math.floor(Date.now() / 1000);
53-
const weekAgo = now - 7 * 24 * 60 * 60;
52+
// Query data from January 2024 (when the stream has data)
53+
const startTime = 1704067200; // Jan 1, 2024 00:00:00 UTC
54+
const endTime = startTime + (30 * 24 * 60 * 60); // 30 days later
5455

5556
console.log(`Data Provider: ${dataProvider}`);
5657
console.log(`Stream ID: ${streamId}`);
57-
console.log(`Time Range: ${new Date(weekAgo * 1000).toISOString()} to ${new Date(now * 1000).toISOString()}\n`);
58+
console.log(`Time Range: ${new Date(startTime * 1000).toISOString()} to ${new Date(endTime * 1000).toISOString()}`);
59+
console.log(` (Using historical data from January 2024)\n`);
5860

5961
// ===== 3. List My Recent Attestations =====
6062
console.log("===== Listing Recent Attestations =====\n");
@@ -129,7 +131,30 @@ async function main() {
129131
console.log(`✅ Validator Address: ${validatorAddress}`);
130132
console.log(` This is the address you should use in your EVM smart contract's verify() function\n`);
131133

132-
console.log(` 💡 How to use this payload:`);
134+
// ===== Parse Attestation Payload =====
135+
console.log(`===== Parsing Attestation Payload =====`);
136+
137+
const parsed = parseAttestationPayload(canonicalPayload);
138+
139+
console.log(`📋 Attestation Details:`);
140+
console.log(` Version: ${parsed.version}`);
141+
console.log(` Algorithm: ${parsed.algorithm} (0 = secp256k1)`);
142+
console.log(` Block Height: ${parsed.blockHeight}`);
143+
console.log(` Data Provider: ${parsed.dataProvider}`);
144+
console.log(` Stream ID: ${parsed.streamId}`);
145+
console.log(` Action ID: ${parsed.actionId}\n`);
146+
147+
console.log(`📊 Attested Query Result (from get_record):`);
148+
if (parsed.result.length === 0) {
149+
console.log(` No records found`);
150+
} else {
151+
console.log(` Found ${parsed.result.length} row(s):\n`);
152+
parsed.result.forEach((row, idx) => {
153+
console.log(` Row ${idx + 1}: ${JSON.stringify(row.values)}`);
154+
});
155+
}
156+
157+
console.log(`\n 💡 How to use this payload:`);
133158
console.log(` 1. Send this hex payload to your EVM smart contract`);
134159
console.log(` 2. The contract can verify the signature using ecrecover`);
135160
console.log(` 3. Parse the payload to extract the attested query results`);
@@ -154,8 +179,8 @@ async function main() {
154179
args: [
155180
dataProvider,
156181
streamId,
157-
weekAgo,
158-
now,
182+
startTime,
183+
endTime,
159184
null, // frozen_at (not used)
160185
false, // use_cache (will be forced to false for determinism)
161186
],
@@ -215,6 +240,48 @@ async function main() {
215240
console.log(`First 64 bytes (hex): ${Buffer.from(signedAttestation.payload.slice(0, 64)).toString("hex")}`);
216241
console.log(`Last 65 bytes (signature): ${Buffer.from(signedAttestation.payload.slice(-65)).toString("hex")}`);
217242
console.log(`Full payload (hex): ${Buffer.from(signedAttestation.payload).toString("hex")}\n`);
243+
244+
// ===== Parse and Display the Attestation =====
245+
try {
246+
console.log(`===== Parsing Attestation Payload =====`);
247+
248+
const signatureOffset = signedAttestation.payload.length - 65;
249+
const canonicalPayload = signedAttestation.payload.slice(0, signatureOffset);
250+
const signature = signedAttestation.payload.slice(signatureOffset);
251+
252+
// Verify signature
253+
const digest = sha256(canonicalPayload);
254+
const r = "0x" + Buffer.from(signature.slice(0, 32)).toString("hex");
255+
const s = "0x" + Buffer.from(signature.slice(32, 64)).toString("hex");
256+
const v = signature[64];
257+
const validatorAddress = recoverAddress(digest, { r, s, v });
258+
259+
console.log(`✅ Validator Address: ${validatorAddress}\n`);
260+
261+
// Parse payload
262+
const parsed = parseAttestationPayload(canonicalPayload);
263+
264+
console.log(`📋 Attestation Details:`);
265+
console.log(` Version: ${parsed.version}`);
266+
console.log(` Algorithm: ${parsed.algorithm} (0 = secp256k1)`);
267+
console.log(` Block Height: ${parsed.blockHeight}`);
268+
console.log(` Data Provider: ${parsed.dataProvider}`);
269+
console.log(` Stream ID: ${parsed.streamId}`);
270+
console.log(` Action ID: ${parsed.actionId}\n`);
271+
272+
console.log(`📊 Attested Query Result (from get_record):`);
273+
if (parsed.result.length === 0) {
274+
console.log(` No records found`);
275+
} else {
276+
console.log(` Found ${parsed.result.length} row(s):\n`);
277+
parsed.result.forEach((row, idx) => {
278+
console.log(` Row ${idx + 1}: ${JSON.stringify(row.values)}`);
279+
});
280+
}
281+
console.log("");
282+
} catch (parseErr: any) {
283+
console.log(`⚠️ Could not parse payload: ${parseErr.message}\n`);
284+
}
218285
}
219286
} catch (err: any) {
220287
// Check if it's an insufficient balance error

src/internal.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ export { StreamId } from "./util/StreamId";
2121
export { EthereumAddress } from "./util/EthereumAddress";
2222
export { visibility } from "./util/visibility";
2323

24+
// Attestation encoding/decoding utilities
25+
export {
26+
parseAttestationPayload
27+
} from "./util/AttestationEncoding";
28+
29+
export type {
30+
DecodedRow,
31+
ParsedAttestationPayload
32+
} from "./util/AttestationEncoding";
33+
2434
// Contract values and types
2535
export { StreamType } from "./contracts-api/contractValues";
2636

0 commit comments

Comments
 (0)