Resolver service

Centralized resolver that submits release or refund decisions onchain.

APISource
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { z } from "zod";
import {
  createPublicClient,
  createWalletClient,
  defineChain,
  http,
  isAddress,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { autoEscrowAbi } from "./abi.js";

const envSchema = z.object({
  PORT: z.coerce.number().default(8787),
  RPC_URL: z.string().optional(),
  CONTRACT_ADDRESS: z.string().optional(),
  RESOLVER_PRIVATE_KEY: z.string().optional(),
});

const resolveRequestSchema = z.object({
  contractId: z.coerce.bigint(),
  decision: z.enum(["release", "refund"]),
  resolutionHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
});

const env = envSchema.parse(process.env);
const tempo = defineChain({
  id: 42161_4217,
  name: "Tempo",
  nativeCurrency: {
    name: "Tempo",
    symbol: "TMP",
    decimals: 18,
  },
  rpcUrls: {
    default: {
      http: env.RPC_URL ? [env.RPC_URL] : ["http://localhost:8545"],
    },
  },
});

const app = new Hono();

app.get("/", (c) =>
  c.json({
    service: "auto-contracts-resolver",
    status: "ok",
  }),
);

app.get("/health", (c) =>
  c.json({
    status: "ok",
    configured: Boolean(
      env.RPC_URL && env.CONTRACT_ADDRESS && env.RESOLVER_PRIVATE_KEY,
    ),
  }),
);

app.post("/resolve", async (c) => {
  const json = await c.req.json();
  const payload = resolveRequestSchema.parse(json);

  if (!env.RPC_URL || !env.CONTRACT_ADDRESS || !env.RESOLVER_PRIVATE_KEY) {
    return c.json(
      {
        error:
          "Resolver is missing RPC_URL, CONTRACT_ADDRESS, or RESOLVER_PRIVATE_KEY.",
      },
      400,
    );
  }

  if (!isAddress(env.CONTRACT_ADDRESS)) {
    return c.json({ error: "CONTRACT_ADDRESS is invalid." }, 400);
  }

  const account = privateKeyToAccount(env.RESOLVER_PRIVATE_KEY as `0x${string}`);
  const transport = http(env.RPC_URL);
  const publicClient = createPublicClient({
    chain: tempo,
    transport,
  });
  const walletClient = createWalletClient({
    account,
    chain: tempo,
    transport,
  });

  const { request } = await publicClient.simulateContract({
    address: env.CONTRACT_ADDRESS as `0x${string}`,
    abi: autoEscrowAbi,
    functionName: "resolve",
    args: [
      payload.contractId,
      payload.decision === "release" ? 1 : 2,
      payload.resolutionHash as `0x${string}`,
    ],
    account,
  });

  const txHash = await walletClient.writeContract(request);

  return c.json({
    status: "submitted",
    txHash,
  });
});

serve(
  {
    fetch: app.fetch,
    port: env.PORT,
  },
  (info: { port: number }) => {
    console.log(`resolver listening on http://localhost:${info.port}`);
  },
);