Skip to content

The Tenor TypeScript SDK provides TenorEvaluator for local contract evaluation via WASM. No network calls, no API key, no platform dependency.

Installation

bash
npm install @tenor/sdk

The package includes the WASM evaluator binary. No additional native dependencies are required. Works in Node.js 18+, Deno, Bun, and modern browsers.

TenorEvaluator — Local WASM Evaluation

TenorEvaluator loads a compiled contract (.tenor.wasm) and evaluates it locally in your process. No network calls, no API key, no platform dependency. The evaluator is a pure function: same inputs always produce same outputs.

Loading a Contract

typescript
import { TenorEvaluator } from "@tenor/sdk";

// Load from file path (Node.js)
const evaluator = await TenorEvaluator.load("./contracts/escrow.tenor.wasm");

// Load from URL (browser)
const evaluator = await TenorEvaluator.load(
  "https://cdn.example.com/contracts/escrow.tenor.wasm"
);

// Load from buffer (any runtime)
const wasmBytes = await fs.readFile("./contracts/escrow.tenor.wasm");
const evaluator = await TenorEvaluator.fromBytes(wasmBytes);

The compiled WASM artifact is produced by tenor compile:

bash
tenor compile escrow.tenor --output escrow.tenor.wasm

Evaluating a Contract

typescript
const result = evaluator.evaluate({
  facts: {
    payment_received: true,
    payment_amount: "5000.00",
    customer_tier: "premium",
  },
  entities: {
    Order: { order_001: "pending" },
    Payment: { pay_001: "cleared" },
  },
});

console.log(result.verdicts);
// {
//   payment_ok: { value: true, type: "Bool", stratum: 0 },
//   eligible_for_release: { value: true, type: "Bool", stratum: 1 }
// }

console.log(result.evaluationTimeMs);
// 0.4

Computing the Action Space

typescript
const actions = evaluator.computeActionSpace({
  facts: {
    payment_received: true,
    payment_amount: "5000.00",
    customer_tier: "premium",
  },
  entities: {
    Order: { order_001: "pending" },
    Payment: { pay_001: "cleared" },
  },
});

console.log(actions);
// {
//   escrow_agent: [
//     {
//       operation: "release_funds",
//       preconditionMet: true,
//       entitiesAffected: [
//         { entity: "Order", instance: "order_001", from: "pending", to: "released" }
//       ]
//     }
//   ],
//   buyer: [
//     {
//       operation: "dispute_transaction",
//       preconditionMet: true,
//       entitiesAffected: [
//         { entity: "Order", instance: "order_001", from: "pending", to: "disputed" }
//       ]
//     }
//   ],
//   seller: []
// }

Action Space for a Single Persona

typescript
const buyerActions = evaluator.computeActionSpace({
  facts: { payment_received: true, payment_amount: "5000.00" },
  entities: { Order: { order_001: "pending" } },
  persona: "buyer",
});

console.log(buyerActions);
// [
//   {
//     operation: "dispute_transaction",
//     preconditionMet: true,
//     entitiesAffected: [...]
//   }
// ]

Querying Contract Metadata

typescript
const meta = evaluator.metadata();

console.log(meta.entities);
// [
//   { name: "Order", states: ["pending", "approved", "disputed", "released"], initial: "pending" },
//   { name: "Payment", states: ["held", "released", "refunded"], initial: "held" }
// ]

console.log(meta.personas);
// ["escrow_agent", "buyer", "seller"]

console.log(meta.facts);
// [
//   { name: "payment_received", type: "Bool" },
//   { name: "payment_amount", type: "Money" },
//   { name: "customer_tier", type: "Text" }
// ]

console.log(meta.contractHash);
// "sha256:9f4a2b..."

Error Handling

typescript
try {
  evaluator.evaluate({
    facts: {
      payment_received: "yes", // Wrong type: expected Bool, got Text
    },
    entities: {},
  });
} catch (err) {
  if (err instanceof TenorTypeError) {
    console.error(err.fact); // "payment_received"
    console.error(err.expectedType); // "Bool"
    console.error(err.receivedType); // "Text"
    console.error(err.message);
    // "Fact 'payment_received' expected type Bool, got Text"
  }
}

try {
  evaluator.evaluate({
    facts: {
      nonexistent_fact: true, // Undeclared fact
    },
    entities: {},
  });
} catch (err) {
  if (err instanceof TenorUndeclaredFactError) {
    console.error(err.fact); // "nonexistent_fact"
    console.error(err.message);
    // "Fact 'nonexistent_fact' is not declared in this contract"
  }
}

Generated Types

Use tenor generate typescript to produce typed bindings from your contract:

bash
tenor generate typescript escrow.tenor --output ./src/generated/escrow.ts

This produces typed interfaces you can use with the evaluator:

typescript
// src/generated/escrow.ts (generated — do not edit)
export interface EscrowFacts {
  payment_received: boolean;
  payment_amount: string; // Money type is string in TS
  customer_tier: string;
  release_approved?: boolean;
  dispute_reason?: string;
}

export type OrderState =
  | "pending"
  | "approved"
  | "disputed"
  | "released"
  | "cancelled";
export type PaymentState = "held" | "released" | "refunded";

export type EscrowPersona = "escrow_agent" | "buyer" | "seller";

export type EscrowOperation =
  | "release_funds"
  | "dispute_transaction"
  | "cancel_order"
  | "verify_release"
  | "escalate_dispute"
  | "refund_payment";

export interface EscrowVerdicts {
  payment_ok?: { value: boolean; stratum: 0 };
  eligible_for_release?: { value: boolean; stratum: 1 };
  release_approval_ok?: { value: boolean; stratum: 1 };
}

export interface EscrowEntities {
  Order: Record<string, OrderState>;
  Payment: Record<string, PaymentState>;
}

Using the generated types:

typescript
import { TenorEvaluator } from "@tenor/sdk";
import type { EscrowFacts, EscrowEntities, EscrowVerdicts } from "./generated/escrow";

const evaluator = await TenorEvaluator.load("./escrow.tenor.wasm");

const facts: EscrowFacts = {
  payment_received: true,
  payment_amount: "5000.00",
  customer_tier: "premium",
};

const entities: EscrowEntities = {
  Order: { order_001: "pending" },
  Payment: { pay_001: "cleared" },
  // Compile error: unknown entity type
};

const result = evaluator.evaluate({ facts, entities });

const verdicts = result.verdicts as EscrowVerdicts;
if (verdicts.payment_ok?.value) {
  console.log("Payment verified at stratum", verdicts.payment_ok.stratum);
}

Full Working Example: Browser Action Space Preview

Using the WASM evaluator in a React component to show available actions:

typescript
import { useState, useEffect } from "react";
import { TenorEvaluator } from "@tenor/sdk";
import type { EscrowPersona } from "./generated/escrow";

interface ActionPreviewProps {
  persona: EscrowPersona;
  orderId: string;
  orderState: string;
  facts: Record<string, unknown>;
}

export function ActionPreview({ persona, orderId, orderState, facts }: ActionPreviewProps) {
  const [actions, setActions] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;

    async function evaluate() {
      const evaluator = await TenorEvaluator.load("/contracts/escrow.tenor.wasm");
      const result = evaluator.computeActionSpace({
        facts,
        entities: { Order: { [orderId]: orderState } },
        persona,
      });

      if (!cancelled) {
        setActions(result);
        setLoading(false);
      }
    }

    evaluate();
    return () => { cancelled = true; };
  }, [persona, orderId, orderState, facts]);

  if (loading) return <div>Evaluating contract...</div>;

  return (
    <div>
      <h3>Available Actions for {persona}</h3>
      {actions.length === 0 ? (
        <p>No actions available in current state.</p>
      ) : (
        <ul>
          {actions.map((action) => (
            <li key={action.operation}>
              <strong>{action.operation}</strong>
              {action.preconditionMet ? " (ready)" : " (precondition not met)"}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}