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
36 changes: 36 additions & 0 deletions .github/workflows/cre-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CRE Workflow CI

on:
push:
branches: [ main, develop, cre ]
paths:
- 'cre-workflow/**'
- '.github/workflows/cre-ci.yml'
pull_request:
branches: [ main, develop, cre ]
paths:
- 'cre-workflow/**'

jobs:
validate:
name: Check Types
runs-on: ubuntu-latest

defaults:
run:
working-directory: ./cre-workflow

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install

- name: Type check
run: bun x tsc --noEmit
24 changes: 16 additions & 8 deletions cre-workflow/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
prepareReportRequest,
type Runtime,
} from "@chainlink/cre-sdk";
import { encodeAbiParameters } from "viem";
import { BlockchainClient } from "./src/blockchain-client.js";
import { DataSourceClient } from "./src/data-sources.js";
import { AIAnalyzer } from "./src/ai-analyzer.js";
Expand Down Expand Up @@ -59,19 +60,26 @@ const onCronTrigger = async (runtime: Runtime<Config>) => {
config.openAiApiKey || ""
);

if (aiResult.confidence < 0.7) {
runtime.log(`⚠️ Low confidence (${aiResult.confidence}) for market #${market.id}. Skipping.`);
if (!aiResult) {
runtime.log(`⚠️ Consensus failed or AI error for market #${market.id}. Skipping.`);
continue;
}

runtime.log(`βœ… DON Consensus reached: "${aiResult.outcome}"`);
runtime.log(`βœ… DON Consensus reached: "${aiResult}"`);

// 4. Secure On-chain Write (2-step pattern)
// a) Generate signed report
const settlementData = `receiveSettlement(uint256 ${market.id}, string "${aiResult.outcome}", bytes 0x)`;
// Note: We'll use a manual encoding match for the report
// In a real environment, we'd use encodeAbiParameters, but for simplicity we'll replicate the core logic
const reportRequest = prepareReportRequest(aiResult.outcome as `0x${string}`);
// a) ABI Encode the parameters for receiveSettlement(uint256, string, bytes)
const encodedPayload = encodeAbiParameters(
[
{ name: 'marketId', type: 'uint256' },
{ name: 'outcome', type: 'string' },
{ name: 'proof', type: 'bytes' }
],
[market.id, aiResult as string, '0x']
);

// b) Generate signed report using the helper
const reportRequest = prepareReportRequest(encodedPayload);
const report = runtime.report(reportRequest).result();

// b) Submit report via EVM capability
Expand Down
51 changes: 28 additions & 23 deletions cre-workflow/src/ai-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
HTTPClient,
type Runtime,
consensusIdenticalAggregation,
Value,
} from "@chainlink/cre-sdk";
import type { DataSource, AIAnalysisResult } from "./types.js";

Expand All @@ -25,8 +26,8 @@ export class AIAnalyzer {
outcomes: string[],
dataSources: DataSource[],
apiKey: string
): Promise<AIAnalysisResult> {
return runtime.runInNodeMode(
): Promise<string> {
const nodeRunFunction = runtime.runInNodeMode(
async (nodeRuntime) => {
try {
const context = dataSources
Expand Down Expand Up @@ -64,7 +65,8 @@ IMPORTANT:
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
// The engine expects bytes/base64 for the body field if it's a 'bytes' type in proto
body: Buffer.from(JSON.stringify({
model: "gpt-4o-mini",
messages: [
{
Expand All @@ -78,44 +80,47 @@ IMPORTANT:
],
temperature: 0.3,
max_tokens: 500,
}),
})).toString('base64'),
});

const response = request.result();
const bodyText = new TextDecoder().decode(response.body);
const data = JSON.parse(bodyText);

if (!data.choices || data.choices.length === 0) {
throw new Error(`OpenAI API Error: ${bodyText}`);
}

const aiText = data.choices[0].message.content;

// Extract JSON
const jsonMatch = aiText.match(/\{[\s\S]*\}/);
if (!jsonMatch) throw new Error("No JSON found in AI response");
if (!jsonMatch) throw new Error(`No JSON found in AI response: ${aiText}`);
const parsed = JSON.parse(jsonMatch[0]);

// Validate outcome
const outcomeIndex = outcomes.findIndex(
(o) => o.toLowerCase() === parsed.outcome.toLowerCase()
(o) => o.toLowerCase() === (parsed.outcome || "").toLowerCase()
);
if (outcomeIndex === -1) throw new Error(`Invalid outcome: ${parsed.outcome}`);

return {
outcome: outcomes[outcomeIndex],
outcomeIndex,
confidence: parsed.confidence,
reasoning: parsed.reasoning,
sources: [],
};
return outcomes[outcomeIndex];
} catch (error) {
// Fallback
return {
outcome: outcomes[0],
outcomeIndex: 0,
confidence: 0,
reasoning: `Error: ${error}`,
sources: [],
};
nodeRuntime.log(`[AI Node] Error: ${error}`);
return outcomes[0]; // Fallback
}
},
consensusIdenticalAggregation<AIAnalysisResult>() as any
)().result();
consensusIdenticalAggregation<string>() as any
);

// runInNodeMode returns a function that returns a Promise for the ExecutionResult
const executionResult = await nodeRunFunction();
const resultValue = executionResult.result();

try {
return Value.from(resultValue as any).unwrap() as string;
} catch (e) {
return String(resultValue || outcomes[0]);
}
}
}
10 changes: 5 additions & 5 deletions cre-workflow/src/data-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ export class DataSourceClient {
name: "NewsAPI",
data: articles,
confidence: 0.8,
timestamp: Date.now(),
timestamp: 0, // Set to 0 for consensus determinism
};
},
// Consensus: For text data, consensus usually means nodes provide identical or similar strings.
// We'll use consensusIdenticalAggregation to ensure all nodes agree on the exact string.
consensusIdenticalAggregation()
).result();
consensusIdenticalAggregation<DataSource>() as any
)().result() as Promise<DataSource>;
}

/**
Expand All @@ -79,7 +79,7 @@ export class DataSourceClient {
return data[coinId]?.usd || null;
},
consensusIdenticalAggregation<number | null>() as any
)().result();
)().result() as Promise<number | null>;
}

/**
Expand All @@ -100,7 +100,7 @@ export class DataSourceClient {
name: "General Knowledge (GPT-4 Internal)",
data: `The AI analyzer will verify the following question using its internal knowledge base: "${question}".`,
confidence: 0.5,
timestamp: Date.now(),
timestamp: 0, // Set to 0 for consensus determinism
});
}

Expand Down