casper-trust 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # casper-trust
2
+
3
+ The settled-payment trust layer for Casper agents — read an agent's reputation in one line, gate x402 payments on it.
4
+
5
+ ## What it is
6
+
7
+ `casper-trust` wraps three on-chain contracts (identity registry, reputation ledger, escrow) deployed on `casper-test` and exposes them through a minimal TypeScript SDK. The read path is wallet-free and gas-free. The payment path delegates the 402-handshake to `@make-software/casper-x402` + `@x402/fetch`; this SDK adds the trust-score gate on top.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install casper-trust
13
+ ```
14
+
15
+ ## Wallet-free quickstart
16
+
17
+ No wallet, no gas. Read any agent's trust score in one call:
18
+
19
+ ```ts
20
+ import { createTrustClient, checkTrust } from "casper-trust";
21
+
22
+ const client = createTrustClient(); // reads from casper-test by default
23
+ const result = await checkTrust(client, 1); // agentId = 1
24
+ console.log(result.trusted, result.score); // e.g. true, 9500n (basis points)
25
+ ```
26
+
27
+ `result.trusted` is `true` when the agent is Active and `score >= minScore` (default 0).
28
+
29
+ ## Trust-gated x402 payment
30
+
31
+ ```ts
32
+ import { createTrustClient, pay, type X402TrustClient } from "casper-trust";
33
+ import { toClientCasperSigner } from "@make-software/casper-x402";
34
+
35
+ const signer = toClientCasperSigner(yourCasperAccount);
36
+ const client: X402TrustClient = { ...createTrustClient(), signer };
37
+
38
+ const response = await pay(client, {
39
+ url: "https://api.example.com/protected-resource",
40
+ providerAgentId: 1, // on-chain identity id of the provider
41
+ minScore: 5000n, // gate: reject if score < 50 % (basis points)
42
+ });
43
+ ```
44
+
45
+ `pay()` throws `TrustGateError` before sending any money if the provider's on-chain score is below `minScore`.
46
+
47
+ ## API reference
48
+
49
+ | Export | Description |
50
+ |---|---|
51
+ | `createTrustClient(overrides?)` | Returns `{ cfg, rpc }` — wallet-free read client |
52
+ | `checkTrust(client, agentId, opts?)` | Full trust check; returns `TrustResult` |
53
+ | `getReputation(client, agentId)` | Raw `Reputation` (jobsCompleted, scoreBps, …) |
54
+ | `getAgent(client, agentId)` | Raw `Agent` (owner, wallet, status, bond) or `null` |
55
+ | `pay(client, req)` | Trust-gated x402 v2 fetch |
56
+ | `gateByTrust(checkFn, agentId, minScore)` | Standalone gate (throws `TrustGateError`) |
57
+ | `register(cfg, signer, uri, bond)` | Build+sign a register transaction (no submit) |
58
+ | `attestSettlement(cfg, params)` | Build the 4-tx settlement sequence (no submit) |
59
+ | `CASPER_TEST` | Default `NetworkConfig` (testnet RPC + contract hashes) |
60
+
61
+ ## x402 delegation
62
+
63
+ The 402-handshake (retry loop, `PAYMENT-SIGNATURE` header, amount field) is fully delegated to `@make-software/casper-x402` and `@x402/fetch`. `casper-trust` adds only the trust-score check before the handshake starts.
64
+
65
+ ## Network / contracts
66
+
67
+ Live on `casper-test`. Contract package hashes are in `CASPER_TEST` (see `src/config.ts`) and documented with their field-index calibration in `.git/sdd/task-3-report.md` and `DEPLOYMENT.md`.
68
+
69
+ > The wallet-free read path is live-verified against testnet. The full x402 payment handshake is unit-tested at the gating level; an end-to-end live run against a funded facilitator token is tracked in the demo task.
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,34 @@
1
+ export interface NetworkConfig {
2
+ rpcUrl: string;
3
+ authToken?: string;
4
+ chainName: string;
5
+ facilitatorUrl: string;
6
+ packages: {
7
+ identity: string;
8
+ reputation: string;
9
+ escrow: string;
10
+ cep18: string;
11
+ };
12
+ /** Odra field indices — CALIBRATED against live data in Task 3, not assumed from source. */
13
+ fields: {
14
+ identity: {
15
+ escrowVarIndex: number;
16
+ agents: number;
17
+ count: number;
18
+ };
19
+ reputation: {
20
+ escrowVarIndex: number;
21
+ identityVarIndex: number;
22
+ reps: number;
23
+ pairs: number;
24
+ };
25
+ escrow: {
26
+ identityVarIndex: number;
27
+ reputationVarIndex: number;
28
+ tokenVarIndex: number;
29
+ jobs: number;
30
+ count: number;
31
+ };
32
+ };
33
+ }
34
+ export declare const CASPER_TEST: NetworkConfig;
package/dist/config.js ADDED
@@ -0,0 +1,38 @@
1
+ export const CASPER_TEST = {
2
+ rpcUrl: process.env.CSPR_CLOUD_TOKEN
3
+ ? "https://node.testnet.cspr.cloud/rpc"
4
+ : "https://node.testnet.casper.network/rpc",
5
+ authToken: process.env.CSPR_CLOUD_TOKEN,
6
+ chainName: "casper-test",
7
+ facilitatorUrl: "https://x402-facilitator.cspr.cloud",
8
+ packages: {
9
+ identity: "3a51cc5f4c524f806b3b8899039030bbad141005f81ab99895615d8f050c7adc",
10
+ reputation: "d73fb11144c07ec05071cf986ad65b407f2da91bd871b0c10f67a974832ee7eb",
11
+ escrow: "fe6b0ddb307549cc9101659abcfaf114e37a8d99461c0632cbce582ebdc4902c",
12
+ cep18: "f962076e6c2ba423aaade9f75935ff37ef4aa4cde6077bac9a259af141c3d5c6",
13
+ },
14
+ // Indices CALIBRATED live against testnet in Task 3 — uniform +1 offset from source order.
15
+ // Evidence: all three contracts store the admin Address at live idx 0; declared fields
16
+ // shift up by 1. See task-3-report.md for the full scan log.
17
+ fields: {
18
+ // source-order → live idx (offset +1 confirmed on all 3 contracts)
19
+ identity: {
20
+ escrowVarIndex: 2, // source 1 → live 2
21
+ agents: 3, // Mapping source 2 → live 3
22
+ count: 4, // source 3 → live 4
23
+ },
24
+ reputation: {
25
+ escrowVarIndex: 2, // source 1 → live 2
26
+ identityVarIndex: 3, // source 2 → live 3
27
+ reps: 4, // Mapping source 3 → live 4
28
+ pairs: 5, // Mapping source 4 → live 5
29
+ },
30
+ escrow: {
31
+ identityVarIndex: 1, // source 0 → live 1
32
+ reputationVarIndex: 2, // source 1 → live 2
33
+ tokenVarIndex: 3, // source 2 → live 3
34
+ jobs: 4, // Mapping source 3 → live 4
35
+ count: 5, // source 4 → live 5
36
+ },
37
+ },
38
+ };
@@ -0,0 +1,24 @@
1
+ import { type NetworkConfig } from "./config.js";
2
+ import type { TrustClient } from "./trust/index.js";
3
+ export { checkTrust, getReputation, getAgent, type TrustClient } from "./trust/index.js";
4
+ export { pay, gateByTrust, TrustGateError, type PayRequest, type X402TrustClient } from "./x402/index.js";
5
+ /**
6
+ * Build-and-sign helpers (offline mode).
7
+ *
8
+ * These functions BUILD and SIGN a transaction but do NOT submit it.
9
+ * To submit, call `rpc.putTransaction(tx)` with the returned Transaction object.
10
+ * Example:
11
+ * const tx = register(cfg, signer, uri, bond);
12
+ * await c.rpc.putTransaction(tx);
13
+ */
14
+ export { buildRegister as register, buildAttestSettlement as attestSettlement, } from "./registry/index.js";
15
+ export type { Reputation, Agent, TrustResult } from "./types.js";
16
+ export { CASPER_TEST, type NetworkConfig } from "./config.js";
17
+ /**
18
+ * Creates a wallet-free read-only trust client for casper-test (or overridden network).
19
+ *
20
+ * Returns { cfg, rpc } — sufficient for checkTrust / getReputation / getAgent.
21
+ * For x402 payments (pay()), construct an X402TrustClient by adding a signer:
22
+ * const c: X402TrustClient = { ...createTrustClient(), signer };
23
+ */
24
+ export declare function createTrustClient(overrides?: Partial<NetworkConfig>): TrustClient;
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ import { CASPER_TEST } from "./config.js";
2
+ import { makeRpcClient } from "./rpc/client.js";
3
+ export { checkTrust, getReputation, getAgent } from "./trust/index.js";
4
+ export { pay, gateByTrust, TrustGateError } from "./x402/index.js";
5
+ /**
6
+ * Build-and-sign helpers (offline mode).
7
+ *
8
+ * These functions BUILD and SIGN a transaction but do NOT submit it.
9
+ * To submit, call `rpc.putTransaction(tx)` with the returned Transaction object.
10
+ * Example:
11
+ * const tx = register(cfg, signer, uri, bond);
12
+ * await c.rpc.putTransaction(tx);
13
+ */
14
+ export { buildRegister as register, buildAttestSettlement as attestSettlement, } from "./registry/index.js";
15
+ export { CASPER_TEST } from "./config.js";
16
+ /**
17
+ * Creates a wallet-free read-only trust client for casper-test (or overridden network).
18
+ *
19
+ * Returns { cfg, rpc } — sufficient for checkTrust / getReputation / getAgent.
20
+ * For x402 payments (pay()), construct an X402TrustClient by adding a signer:
21
+ * const c: X402TrustClient = { ...createTrustClient(), signer };
22
+ */
23
+ export function createTrustClient(overrides = {}) {
24
+ const cfg = { ...CASPER_TEST, ...overrides };
25
+ return { cfg, rpc: makeRpcClient(cfg) };
26
+ }
@@ -0,0 +1,14 @@
1
+ export declare class Reader {
2
+ b: Uint8Array;
3
+ o: number;
4
+ constructor(b: Uint8Array, o?: number);
5
+ }
6
+ export declare const u8: (r: Reader) => number;
7
+ export declare const u32: (r: Reader) => number;
8
+ export declare const u64: (r: Reader) => bigint;
9
+ export declare const uN: (r: Reader) => bigint;
10
+ export declare const bool: (r: Reader) => boolean;
11
+ export declare const str: (r: Reader) => string;
12
+ export declare const addr: (r: Reader) => string;
13
+ export declare const option: <T>(r: Reader, f: (r: Reader) => T) => T | null;
14
+ export declare const unitEnum: (r: Reader, names: string[]) => string;
@@ -0,0 +1,38 @@
1
+ export class Reader {
2
+ b;
3
+ o;
4
+ constructor(b, o = 0) {
5
+ this.b = b;
6
+ this.o = o;
7
+ }
8
+ }
9
+ export const u8 = (r) => r.b[r.o++];
10
+ export const u32 = (r) => {
11
+ const v = new DataView(r.b.buffer, r.b.byteOffset + r.o, 4).getUint32(0, true);
12
+ r.o += 4;
13
+ return v;
14
+ };
15
+ export const u64 = (r) => { const lo = BigInt(u32(r)), hi = BigInt(u32(r)); return hi * (2n ** 32n) + lo; };
16
+ export const uN = (r) => {
17
+ const n = u8(r);
18
+ let v = 0n;
19
+ for (let i = 0; i < n; i++)
20
+ v += BigInt(r.b[r.o + i]) << (8n * BigInt(i));
21
+ r.o += n;
22
+ return v;
23
+ };
24
+ export const bool = (r) => u8(r) === 1;
25
+ export const str = (r) => {
26
+ const n = u32(r);
27
+ const s = new TextDecoder().decode(r.b.slice(r.o, r.o + n));
28
+ r.o += n;
29
+ return s;
30
+ };
31
+ export const addr = (r) => {
32
+ const tag = u8(r);
33
+ const h = Buffer.from(r.b.slice(r.o, r.o + 32)).toString("hex");
34
+ r.o += 32;
35
+ return (tag === 0 ? "account-hash-" : "contract-") + h;
36
+ };
37
+ export const option = (r, f) => (u8(r) === 1 ? f(r) : null);
38
+ export const unitEnum = (r, names) => names[u8(r)];
@@ -0,0 +1,2 @@
1
+ export declare const varKey: (fieldIndex: number) => string;
2
+ export declare const mapKeyU32: (fieldIndex: number, mapKey: number) => string;
@@ -0,0 +1,17 @@
1
+ import { blake2b } from "blakejs";
2
+ const u32LE = (n) => {
3
+ const b = new Uint8Array(4);
4
+ new DataView(b.buffer).setUint32(0, n >>> 0, true);
5
+ return b;
6
+ };
7
+ const u32BE = (n) => {
8
+ const b = new Uint8Array(4);
9
+ new DataView(b.buffer).setUint32(0, n >>> 0, false);
10
+ return b;
11
+ };
12
+ // Odra: index_bytes = u32 big-endian of the field index for indices <= 15.
13
+ const idxBytes = (i) => (i > 15 ? Uint8Array.from([0xff, 1, i]) : u32BE(i));
14
+ const cat = (a, b) => { const o = new Uint8Array(a.length + b.length); o.set(a); o.set(b, a.length); return o; };
15
+ const hex = (b) => Buffer.from(b).toString("hex");
16
+ export const varKey = (fieldIndex) => hex(blake2b(idxBytes(fieldIndex), undefined, 32));
17
+ export const mapKeyU32 = (fieldIndex, mapKey) => hex(blake2b(cat(idxBytes(fieldIndex), u32LE(mapKey)), undefined, 32));
@@ -0,0 +1,10 @@
1
+ import { type RpcClient } from "casper-js-sdk";
2
+ /**
3
+ * Reads a single Odra dictionary item from the "state" named key on a contract.
4
+ *
5
+ * Returns the raw payload bytes with the 4-byte List<U8> length prefix stripped,
6
+ * or null if the item does not exist (Odra stores type-defaults as absent keys).
7
+ *
8
+ * itemKeyHex: hex string of the dictionary item key (from varKey() / mapKeyU32())
9
+ */
10
+ export declare function readOdraValue(rpc: RpcClient, contractHashHex: string, itemKeyHex: string): Promise<Uint8Array | null>;
@@ -0,0 +1,42 @@
1
+ import { ParamDictionaryIdentifier, ParamDictionaryIdentifierContractNamedKey, } from "casper-js-sdk";
2
+ /**
3
+ * Reads a single Odra dictionary item from the "state" named key on a contract.
4
+ *
5
+ * Returns the raw payload bytes with the 4-byte List<U8> length prefix stripped,
6
+ * or null if the item does not exist (Odra stores type-defaults as absent keys).
7
+ *
8
+ * itemKeyHex: hex string of the dictionary item key (from varKey() / mapKeyU32())
9
+ */
10
+ export async function readOdraValue(rpc, contractHashHex, itemKeyHex) {
11
+ // contractNamedKey must be a proper class instance (TypedJSON rejects plain objects)
12
+ const contractNamedKey = new ParamDictionaryIdentifierContractNamedKey(`hash-${contractHashHex}`, "state", itemKeyHex);
13
+ const id = new ParamDictionaryIdentifier(undefined, contractNamedKey, undefined, undefined);
14
+ try {
15
+ const res = await rpc.getDictionaryItemByIdentifier(null, id);
16
+ // Use rawJSON (the raw RPC response) to get the hex bytes string.
17
+ // Path: rawJSON.stored_value.CLValue.bytes — snake_case keys in raw JSON.
18
+ // CLValue encoding for List<U8>: 4-byte LE length prefix followed by payload.
19
+ const clHex = res.rawJSON?.stored_value?.CLValue?.bytes;
20
+ if (typeof clHex !== "string" || clHex.length === 0) {
21
+ // Fallback: clValue.bytes() returns the full CLValue byte encoding for List<U8>,
22
+ // which is a 4-byte LE u32 length prefix followed by the payload — same layout
23
+ // as the rawJSON hex path above, so we slice(4) identically.
24
+ const clBytes = res.storedValue.clValue?.bytes();
25
+ if (!clBytes || clBytes.length < 4)
26
+ return null;
27
+ return clBytes.slice(4);
28
+ }
29
+ const full = Uint8Array.from(Buffer.from(clHex, "hex"));
30
+ // Strip 4-byte LE u32 length prefix that Odra prepends to every List<U8>
31
+ return full.slice(4);
32
+ }
33
+ catch (e) {
34
+ // getDictionaryItemByIdentifier wraps the JSON-RPC error: the -32003 (QueryFailed)
35
+ // code is on e.statusCode / e.sourceErr.code, NOT e.code. -32003 = the key is simply
36
+ // absent (Odra stores type-defaults as missing keys); any other error must propagate.
37
+ const code = e?.code ?? e?.statusCode ?? e?.sourceErr?.code;
38
+ if (code === -32003)
39
+ return null;
40
+ throw e;
41
+ }
42
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Registry write-path: register (via proxy_caller_with_return.wasm), token approve,
3
+ * escrow create_job / submit_work / approve, and attestSettlement orchestration.
4
+ *
5
+ * proxy_caller arg names are taken verbatim from odra-core-2.8.1 consts.rs:
6
+ * PACKAGE_HASH_ARG = "package_hash"
7
+ * ENTRY_POINT_ARG = "entry_point"
8
+ * ARGS_ARG = "args"
9
+ * ATTACHED_VALUE_ARG = "attached_value"
10
+ * AMOUNT_ARG = "amount"
11
+ * (source: odra-casper-rpc-client/src/casper_client/transactions.rs lines 17, 159-164)
12
+ *
13
+ * Live submission (putTransaction) is intentionally absent — deferred to a funded-key run.
14
+ */
15
+ import { type PrivateKey, type Transaction } from "casper-js-sdk";
16
+ import type { NetworkConfig } from "../config.js";
17
+ /**
18
+ * Builds and signs a register transaction routed through proxy_caller_with_return.wasm.
19
+ *
20
+ * The proxy receives five outer runtime args (names from odra-core-2.8.1/src/consts.rs):
21
+ * package_hash — Key::Hash wrapping the identity registry package hash
22
+ * entry_point — String "register"
23
+ * args — ByteArray of the CL-serialized inner RuntimeArgs (agent_uri: String)
24
+ * attached_value — UInt512 bond in motes (actual CSPR attached to the call)
25
+ * amount — UInt512 same value (grants main-purse access to the proxy)
26
+ *
27
+ * Does NOT submit. Returns the signed Transaction for offline inspection.
28
+ */
29
+ export declare function buildRegister(cfg: NetworkConfig, signer: PrivateKey, agentUri: string, bondMotes: bigint, gasMotes?: number): Transaction;
30
+ /**
31
+ * Builds and signs a CEP-18 approve transaction.
32
+ * `spenderPackageHash` is the bare 64-hex package hash of the escrow contract.
33
+ */
34
+ export declare function buildApproveToken(cfg: NetworkConfig, signer: PrivateKey, spenderPackageHash: string, amount: bigint): Transaction;
35
+ export interface CreateJobParams {
36
+ clientId: number;
37
+ provider: number;
38
+ amount: bigint;
39
+ deadline: number;
40
+ }
41
+ export declare function buildCreateJob(cfg: NetworkConfig, signer: PrivateKey, p: CreateJobParams): Transaction;
42
+ export declare function buildSubmitWork(cfg: NetworkConfig, signer: PrivateKey, jobId: bigint, resultHash: string): Transaction;
43
+ export declare function buildApproveJob(cfg: NetworkConfig, signer: PrivateKey, jobId: bigint): Transaction;
44
+ export interface AttestSettlementParams {
45
+ clientSigner: PrivateKey;
46
+ providerSigner: PrivateKey;
47
+ clientId: number;
48
+ providerId: number;
49
+ tokenAmount: bigint;
50
+ deadline: number;
51
+ jobId: bigint;
52
+ resultHash: string;
53
+ }
54
+ /**
55
+ * Builds the ordered transaction sequence for a full settlement:
56
+ * 1. approveToken (client approves escrow to spend tokens)
57
+ * 2. createJob (client locks funds in escrow)
58
+ * 3. submitWork (provider submits deliverable hash)
59
+ * 4. approveJob (client approves work, triggers 2% burn + reputation update)
60
+ *
61
+ * Returns the four built+signed Transaction objects in sequence order so the
62
+ * caller can inspect structure offline, then submit in order with waitForTx
63
+ * between each when a funded key is available.
64
+ *
65
+ * Does NOT submit any transaction.
66
+ */
67
+ export declare function buildAttestSettlement(cfg: NetworkConfig, p: AttestSettlementParams): {
68
+ approveTx: Transaction;
69
+ createJobTx: Transaction;
70
+ submitWorkTx: Transaction;
71
+ approveJobTx: Transaction;
72
+ };
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Registry write-path: register (via proxy_caller_with_return.wasm), token approve,
3
+ * escrow create_job / submit_work / approve, and attestSettlement orchestration.
4
+ *
5
+ * proxy_caller arg names are taken verbatim from odra-core-2.8.1 consts.rs:
6
+ * PACKAGE_HASH_ARG = "package_hash"
7
+ * ENTRY_POINT_ARG = "entry_point"
8
+ * ARGS_ARG = "args"
9
+ * ATTACHED_VALUE_ARG = "attached_value"
10
+ * AMOUNT_ARG = "amount"
11
+ * (source: odra-casper-rpc-client/src/casper_client/transactions.rs lines 17, 159-164)
12
+ *
13
+ * Live submission (putTransaction) is intentionally absent — deferred to a funded-key run.
14
+ */
15
+ import { readFileSync } from "node:fs";
16
+ import { fileURLToPath } from "node:url";
17
+ import { dirname, resolve } from "node:path";
18
+ import { ContractCallBuilder, SessionBuilder, Args, CLValue, CLTypeUInt8, Key, } from "casper-js-sdk";
19
+ // ---------------------------------------------------------------------------
20
+ // Wasm path: sdk/assets/proxy_caller_with_return.wasm (copied from contracts/)
21
+ // ---------------------------------------------------------------------------
22
+ function loadProxyCallerWasm() {
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const wasmPath = resolve(__dirname, "../../assets/proxy_caller_with_return.wasm");
25
+ const buf = readFileSync(wasmPath);
26
+ return new Uint8Array(buf);
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Internal non-payable contract call builder (no submission)
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Default gas for a small stored-contract call. casper-test assigns a V1 tx to a
33
+ * lane by its serialized size, then requires the PaymentLimited amount to match
34
+ * that lane's gas limit — a small call (≤65_536 bytes, args ≤512) lands in the
35
+ * "wasm lane 5" whose gas_limit is exactly 5 CSPR. Paying less (1.5/2.5 CSPR) is
36
+ * rejected at submission with "Invalid payment amount for Transaction::V1".
37
+ * (Live-verified against the testnet chainspec wasm_lanes config.)
38
+ */
39
+ const CALL_GAS_MOTES = 5_000_000_000;
40
+ function buildCall(cfg, signer, pkgHash, entry, args, gasMotes = CALL_GAS_MOTES) {
41
+ const tx = new ContractCallBuilder()
42
+ .from(signer.publicKey)
43
+ .byPackageHash(pkgHash)
44
+ .entryPoint(entry)
45
+ .runtimeArgs(args)
46
+ .chainName(cfg.chainName)
47
+ .payment(gasMotes)
48
+ .build();
49
+ tx.sign(signer);
50
+ return tx;
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // register — payable via proxy_caller_with_return.wasm
54
+ // ---------------------------------------------------------------------------
55
+ /**
56
+ * Builds and signs a register transaction routed through proxy_caller_with_return.wasm.
57
+ *
58
+ * The proxy receives five outer runtime args (names from odra-core-2.8.1/src/consts.rs):
59
+ * package_hash — Key::Hash wrapping the identity registry package hash
60
+ * entry_point — String "register"
61
+ * args — ByteArray of the CL-serialized inner RuntimeArgs (agent_uri: String)
62
+ * attached_value — UInt512 bond in motes (actual CSPR attached to the call)
63
+ * amount — UInt512 same value (grants main-purse access to the proxy)
64
+ *
65
+ * Does NOT submit. Returns the signed Transaction for offline inspection.
66
+ */
67
+ export function buildRegister(cfg, signer, agentUri, bondMotes, gasMotes = 3_000_000_000) {
68
+ const wasm = loadProxyCallerWasm();
69
+ // CL-serialise the inner args that proxy_caller forwards to identity::register.
70
+ const innerArgs = Args.fromMap({
71
+ agent_uri: CLValue.newCLString(agentUri),
72
+ });
73
+ const innerBytes = innerArgs.toBytes();
74
+ const bond = bondMotes.toString();
75
+ // proxy_caller_with_return arg types are dictated by ProxyCall::load_from_args()
76
+ // (odra-casper/proxy-caller/src/lib.rs @ 2.8.0):
77
+ // let package_hash = get_named_arg(PACKAGE_HASH_ARG); // PackageHash
78
+ // let entry_point_name = get_named_arg(ENTRY_POINT_ARG); // String
79
+ // let runtime_args: Bytes = get_named_arg(ARGS_ARG); // Bytes == List<U8>
80
+ // let attached_value: U512 = get_named_arg(ATTACHED_VALUE_ARG);
81
+ //
82
+ // * PackageHash::cl_type() == ByteArray(32) -> raw 32 hash bytes (NOT a Key).
83
+ // * Bytes::cl_type() == List<U8> -> 4-byte LE length prefix + payload.
84
+ //
85
+ // Live-verified: encoding `args` as ByteArray (no length prefix) OR `package_hash`
86
+ // as a Key both yield ApiError::InvalidArgument on register. The host's typed
87
+ // get_named_arg rejects any CLType mismatch before the entry point runs.
88
+ const pkgBytes = Uint8Array.from(Buffer.from(cfg.packages.identity, "hex"));
89
+ const innerArgsList = CLValue.newCLList(CLTypeUInt8, Array.from(innerBytes, (b) => CLValue.newCLUint8(b)));
90
+ const tx = new SessionBuilder()
91
+ .from(signer.publicKey)
92
+ .wasm(wasm)
93
+ .runtimeArgs(Args.fromMap({
94
+ package_hash: CLValue.newCLByteArray(pkgBytes),
95
+ entry_point: CLValue.newCLString("register"),
96
+ args: innerArgsList,
97
+ attached_value: CLValue.newCLUInt512(bond),
98
+ amount: CLValue.newCLUInt512(bond),
99
+ }))
100
+ .chainName(cfg.chainName)
101
+ .payment(gasMotes)
102
+ .build();
103
+ tx.sign(signer);
104
+ return tx;
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // approveToken — CEP-18 approve(spender, amount)
108
+ // ---------------------------------------------------------------------------
109
+ /**
110
+ * Builds and signs a CEP-18 approve transaction.
111
+ * `spenderPackageHash` is the bare 64-hex package hash of the escrow contract.
112
+ */
113
+ export function buildApproveToken(cfg, signer, spenderPackageHash, amount) {
114
+ // CEP-18 approve expects `spender` as a Key (package/hash variant).
115
+ const spenderKey = Key.newKey("hash-" + spenderPackageHash);
116
+ return buildCall(cfg, signer, cfg.packages.cep18, "approve", Args.fromMap({
117
+ spender: CLValue.newCLKey(spenderKey),
118
+ amount: CLValue.newCLUInt256(amount.toString()),
119
+ }));
120
+ }
121
+ export function buildCreateJob(cfg, signer, p) {
122
+ return buildCall(cfg, signer, cfg.packages.escrow, "create_job", Args.fromMap({
123
+ client_id: CLValue.newCLUInt32(p.clientId),
124
+ provider: CLValue.newCLUInt32(p.provider),
125
+ amount: CLValue.newCLUInt256(p.amount.toString()),
126
+ deadline: CLValue.newCLUint64(p.deadline),
127
+ }));
128
+ }
129
+ // ---------------------------------------------------------------------------
130
+ // submitWork — Escrow submit_work(job_id, result_hash)
131
+ // ---------------------------------------------------------------------------
132
+ export function buildSubmitWork(cfg, signer, jobId, resultHash) {
133
+ return buildCall(cfg, signer, cfg.packages.escrow, "submit_work", Args.fromMap({
134
+ job_id: CLValue.newCLUint64(jobId.toString()),
135
+ result_hash: CLValue.newCLString(resultHash),
136
+ }));
137
+ }
138
+ // ---------------------------------------------------------------------------
139
+ // approveJob — Escrow approve(job_id)
140
+ // ---------------------------------------------------------------------------
141
+ export function buildApproveJob(cfg, signer, jobId) {
142
+ return buildCall(cfg, signer, cfg.packages.escrow, "approve", Args.fromMap({
143
+ job_id: CLValue.newCLUint64(jobId.toString()),
144
+ }));
145
+ }
146
+ /**
147
+ * Builds the ordered transaction sequence for a full settlement:
148
+ * 1. approveToken (client approves escrow to spend tokens)
149
+ * 2. createJob (client locks funds in escrow)
150
+ * 3. submitWork (provider submits deliverable hash)
151
+ * 4. approveJob (client approves work, triggers 2% burn + reputation update)
152
+ *
153
+ * Returns the four built+signed Transaction objects in sequence order so the
154
+ * caller can inspect structure offline, then submit in order with waitForTx
155
+ * between each when a funded key is available.
156
+ *
157
+ * Does NOT submit any transaction.
158
+ */
159
+ export function buildAttestSettlement(cfg, p) {
160
+ const approveTx = buildApproveToken(cfg, p.clientSigner, cfg.packages.escrow, p.tokenAmount);
161
+ const createJobTx = buildCreateJob(cfg, p.clientSigner, {
162
+ clientId: p.clientId,
163
+ provider: p.providerId,
164
+ amount: p.tokenAmount,
165
+ deadline: p.deadline,
166
+ });
167
+ const submitWorkTx = buildSubmitWork(cfg, p.providerSigner, p.jobId, p.resultHash);
168
+ const approveJobTx = buildApproveJob(cfg, p.clientSigner, p.jobId);
169
+ return { approveTx, createJobTx, submitWorkTx, approveJobTx };
170
+ }
@@ -0,0 +1,3 @@
1
+ import { RpcClient } from "casper-js-sdk";
2
+ import type { NetworkConfig } from "../config.js";
3
+ export declare function makeRpcClient(cfg: NetworkConfig): RpcClient;
@@ -0,0 +1,7 @@
1
+ import { HttpHandler, RpcClient } from "casper-js-sdk";
2
+ export function makeRpcClient(cfg) {
3
+ const handler = new HttpHandler(cfg.rpcUrl, "fetch");
4
+ if (cfg.authToken)
5
+ handler.setCustomHeaders({ Authorization: cfg.authToken });
6
+ return new RpcClient(handler);
7
+ }
@@ -0,0 +1,9 @@
1
+ import type { RpcClient } from "casper-js-sdk";
2
+ /**
3
+ * Resolves a ContractPackage hash to its active contract hash + the "state" URef.
4
+ * The "state" URef is the Odra dictionary seed for reading stored fields.
5
+ */
6
+ export declare function resolvePackage(rpc: RpcClient, packageHashHex: string): Promise<{
7
+ contractHash: string;
8
+ stateUref: string;
9
+ }>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Resolves a ContractPackage hash to its active contract hash + the "state" URef.
3
+ * The "state" URef is the Odra dictionary seed for reading stored fields.
4
+ */
5
+ export async function resolvePackage(rpc, packageHashHex) {
6
+ const pkg = await rpc.queryLatestGlobalState(`hash-${packageHashHex}`, []);
7
+ // StoredValue.contractPackage — lowercase 'c' in casper-js-sdk v5
8
+ const versions = pkg.storedValue.contractPackage?.versions;
9
+ if (!versions || versions.length === 0) {
10
+ throw new Error(`No versions found in package ${packageHashHex}`);
11
+ }
12
+ // contractHash is a ContractHash object — use .hash.toHex() for raw 64-char hex
13
+ const latestVersion = versions[versions.length - 1];
14
+ const contractHash = latestVersion.contractHash.hash.toHex();
15
+ const contractResult = await rpc.queryLatestGlobalState(`hash-${contractHash}`, []);
16
+ // StoredValue.contract.namedKeys is NamedKey[] — NamedKey.key is a Key object
17
+ const namedKeys = contractResult.storedValue.contract?.namedKeys;
18
+ if (!namedKeys) {
19
+ throw new Error(`No namedKeys on contract ${contractHash}`);
20
+ }
21
+ const stateEntry = namedKeys.find((k) => k.name === "state");
22
+ if (!stateEntry) {
23
+ throw new Error(`No 'state' named key on contract ${contractHash}`);
24
+ }
25
+ // Key.toPrefixedString() returns "uref-<hex>-<access>" format
26
+ return { contractHash, stateUref: stateEntry.key.toPrefixedString() };
27
+ }
@@ -0,0 +1,3 @@
1
+ import type { Reputation, Agent } from "../types.js";
2
+ export declare function decodeReputation(p: Uint8Array): Reputation;
3
+ export declare function decodeAgent(p: Uint8Array): Agent;
@@ -0,0 +1,21 @@
1
+ import { Reader, u32, u64, uN, str, addr, unitEnum } from "../odra/bytesrepr.js";
2
+ export function decodeReputation(p) {
3
+ const r = new Reader(p);
4
+ return {
5
+ jobsCompleted: u64(r),
6
+ totalVolume: uN(r),
7
+ distinctClients: u32(r),
8
+ scoreBps: uN(r),
9
+ grantedOutBps: uN(r),
10
+ };
11
+ }
12
+ export function decodeAgent(p) {
13
+ const r = new Reader(p);
14
+ return {
15
+ owner: addr(r),
16
+ wallet: addr(r),
17
+ agentUri: str(r),
18
+ bond: uN(r),
19
+ status: unitEnum(r, ["Active", "Slashed", "Withdrawn"]),
20
+ };
21
+ }
@@ -0,0 +1,12 @@
1
+ import type { RpcClient } from "casper-js-sdk";
2
+ import type { NetworkConfig } from "../config.js";
3
+ import type { Reputation, Agent, TrustResult } from "../types.js";
4
+ export interface TrustClient {
5
+ rpc: RpcClient;
6
+ cfg: NetworkConfig;
7
+ }
8
+ export declare function getReputation(c: TrustClient, agentId: number): Promise<Reputation>;
9
+ export declare function getAgent(c: TrustClient, agentId: number): Promise<Agent | null>;
10
+ export declare function checkTrust(c: TrustClient, agentId: number, opts?: {
11
+ minScore?: bigint;
12
+ }): Promise<TrustResult>;
@@ -0,0 +1,36 @@
1
+ import { resolvePackage } from "../rpc/resolve.js";
2
+ import { readOdraValue } from "../odra/read.js";
3
+ import { mapKeyU32 } from "../odra/keys.js";
4
+ import { decodeReputation, decodeAgent } from "./decode.js";
5
+ const ZERO_REP = {
6
+ jobsCompleted: 0n,
7
+ totalVolume: 0n,
8
+ distinctClients: 0,
9
+ scoreBps: 0n,
10
+ grantedOutBps: 0n,
11
+ };
12
+ export async function getReputation(c, agentId) {
13
+ const { contractHash } = await resolvePackage(c.rpc, c.cfg.packages.reputation);
14
+ const p = await readOdraValue(c.rpc, contractHash, mapKeyU32(c.cfg.fields.reputation.reps, agentId));
15
+ return p ? decodeReputation(p) : ZERO_REP; // absent = never settled = zero (mirror on-chain default)
16
+ }
17
+ export async function getAgent(c, agentId) {
18
+ const { contractHash } = await resolvePackage(c.rpc, c.cfg.packages.identity);
19
+ const p = await readOdraValue(c.rpc, contractHash, mapKeyU32(c.cfg.fields.identity.agents, agentId));
20
+ return p ? decodeAgent(p) : null; // absent = agent does not exist
21
+ }
22
+ export async function checkTrust(c, agentId, opts = {}) {
23
+ const [agent, rep] = await Promise.all([getAgent(c, agentId), getReputation(c, agentId)]);
24
+ const exists = agent !== null;
25
+ const min = opts.minScore ?? 0n;
26
+ const trusted = exists && agent.status === "Active" && rep.scoreBps >= min;
27
+ return {
28
+ agentId,
29
+ exists,
30
+ trusted,
31
+ score: rep.scoreBps,
32
+ jobsCompleted: rep.jobsCompleted,
33
+ status: agent?.status ?? "None",
34
+ bond: agent?.bond ?? 0n,
35
+ };
36
+ }
@@ -0,0 +1,23 @@
1
+ export interface Reputation {
2
+ jobsCompleted: bigint;
3
+ totalVolume: bigint;
4
+ distinctClients: number;
5
+ scoreBps: bigint;
6
+ grantedOutBps: bigint;
7
+ }
8
+ export interface Agent {
9
+ owner: string;
10
+ wallet: string;
11
+ agentUri: string;
12
+ bond: bigint;
13
+ status: "Active" | "Slashed" | "Withdrawn";
14
+ }
15
+ export interface TrustResult {
16
+ agentId: number;
17
+ exists: boolean;
18
+ trusted: boolean;
19
+ score: bigint;
20
+ jobsCompleted: bigint;
21
+ status: Agent["status"] | "None";
22
+ bond: bigint;
23
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import type { TrustClient } from "../trust/index.js";
2
+ export declare class TrustGateError extends Error {
3
+ readonly agentId: number;
4
+ readonly score: bigint;
5
+ readonly minScore: bigint;
6
+ constructor(agentId: number, score: bigint, minScore: bigint);
7
+ }
8
+ type CheckFn = (agentId: number, opts: {
9
+ minScore?: bigint;
10
+ }) => Promise<{
11
+ trusted: boolean;
12
+ score: bigint;
13
+ }>;
14
+ /**
15
+ * Pure gate: throws TrustGateError if a provider is below threshold.
16
+ * No-op when minScore or providerAgentId is undefined.
17
+ */
18
+ export declare function gateByTrust(check: CheckFn, providerAgentId?: number, minScore?: bigint): Promise<void>;
19
+ export interface PayRequest {
20
+ url: string;
21
+ providerAgentId?: number;
22
+ minScore?: bigint;
23
+ init?: RequestInit;
24
+ }
25
+ /**
26
+ * The x402 client signer shape produced by createClientCasperSigner /
27
+ * toClientCasperSigner from @make-software/casper-x402.
28
+ * The package does not export a named interface type — this mirrors the
29
+ * return type of toClientCasperSigner() exactly.
30
+ */
31
+ export interface CasperClientSigner {
32
+ accountAddress(): string;
33
+ publicKey(): string;
34
+ signEIP712(digest: Uint8Array): Promise<Uint8Array>;
35
+ }
36
+ /**
37
+ * Extended TrustClient that carries an x402 client signer.
38
+ *
39
+ * Delegation: pay() builds an x402Client (from @x402/core) registered with
40
+ * ExactCasperScheme and delegates the 402-handshake to wrapFetchWithPayment
41
+ * from @x402/fetch. That wrapper owns the retry loop and sets the
42
+ * PAYMENT-SIGNATURE header (x402 v2 wire contract — v1 used X-PAYMENT).
43
+ */
44
+ export interface X402TrustClient extends TrustClient {
45
+ signer: CasperClientSigner;
46
+ /** CAIP-2 network override; defaults to NETWORK_CASPER_TESTNET ("casper:casper-test") */
47
+ network?: string;
48
+ }
49
+ /**
50
+ * Trust-gated x402 v2 payment wrapper.
51
+ *
52
+ * 1. Gates on the provider's on-chain trust score (throws TrustGateError if below minScore).
53
+ * 2. Delegates the 402-handshake to wrapFetchWithPayment (@x402/fetch), which:
54
+ * - Detects the 402 response
55
+ * - Calls x402Client.createPaymentPayload (dispatches to ExactCasperScheme)
56
+ * - Retries with PAYMENT-SIGNATURE header (x402 v2 wire name; v1 was X-PAYMENT)
57
+ * - Uses amount field from paymentRequirements (x402 v2 — not maxAmountRequired)
58
+ *
59
+ * Integration note: live facilitator not exercised here; the 402-loop is
60
+ * verified at the unit level via the gating tests. The handshake will be
61
+ * exercised end-to-end in the dashboard/demo task.
62
+ */
63
+ export declare function pay(c: X402TrustClient, req: PayRequest): Promise<Response>;
64
+ export {};
@@ -0,0 +1,61 @@
1
+ import { checkTrust } from "../trust/index.js";
2
+ import { ExactCasperScheme, NETWORK_CASPER_TESTNET } from "@make-software/casper-x402";
3
+ import { wrapFetchWithPayment } from "@x402/fetch";
4
+ import { x402Client } from "@x402/core/client";
5
+ // ---------------------------------------------------------------------------
6
+ // Trust gate
7
+ // ---------------------------------------------------------------------------
8
+ export class TrustGateError extends Error {
9
+ agentId;
10
+ score;
11
+ minScore;
12
+ constructor(agentId, score, minScore) {
13
+ super(`agent ${agentId} score ${score} < required ${minScore}`);
14
+ this.agentId = agentId;
15
+ this.score = score;
16
+ this.minScore = minScore;
17
+ this.name = "TrustGateError";
18
+ }
19
+ }
20
+ /**
21
+ * Pure gate: throws TrustGateError if a provider is below threshold.
22
+ * No-op when minScore or providerAgentId is undefined.
23
+ */
24
+ export async function gateByTrust(check, providerAgentId, minScore) {
25
+ if (minScore === undefined || providerAgentId === undefined)
26
+ return;
27
+ const r = await check(providerAgentId, { minScore });
28
+ if (!r.trusted)
29
+ throw new TrustGateError(providerAgentId, r.score, minScore);
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // pay() — trust-gated x402 payment
33
+ // ---------------------------------------------------------------------------
34
+ /**
35
+ * Trust-gated x402 v2 payment wrapper.
36
+ *
37
+ * 1. Gates on the provider's on-chain trust score (throws TrustGateError if below minScore).
38
+ * 2. Delegates the 402-handshake to wrapFetchWithPayment (@x402/fetch), which:
39
+ * - Detects the 402 response
40
+ * - Calls x402Client.createPaymentPayload (dispatches to ExactCasperScheme)
41
+ * - Retries with PAYMENT-SIGNATURE header (x402 v2 wire name; v1 was X-PAYMENT)
42
+ * - Uses amount field from paymentRequirements (x402 v2 — not maxAmountRequired)
43
+ *
44
+ * Integration note: live facilitator not exercised here; the 402-loop is
45
+ * verified at the unit level via the gating tests. The handshake will be
46
+ * exercised end-to-end in the dashboard/demo task.
47
+ */
48
+ export async function pay(c, req) {
49
+ if (!c.signer)
50
+ throw new Error("pay() requires a signer-bearing client; build it as { ...createTrustClient(overrides), signer }");
51
+ // Step 1: gate
52
+ await gateByTrust((id, o) => checkTrust(c, id, o), req.providerAgentId, req.minScore);
53
+ // Step 2: build x402Client with ExactCasperScheme and delegate the 402-loop
54
+ // x402 v2 wire: amount (not maxAmountRequired), PAYMENT-SIGNATURE header
55
+ // Network defaults to casper:casper-test per research §2 (CAIP-2 identifier)
56
+ const network = c.network ?? NETWORK_CASPER_TESTNET;
57
+ const client = new x402Client();
58
+ client.register(network, new ExactCasperScheme(c.signer));
59
+ const paymentFetch = wrapFetchWithPayment(fetch, client);
60
+ return paymentFetch(req.url, req.init);
61
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "casper-trust",
3
+ "version": "0.1.0",
4
+ "description": "The settled-payment trust layer for Casper agents — read reputation in one line, gate x402 payments on it.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "keywords": [
18
+ "casper",
19
+ "x402",
20
+ "ai-agents",
21
+ "agent-trust",
22
+ "reputation",
23
+ "erc-8004",
24
+ "escrow",
25
+ "web3",
26
+ "blockchain"
27
+ ],
28
+ "author": "Ebubekir Erdem",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Bekirerdem/casper-trust-layer.git",
32
+ "directory": "sdk"
33
+ },
34
+ "homepage": "https://github.com/Bekirerdem/casper-trust-layer#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/Bekirerdem/casper-trust-layer/issues"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.json",
40
+ "test": "vitest run",
41
+ "test:live": "CASPER_LIVE=1 vitest run"
42
+ },
43
+ "license": "MIT",
44
+ "dependencies": {
45
+ "@make-software/casper-x402": "^0.1.0",
46
+ "@x402/fetch": "^2.15.0",
47
+ "blakejs": "^1.2.1",
48
+ "casper-js-sdk": "5.0.12"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^25.9.3",
52
+ "dotenv": "^16.4.0",
53
+ "typescript": "^5.5.0",
54
+ "vitest": "^2.0.0"
55
+ }
56
+ }