# Cartouche

[![Hex.pm](https://img.shields.io/hexpm/v/cartouche.svg)](https://hex.pm/packages/cartouche)

Lightweight Ethereum and Solana RPC client for Elixir. Cartouche is an **attributed fork** of [hayesgm/signet](https://github.com/hayesgm/signet) maintained by [ZenHive](https://github.com/ZenHive).

It bundles four capabilities into one library:

- **JSON-RPC clients** for Ethereum and Solana (`Cartouche.RPC`, `Cartouche.Solana.RPC`).
- **Signers** as supervised GenServers — local secp256k1 / Ed25519 seeds, or GCP Cloud KMS — exposing a uniform `sign/3` API.
- **Transaction builders** for legacy (V1) and EIP-1559 (V2) Ethereum transactions, plus Solana legacy transactions.
- **Contract codegen** — `mix cartouche.gen` turns Foundry / Hardhat artifacts into typed Elixir modules with `encode_*` / `call_*` / `execute_*` helpers backed by the RPC client.

## Status

**`0.1.1` — current release.** Ports the signet codebase under the `Cartouche` module tree, adds the Solana surface, and ships a published-on-hex ABI dependency (`hieroglyph`). See [CHANGELOG.md](CHANGELOG.md) for what has shipped.

## Installation

```elixir
def deps do
  [
    {:cartouche, "~> 0.1"}
  ]
end
```

## Configuration

Cartouche is an OTP application — its supervisor starts on boot and reads `config :cartouche, ...`. A typical mainnet setup:

```elixir
# config/runtime.exs
import Config

config :cartouche,
  chain_id: 1,
  ethereum_node: "https://mainnet.infura.io/v3/" <> System.fetch_env!("INFURA_KEY"),
  signer: [
    default: {:priv_key, System.fetch_env!("ETH_PRIVATE_KEY")}
  ]
```

Each entry under `:signer` becomes a supervised `Cartouche.Signer` GenServer; the `:default` name is special — it's registered as `Cartouche.Signer.Default` and used when a caller doesn't pass `:signer` explicitly. Solana mirrors this with `:solana_node` and `:solana_signer`.

| Key | Default | Purpose |
| --- | --- | --- |
| `:chain_id` | `1` | Default Ethereum chain ID for signers and transactions |
| `:ethereum_node` | `"https://mainnet.infura.io"` | Ethereum JSON-RPC endpoint |
| `:signer` | `[]` | List of `{name, signer_spec}` for Ethereum signers |
| `:solana_node` | `nil` | Solana JSON-RPC endpoint (required for any Solana RPC call) |
| `:solana_signer` | `[]` | List of `{name, signer_spec}` for Solana signers |
| `:contracts` | `[]` | Named contract address registry — see `Cartouche.get_contract_address/1` |
| `:client` | `Finch` | HTTP client module |
| `:finch_name` | `CartoucheFinch` | Name of the supervised Finch pool |
| `:start_finch` | `true` | Set `false` to manage your own Finch pool |
| `:timeout` | `30_000` | Ethereum RPC request timeout (ms) — compile-time |
| `:solana_timeout` | `30_000` | Solana RPC request timeout (ms) — compile-time |
| `:open_chain_client` | `Finch` | HTTP client for OpenChain / 4byte signature lookups |
| `:open_chain_base_url` | `"https://api.4byte.sourcify.dev"` | OpenChain base URL |

Signer specs:

```elixir
# Local secp256k1 key (Ethereum)
{:priv_key, "0xdeadbeef..."}

# GCP Cloud KMS (Ethereum)
{:cloud_kms, kms_credentials, "projects/P/locations/L/keyRings/R/cryptoKeys/K", "1"}

# Local Ed25519 seed (Solana) — accepts raw 32-byte binary, hex, or Base58
{:ed25519, "0x..."}

# GCP Cloud KMS (Solana, Ed25519)
{:cloud_kms, kms_credentials, "projects/P/locations/L/keyRings/R/cryptoKeys/K", "1"}
```

## Quick start: send your first transaction

With the configuration above (a `:default` signer registered), `Cartouche.RPC.execute_trx/3` looks up the nonce, signs, and sends in one call:

```elixir
{:ok, tx_hash} =
  Cartouche.RPC.execute_trx(
    <<1::160>>,                                                # contract address (20 bytes)
    {"baz(uint,address)", [50, :binary.decode_unsigned(<<1::160>>)]},
    base_fee: {1, :gwei},
    priority_fee: {3, :gwei},
    value: 0
  )
```

`execute_trx/3` accepts:

- `:gas_price` — V1 (legacy) path. Mutually exclusive with the V2 fee fields.
- `:base_fee` and `:priority_fee` — V2 (EIP-1559) path. If both are omitted, V2 is the default and `eth_gasPrice` is consulted for the base fee.
- `:gas_limit` — defaults to `eth_estimateGas` × `:gas_buffer` (1.5).
- `:nonce` — defaults to `eth_getTransactionCount`.
- `:value` — wei (default `0`). Accepts `{n, :gwei}` or a bare integer.
- `:verify` — runs `eth_call` first to surface revert reasons before broadcasting (default `true`).
- `:trx_type` — `:v1`, `:v2`, or `nil` (auto-detect).
- `:signer`, `:chain_id` — override the configured defaults.

`Cartouche.RPC.prepare_trx/3` has the same option surface but returns the signed `%V1{}` or `%V2{}` struct without broadcasting — useful for offline signing or batch submission.

## Ethereum

### RPC calls

`Cartouche.RPC` exposes the JSON-RPC surface and a small set of higher-level wrappers. The transport is `send_rpc/3`; everything else is convenience:

```elixir
{:ok, balance_wei}    = Cartouche.RPC.get_balance(<<1::160>>)
{:ok, nonce}          = Cartouche.RPC.get_nonce(<<1::160>>)
{:ok, chain_id}       = Cartouche.RPC.eth_chain_id()
{:ok, block_number}   = Cartouche.RPC.eth_block_number()
{:ok, %Cartouche.Block{}}   = Cartouche.RPC.get_block_by_number(block_number)
{:ok, %Cartouche.Block{}}   = Cartouche.RPC.get_block_by_number("latest")
{:ok, %Cartouche.Receipt{}} = Cartouche.RPC.get_trx_receipt(tx_hash)

# Read-only contract call (eth_call)
{:ok, return_data} =
  Cartouche.RPC.call_trx(%Cartouche.Transaction.V2{
    destination: contract,
    data: call_data
  })
```

Every RPC function takes a final `opts` keyword list — `:ethereum_node`, `:block_number`, `:timeout`, `:headers` — letting you target multiple nodes from one process tree.

### Signing

A signer process knows its own address and signs on demand. With a `:default` entry in config, callers don't need to pass anything:

```elixir
{:ok, signature} = Cartouche.Signer.sign("hello world")
address          = Cartouche.Signer.address()  # 20-byte binary
```

To start a signer manually (e.g. in a test):

```elixir
{:ok, pid} =
  Cartouche.Signer.start_link(
    mfa: {Cartouche.Signer.Curvy, :sign, [private_key_bytes]},
    name: MySigner
  )

{:ok, sig} = Cartouche.Signer.sign("hello", MySigner)
```

Each signer process keeps its own public key, and signatures are verified against it before they're returned. Cloud KMS doesn't emit a recovery bit, so Cartouche tries all four and picks the one that recovers to the registered address.

### Transactions

Build, sign, and encode a **V1 (legacy)** transaction:

```elixir
{:ok, signed_trx} =
  Cartouche.Transaction.build_signed_trx(
    contract,         # 20-byte address
    nonce,            # integer
    {"baz(uint,address)", [50, :binary.decode_unsigned(<<1::160>>)]},
    {50, :gwei},      # gas price
    100_000,          # gas limit
    0,                # value
    chain_id: :goerli
  )

raw = Cartouche.Transaction.V1.encode(signed_trx)
{:ok, tx_hash} = Cartouche.RPC.send_rpc("eth_sendRawTransaction", [Cartouche.Hex.to_hex(raw)])
```

Build, sign, and encode a **V2 (EIP-1559)** transaction:

```elixir
{:ok, signed_trx} =
  Cartouche.Transaction.build_signed_trx_v2(
    contract,
    nonce,
    {"baz(uint,address)", [50, :binary.decode_unsigned(<<1::160>>)]},
    {50, :gwei},      # max priority fee per gas
    {10, :gwei},      # max fee per gas
    100_000,          # gas limit
    0,                # value
    [],               # access list
    chain_id: :goerli
  )

raw = Cartouche.Transaction.V2.encode(signed_trx)
```

Both builders accept a `:callback` option — a `fn trx -> {:ok, trx} | {:error, reason}` run after construction and before signing — useful for last-mile mutations (nonce reservation, gas overrides). `V1.recover_signer/2` and `V2.recover_signer/1` round-trip the encoded form back to the signing address.

## Contract bindings

`mix cartouche.gen` turns Solidity build artifacts into Elixir modules:

```bash
mix cartouche.gen "out/**/*.json" --prefix my_app/contracts
```

Flags:

- `--prefix` — module + path prefix. `my_app/contracts` produces `MyApp.Contracts.SomeContract` in `lib/my_app/contracts/some_contract.ex`.
- `--out` — output root (default `lib/`).

The generator accepts both raw ABI JSON arrays and full Foundry / Hardhat artifacts (with `"abi"` and `"bytecode"`). For each ABI entry it emits:

- `encode_<fn>/N` — ABI-encodes the call data
- `selector_<fn>/0` — returns the `%ABI.FunctionSelector{}`
- `call_<fn>(contract, args, opts)` — wraps `Cartouche.RPC.call_trx/2`
- `execute_<fn>(contract, args, opts)` — wraps `Cartouche.RPC.execute_trx/3`
- `prepare_<fn>(contract, args, opts)` — wraps `Cartouche.RPC.prepare_trx/3`
- `estimate_gas_<fn>(contract, args, opts)`
- `decode_call/1`, `decode_event/2`, `decode_error/1` dispatchers
- For pure functions with bytecode: `exec_vm_<fn>` (local EVM execution via `Cartouche.VM`)

Once generated, callsites read like any other Elixir module:

```elixir
{:ok, tx_hash} =
  MyApp.Contracts.SomeContract.execute_some_function(addr, 55, priority_fee: {55, :gwei})
```

## Solana

Solana support mirrors the Ethereum surface. With `:solana_node` and a `:solana_signer` configured:

```elixir
fee_payer  = Cartouche.Solana.Signer.address()  # 32-byte pubkey from configured signer
recipient  = Cartouche.Base58.decode!("RecipientPublicKeyInBase58...")
{:ok, %{blockhash: blockhash}} = Cartouche.Solana.RPC.get_latest_blockhash()

instruction = Cartouche.Solana.SystemProgram.transfer(fee_payer, recipient, 1_000_000_000)
message     = Cartouche.Solana.Transaction.build_message(fee_payer, [instruction], blockhash)

# Sign via the configured GenServer signer (no raw seed handling in app code)
msg_bytes  = Cartouche.Solana.Transaction.serialize_message(message)
{:ok, sig} = Cartouche.Solana.Signer.sign(msg_bytes)
signed     = %Cartouche.Solana.Transaction{signatures: [sig], message: message}

{:ok, signature} = Cartouche.Solana.RPC.send_and_confirm(signed, commitment: :confirmed)
```

For offline signing (no GenServer), pass raw 32-byte Ed25519 seeds directly: `Cartouche.Solana.Transaction.sign(message, [fee_payer_seed])`. For sponsored transactions (one party pays fees for another), see `Cartouche.Solana.Transaction.sign_partial/2` and `add_signature/3`.

`Cartouche.Solana.RPC` covers the standard JSON-RPC surface (`get_balance/2`, `get_account_info/2`, `simulate_transaction/2`, `request_airdrop/3`, plus the SPL token and fee queries). `Cartouche.Solana.Keys` handles keypair generation, seed loading, and Base58 conversion; `Cartouche.Solana.Signer` is the GenServer parallel to `Cartouche.Signer` for both Ed25519 and Cloud KMS backends.

## Hex utilities

`use Cartouche.Hex` brings in the `~h` sigil for compile-time hex literals plus the common encoders:

```elixir
defmodule MyApp.Calls do
  use Cartouche.Hex

  @selector ~h[0xa9059cbb]                 # decoded at compile time

  def is_transfer?(<<@selector::binary, _rest::binary>>), do: true
  def is_transfer?(_), do: false
end
```

Module-level helpers:

```elixir
Cartouche.Hex.decode_hex!("0xaabb")            # <<0xaa, 0xbb>>
Cartouche.Hex.to_hex(<<0xaa, 0xbb>>)           # "0xaabb"
Cartouche.Hex.to_address(<<1::160>>)           # "0x0000...0001" (EIP-55 checksummed)
Cartouche.Hash.keccak("hello")                 # 32-byte digest
Cartouche.Wei.to_wei({2, :gwei})               # 2_000_000_000
```

## Modules at a glance

| Module | What it does |
| --- | --- |
| `Cartouche.RPC` | Ethereum JSON-RPC client; high-level `execute_trx` / `prepare_trx` / `call_trx` |
| `Cartouche.Signer` | GenServer signer (secp256k1, Cloud KMS) — `sign/3`, `address/1` |
| `Cartouche.Transaction` | V1 (legacy) and V2 (EIP-1559) builders, encoders, signature recovery |
| `Cartouche.Hex` / `Cartouche.Hash` | `~h` sigil, encode/decode helpers, keccak digests |
| `Cartouche.Wei` | `to_wei/1` — accepts integers or `{n, :gwei}` |
| `Cartouche.Solana.RPC` | Solana JSON-RPC client |
| `Cartouche.Solana.Transaction` | Build / sign / serialize Solana legacy transactions |
| `Cartouche.Solana.Signer` | GenServer Ed25519 signer (local seed, Cloud KMS) |
| `Mix.Tasks.Cartouche.Gen` | Codegen from Solidity artifacts — `mix cartouche.gen` |

## Documentation

Full API reference: [hexdocs.pm/cartouche](https://hexdocs.pm/cartouche). Release history: [CHANGELOG.md](CHANGELOG.md).

## Relationship to upstream

Cartouche is a fork of `hayesgm/signet`. We upstream fixes where it makes sense. Attribution to the original maintainer (Geoffrey Hayes, Compound Labs) is preserved in `LICENSE` and `CHANGELOG.md`.

## License

MIT. See `LICENSE`.
