Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

完整 Workflow:把它们接在一起

是时候把所学组合成一个可运行的完整结算 workflow 了!

完整流程

SettlementRequested Event
         │
         ▼
    Log Trigger
         │
         ▼
┌────────────────────┐
│ Step 1: Decode     │
│ Event data         │
└────────┬───────────┘
         │
         ▼
┌────────────────────┐
│ Step 2: EVM Read   │
│ Get market details │
└────────┬───────────┘
         │
         ▼
┌────────────────────┐
│ Step 3: HTTP       │
│ Query DeepSeek AI  │
└────────┬───────────┘
         │
         ▼
┌────────────────────┐
│ Step 4: EVM Write  │
│ Submit settlement  │
└────────┬───────────┘
         │
         ▼
    Return txHash

完整的 logCallback.ts

用下面的完整结算流程更新 my-workflow/logCallback.ts

// prediction-market/my-workflow/logCallback.ts

import {
    cre,
    type Runtime,
    type EVMLog,
    getNetwork,
    bytesToHex,
    hexToBase64,
    TxStatus,
    encodeCallMsg,
  } from "@chainlink/cre-sdk";
  import {
    decodeEventLog,
    parseAbi,
    encodeAbiParameters,
    parseAbiParameters,
    encodeFunctionData,
    decodeFunctionResult,
    zeroAddress,
  } from "viem";
  import { askDeepSeek } from "./deepseek";
  
  // Inline types
  type Config = {
    deepseekModel: string;
    evms: Array<{
      marketAddress: string;
      chainSelectorName: string;
      gasLimit: string;
    }>;
  };
  
  interface Market {
    creator: string;
    createdAt: bigint;
    settledAt: bigint;
    settled: boolean;
    confidence: number;
    outcome: number; // 0 = Yes, 1 = No
    totalYesPool: bigint;
    totalNoPool: bigint;
    question: string;
  }
  
  interface AIResult {
    result: "YES" | "NO" | "INCONCLUSIVE";
    confidence: number; // 0-10000
  }
  
  // ===========================
  // Contract ABIs
  // ===========================
  
  /** ABI for the SettlementRequested event */
  const EVENT_ABI = parseAbi([
    "event SettlementRequested(uint256 indexed marketId, string question)",
  ]);
  
  /** ABI for reading market data */
  const GET_MARKET_ABI = [
    {
      name: "getMarket",
      type: "function",
      stateMutability: "view",
      inputs: [{ name: "marketId", type: "uint256" }],
      outputs: [
        {
          name: "",
          type: "tuple",
          components: [
            { name: "creator", type: "address" },
            { name: "createdAt", type: "uint48" },
            { name: "settledAt", type: "uint48" },
            { name: "settled", type: "bool" },
            { name: "confidence", type: "uint16" },
            { name: "outcome", type: "uint8" },
            { name: "totalYesPool", type: "uint256" },
            { name: "totalNoPool", type: "uint256" },
            { name: "question", type: "string" },
          ],
        },
      ],
    },
  ] as const;
  
  /** ABI parameters for settlement report (outcome is uint8 for Prediction enum) */
  const SETTLEMENT_PARAMS = parseAbiParameters("uint256 marketId, uint8 outcome, uint16 confidence");
  
  // ===========================
  // Log Trigger Handler
  // ===========================
  
  /**
   * Handles Log Trigger events for settling prediction markets.
   *
   * Flow:
   * 1. Decode the SettlementRequested event
   * 2. Read market details from the contract (EVM Read)
   * 3. Query DeepSeek AI for the outcome (HTTP)
   * 4. Write the settlement report to the contract (EVM Write)
   *
   * @param runtime - CRE runtime with config and capabilities
   * @param log - The EVM log event data
   * @returns Success message with transaction hash
   */
  export function onLogTrigger(runtime: Runtime<Config>, log: EVMLog): string {
    runtime.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    runtime.log("CRE Workflow: Log Trigger - Settle Market");
    runtime.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
  
    try {
      // ─────────────────────────────────────────────────────────────
      // Step 1: Decode the event log
      // ─────────────────────────────────────────────────────────────
      const topics = log.topics.map((t: Uint8Array) => bytesToHex(t)) as [
        `0x${string}`,
        ...`0x${string}`[]
      ];
      const data = bytesToHex(log.data);
  
      const decodedLog = decodeEventLog({ abi: EVENT_ABI, data, topics });
      const marketId = decodedLog.args.marketId as bigint;
      const question = decodedLog.args.question as string;
  
      runtime.log(`[Step 1] Settlement requested for Market #${marketId}`);
      runtime.log(`[Step 1] Question: "${question}"`);
  
      // ─────────────────────────────────────────────────────────────
      // Step 2: Read market details from contract (EVM Read)
      // ─────────────────────────────────────────────────────────────
      runtime.log("[Step 2] Reading market details from contract...");
  
      const evmConfig = runtime.config.evms[0];
      const network = getNetwork({
        chainFamily: "evm",
        chainSelectorName: evmConfig.chainSelectorName,
        isTestnet: true,
      });
  
      if (!network) {
        throw new Error(`Unknown chain: ${evmConfig.chainSelectorName}`);
      }
  
      const evmClient = new cre.capabilities.EVMClient(network.chainSelector.selector);
  
      const callData = encodeFunctionData({
        abi: GET_MARKET_ABI,
        functionName: "getMarket",
        args: [marketId],
      });
  
      const readResult = evmClient
        .callContract(runtime, {
          call: encodeCallMsg({
            from: zeroAddress,
            to: evmConfig.marketAddress as `0x${string}`,
            data: callData,
          })
        })
        .result();
  
      const market = decodeFunctionResult({
        abi: GET_MARKET_ABI as any,
        functionName: "getMarket",
        data: bytesToHex(readResult.data),
      }) as Market;
  
      runtime.log(`[Step 2] Market creator: ${market.creator}`);
      runtime.log(`[Step 2] Already settled: ${market.settled}`);
      runtime.log(`[Step 2] Yes Pool: ${market.totalYesPool}`);
      runtime.log(`[Step 2] No Pool: ${market.totalNoPool}`);
  
      if (market.settled) {
        runtime.log("[Step 2] Market already settled, skipping...");
        return "Market already settled";
      }
  
      // ─────────────────────────────────────────────────────────────
      // Step 3: Query AI (HTTP)
      // ─────────────────────────────────────────────────────────────
      runtime.log("[Step 3] Querying DeepSeek AI...");
  
      const aiResult = askDeepSeek(runtime, question);
      
      // Extract JSON from response (AI may include prose before/after the JSON)
      const jsonMatch = aiResult.aiResponse.match(/\{[\s\S]*"result"[\s\S]*"confidence"[\s\S]*\}/);
      if (!jsonMatch) {
        throw new Error(`Could not find JSON in AI response: ${aiResult.aiResponse}`);
      }
      const parsed = JSON.parse(jsonMatch[0]) as AIResult;
  
      // Validate the result - only YES or NO can settle a market
      if (!["YES", "NO"].includes(parsed.result)) {
        throw new Error(`Cannot settle: AI returned ${parsed.result}. Only YES or NO can settle a market.`);
      }
      if (parsed.confidence < 0 || parsed.confidence > 10000) {
        throw new Error(`Invalid confidence: ${parsed.confidence}`);
      }
  
      runtime.log(`[Step 3] AI Result: ${parsed.result}`);
      runtime.log(`[Step 3] AI Confidence: ${parsed.confidence / 100}%`);
  
      // Convert result string to Prediction enum value (0 = Yes, 1 = No)
      const outcomeValue = parsed.result === "YES" ? 0 : 1;
  
      // ─────────────────────────────────────────────────────────────
      // Step 4: Write settlement report to contract (EVM Write)
      // ─────────────────────────────────────────────────────────────
      runtime.log("[Step 4] Generating settlement report...");
  
      // Encode settlement data
      const settlementData = encodeAbiParameters(SETTLEMENT_PARAMS, [
        marketId,
        outcomeValue,
        parsed.confidence,
      ]);
  
      // Prepend 0x01 prefix so contract routes to _settleMarket
      const reportData = ("0x01" + settlementData.slice(2)) as `0x${string}`;
  
      const reportResponse = runtime
        .report({
          encodedPayload: hexToBase64(reportData),
          encoderName: "evm",
          signingAlgo: "ecdsa",
          hashingAlgo: "keccak256",
        })
        .result();
  
      runtime.log(`[Step 4] Writing to contract: ${evmConfig.marketAddress}`);
  
      const writeResult = evmClient
        .writeReport(runtime, {
          receiver: evmConfig.marketAddress,
          report: reportResponse,
          gasConfig: {
            gasLimit: evmConfig.gasLimit,
          },
        })
        .result();
  
      if (writeResult.txStatus === TxStatus.SUCCESS) {
        const txHash = bytesToHex(writeResult.txHash || new Uint8Array(32));
        runtime.log(`[Step 4] ✓ Settlement successful: ${txHash}`);
        runtime.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        return `Settled: ${txHash}`;
      }
  
      throw new Error(`Transaction failed: ${writeResult.txStatus}`);
  
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      runtime.log(`[ERROR] ${msg}`);
      runtime.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
      throw err;
    }
  }

下注预测

在请求结算之前,我们先在市场上做一次预测。这样可以演示完整流程:用 ETH 下注、AI 结算、赢家领取份额。

# Predict YES on market #0 with 0.01 ETH
# Prediction enum: 0 = Yes, 1 = No
cast send $MARKET_ADDRESS \
  "predict(uint256,uint8)" 0 0 \
  --value 0.01ether \
  --rpc-url "https://ethereum-sepolia-rpc.publicnode.com" \
  --private-key $CRE_ETH_PRIVATE_KEY

然后可以再次查看市场详情:

cast call $MARKET_ADDRESS \
  "getMarket(uint256) returns ((address,uint48,uint48,bool,uint16,uint8,uint256,uint256,string))" \
  0 \
  --rpc-url "https://ethereum-sepolia-rpc.publicnode.com"

也可以只查自己的预测:

export PREDICTOR=0xYOUR_WALLET_ADDRESS
cast call $MARKET_ADDRESS \
  "getPrediction(uint256,address) returns ((uint256,uint8,bool))" \
  0 $PREDICTOR \
  --rpc-url "https://ethereum-sepolia-rpc.publicnode.com"

可以有多名参与者分别预测——有人选 YES,有人选 NO。CRE 完成市场结算后,赢家可以调用 claim() 领取总池中的份额!


结算市场

下面用 Log Trigger 执行完整结算流程。

步骤 1:请求结算

首先从智能合约触发 SettlementRequested 事件:

cast send $MARKET_ADDRESS \
  "requestSettlement(uint256)" 0 \
  --rpc-url "https://ethereum-sepolia-rpc.publicnode.com" \
  --private-key $CRE_ETH_PRIVATE_KEY

**请保存交易哈希!**下一步会用到。

步骤 2:运行 Simulation

cre workflow simulate my-workflow --broadcast

步骤 3:选择 Log Trigger

🚀 Workflow simulation ready. Please select a trigger:
1. http-trigger@1.0.0-alpha Trigger
2. evm:ChainSelector:16015286601757825753@1.0.0 LogTrigger

Enter your choice (1-2): 2

步骤 4:输入交易信息

🔗 EVM Trigger Configuration:
Please provide the transaction hash and event index for the EVM log event.
Enter transaction hash (0x...):

粘贴步骤 1 中的交易哈希。

步骤 5:输入 Event Index

Enter event index (0-based): 0

输入0

预期输出

[SIMULATION] Running trigger trigger=evm:ChainSelector:16015286601757825753@1.0.0
[USER LOG] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[USER LOG] CRE Workflow: Log Trigger - Settle Market
[USER LOG] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[USER LOG] [Step 1] Settlement requested for Market #0
[USER LOG] [Step 1] Question: "Will Argentina win the 2022 World Cup?"
[USER LOG] [Step 2] Reading market details from contract...
[USER LOG] [Step 2] Market creator: 0x...
[USER LOG] [Step 2] Already settled: false
[USER LOG] [Step 2] Yes Pool: 10000000000000000
[USER LOG] [Step 2] No Pool: 0
[USER LOG] [Step 3] Querying DeepSeek AI...
[USER LOG] [DeepSeek] Querying AI for market outcome...
[USER LOG] [DeepSeek] Response received: {"result":"YES","confidence":10000}
[USER LOG] [Step 3] AI Result: YES
[USER LOG] [Step 3] AI Confidence: 100%
[USER LOG] [Step 4] Generating settlement report...
[USER LOG] [Step 4] Writing to contract: 0x...
[USER LOG] [Step 4] ✓ Settlement successful: 0xabc123...
[USER LOG] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Workflow Simulation Result:
 "Settled: 0xabc123..."

[SIMULATION] Execution finished signal received

步骤 6:链上验证结算

cast call $MARKET_ADDRESS \
  "getMarket(uint256) returns ((address,uint48,uint48,bool,uint16,uint8,uint256,uint256,string))" \
  0 \
  --rpc-url "https://ethereum-sepolia-rpc.publicnode.com"

你应能看到 settled: true 以及由 AI 判定的结果!

步骤 7:领取奖金

如果你预测的是获胜结果,可以领取池中属于你的部分:

cast send $MARKET_ADDRESS \
  "claim(uint256)" 0 \
  --rpc-url "https://ethereum-sepolia-rpc.publicnode.com" \
  --private-key $CRE_ETH_PRIVATE_KEY

故障排查

Deepseek API 报错:402

如果你看到如下错误:

Capability 'consensus@1.0.0-alpha' method 'Simple' returned an error: failed to execute capability: [2]Unknown: DeepSeek API error: 402 - {"error":{"message":"Insufficient Balance","type":"unknown_error","param":null,"code":"invalid_request_error"}}

请在 Deepseek Platform) 控制台为你的 Deepseek API key 开通计费。海外需要绑定信用卡以启用计费,国内用户可以使用支付宝和微信支付,不必担心——完成本 bootcamp 需要的费用极低,只要 0.01 人民币。

deepseek-billing


🎉 大功告成!

**恭喜!**你已经用 CRE 搭建并跑通了一个完整的 AI 驱动预测市场!

快速回顾你完成的内容:

能力你构建的内容
HTTP Trigger通过 API 请求创建市场
Log Trigger基于事件的结算自动化
EVM Read从链上读取市场状态
HTTP (AI)查询 DeepSeek AI 获取真实世界结果
EVM Write经 DON 共识验证的链上写入

你的 workflow 现在可以:

  • ✅ 通过 HTTP 按需创建市场
  • ✅ 监听链上事件以接收结算请求
  • ✅ 从智能合约读取市场数据
  • ✅ 查询 AI 以判定真实世界结果
  • ✅ 将经校验的结算写回链上
  • ✅ 让赢家领取奖励

下一步

前往最后一章,查看完整的端到端演练以及 CRE 之路的后续方向!