npm package: @kiroboio/offchain-triggers
Description: Runtime-agnostic library for building chained uint256 hashes for Ethereum smart contracts
Author: Kirobo
Latest version: 0.3.1

# @kirobo/offchain-triggers

Runtime-agnostic library for building chained uint256 hashes for Ethereum smart contracts. Works seamlessly with Node.js, Deno, and Bun.

## Features

- Runtime agnostic (Node.js, Deno, Bun)
- TypeScript support with full type definitions
- Zero dependencies (ethers as peer dependency)
- Comprehensive test coverage
- ESM and CommonJS support
- Published to both npm and JSR (Deno registry)

## Installation

### Node.js / Bun

```bash
npm install @kirobo/offchain-triggers ethers
```

or with yarn:

```bash
yarn add @kirobo/offchain-triggers ethers
```

or with pnpm:

```bash
pnpm add @kirobo/offchain-triggers ethers
```

### Deno

```typescript
import { buildChainedUint256Hash } from "jsr:@kirobo/offchain-triggers";
```

## Usage

### Basic Example

```typescript
import { buildChainedUint256Hash } from "@kirobo/offchain-triggers";

// Build hash from array of uint256 values
const hash = buildChainedUint256Hash([1, 2, 3]);
console.log(hash);
// Output: 0x114e0011faa5b73076d891c6b6c015a927a2f8e4d2cf1544f43b836f6a5a77b1
```

### Deno Edge Function Example

```typescript
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
import { Wallet } from "npm:ethers@6";
import { buildChainedUint256Hash } from "jsr:@kirobo/offchain-triggers";

const PRIVATE_KEY = Deno.env.get("SIGNER_PRIVATE_KEY");
const wallet = new Wallet(PRIVATE_KEY);

serve(async (req) => {
  try {
    const body = await req.json();
    const values = body?.values;

    if (!Array.isArray(values) || values.length < 2) {
      return new Response("Invalid input", { status: 400 });
    }

    // Build the chained hash
    const hash = buildChainedUint256Hash(values);

    // Sign the hash
    const sig = wallet.signingKey.sign(hash);
    const { v, r, s } = sig;

    return new Response(
      JSON.stringify({
        hash,
        v,
        r,
        s,
        signerAddress: wallet.address,
      }),
      {
        status: 200,
        headers: { "Content-Type": "application/json" },
      }
    );
  } catch (err) {
    console.error(err);
    return new Response("Internal error", { status: 500 });
  }
});
```

### Different Input Types

The function accepts bigint, string, or number values:

```typescript
import { buildChainedUint256Hash } from "@kirobo/offchain-triggers";

// Using numbers
const hash1 = buildChainedUint256Hash([1, 2, 3]);

// Using bigints
const hash2 = buildChainedUint256Hash([1n, 2n, 3n]);

// Using strings
const hash3 = buildChainedUint256Hash(["1", "2", "3"]);

// Using hex strings
const hash4 = buildChainedUint256Hash(["0x1", "0x2", "0x3"]);

// Mixed types
const hash5 = buildChainedUint256Hash([1n, "2", 3, "0x4"]);

// All produce the same hash for equivalent values
console.log(hash1 === hash2); // true
console.log(hash2 === hash3); // true
```

### Node.js Example

```typescript
import { buildChainedUint256Hash } from "@kirobo/offchain-triggers";
import { Wallet } from "ethers";

async function signChainedHash(values: number[]) {
  const hash = buildChainedUint256Hash(values);
  const wallet = new Wallet(process.env.PRIVATE_KEY);
  const sig = wallet.signingKey.sign(hash);

  return {
    hash,
    signature: sig,
  };
}

const result = await signChainedHash([1, 2, 3, 4, 5]);
console.log(result);
```

### Bun Example

```typescript
import { buildChainedUint256Hash } from "@kirobo/offchain-triggers";

// Bun works exactly like Node.js
const hash = buildChainedUint256Hash([1, 2, 3]);
console.log(hash);
```

## API

### `buildChainedUint256Hash(values: Uint256Value[]): string`

Builds a chained hash from an array of uint256 values.

The hash is computed as follows:

```
h0 = keccak256(abi.encodePacked(v0, v1))
h1 = keccak256(abi.encodePacked(h0, v2))
h2 = keccak256(abi.encodePacked(h1, v3))
...
```

This matches Solidity's `keccak256(abi.encodePacked(...))` behavior for chained hashing.

#### Parameters

- `values: Uint256Value[]` - Array of at least 2 uint256 values (can be bigint, string, or number)

#### Returns

- `string` - The final hash as a hex string (bytes32, 0x-prefixed)

#### Throws

- `Error` - If less than 2 values are provided
- `Error` - If any value cannot be converted to a valid uint256
- `Error` - If any value is negative
- `Error` - If any value exceeds uint256 max (2^256 - 1)

#### Type Definition

```typescript
type Uint256Value = bigint | string | number;
```

## How It Works

The library implements the same chained hashing algorithm used in Solidity:

1. First hash: `keccak256(abi.encodePacked(uint256(v0), uint256(v1)))`
2. Subsequent hashes: `keccak256(abi.encodePacked(bytes32(h), uint256(vi)))`

This creates a chain of hashes where each hash includes the previous hash and the next value, making it impossible to modify any value without invalidating the entire chain.

## Solidity Verification

You can verify the hashes produced by this library match Solidity:

```solidity
function verifyChainedHash(
    uint256[] memory values,
    bytes32 expectedHash
) public pure returns (bool) {
    require(values.length >= 2, "Need at least 2 values");

    bytes32 h = keccak256(abi.encodePacked(values[0], values[1]));

    for (uint256 i = 2; i < values.length; i++) {
        h = keccak256(abi.encodePacked(h, values[i]));
    }

    return h == expectedHash;
}
```

## Development

### Install Dependencies

```bash
npm install
```

### Build

```bash
npm run build
```

### Test

```bash
npm test
```

### Lint

```bash
npm run lint
```

### Format

```bash
npm run format
```

## Publishing

### Publish to npm

```bash
npm publish
```

### Publish to JSR (Deno registry)

```bash
npx jsr publish
```

## License

MIT

## Contributing

Contributions are welcome! Please open an issue or submit a pull request.

## Support

For issues and questions, please open an issue on GitHub.