# Viem integration

The Openfort Node SDK integrates with [Viem](https://viem.sh) to provide type-safe signing operations for Ethereum backend wallets. This integration handles message hashing, transaction serialization, and typed data encoding according to Ethereum standards.

## Install dependencies

:::code-group
```sh [npm]
npm install viem

```

```sh [yarn]
yarn add viem

```

```sh [pnpm]
pnpm add viem

```
:::

:::tip[Runnable examples]
See the complete runnable examples for [EVM signing](https://github.com/openfort-xyz/openfort-node/tree/main/examples/evm/signing) in the Node SDK repository.
:::

## Overview

Backend wallet accounts implement signing methods that use viem internally:

* `signMessage()` applies EIP-191 message hashing
* `signTypedData()` applies EIP-712 typed data hashing
* `signTransaction()` serializes and signs transactions
* `sign()` signs raw hashes directly

All methods return signatures as hex strings compatible with viem and other EVM libraries.

## Signing messages

Sign arbitrary messages using EIP-191 personal sign format. The SDK hashes the message with the `"\x19Ethereum Signed Message:\n"` prefix before signing.

```ts
import 'dotenv/config'
import Openfort from '@openfort/openfort-node'

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
})

const account = await openfort.accounts.evm.backend.create()

// Sign a string message
const signature = await account.signMessage({
  message: 'Hello, Openfort!',
});

// Sign raw bytes
const rawSignature = await account.signMessage({
  message: { raw: '0x68656c6c6f' },
});

// Sign a Uint8Array
const bytes = new Uint8Array([72, 101, 108, 108, 111]);
const bytesSignature = await account.signMessage({
  message: { raw: bytes },
});
```

### Message formats

The `signMessage` method accepts three message formats:

| Format | Example | Description |
|--------|---------|-------------|
| String | `'Hello'` | UTF-8 encoded message |
| Raw hex | `{ raw: '0x...' }` | Hex-encoded bytes |
| Uint8Array | `{ raw: new Uint8Array([...]) }` | Raw byte array |

## Signing typed data

Sign structured data according to EIP-712. The SDK uses viem's `hashTypedData` to compute the typed data hash before signing.

```ts
import 'dotenv/config'
import Openfort from '@openfort/openfort-node'

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
});

const account = await openfort.accounts.evm.backend.create();

// Define EIP-712 typed data
const signature = await account.signTypedData({
  domain: {
    name: 'MyApp',
    version: '1',
    chainId: 1,
    verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
  },
  types: {
    Person: [
      { name: 'name', type: 'string' },
      { name: 'wallet', type: 'address' },
    ],
    Mail: [
      { name: 'from', type: 'Person' },
      { name: 'to', type: 'Person' },
      { name: 'contents', type: 'string' },
    ],
  },
  primaryType: 'Mail',
  message: {
    from: {
      name: 'Alice',
      wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
    },
    to: {
      name: 'Bob',
      wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
    },
    contents: 'Hello, Bob!',
  },
});
```

### Permit signatures

A common use case for typed data signing is ERC-20 permit signatures:

```ts
import 'dotenv/config'
import Openfort from '@openfort/openfort-node'

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
});

const account = await openfort.accounts.evm.backend.get({
  address: '0x...',
});

// Sign an ERC-20 permit
const permitSignature = await account.signTypedData({
  domain: {
    name: 'USD Coin',
    version: '2',
    chainId: 1,
    verifyingContract: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  },
  types: {
    Permit: [
      { name: 'owner', type: 'address' },
      { name: 'spender', type: 'address' },
      { name: 'value', type: 'uint256' },
      { name: 'nonce', type: 'uint256' },
      { name: 'deadline', type: 'uint256' },
    ],
  },
  primaryType: 'Permit',
  message: {
    owner: account.address,
    spender: '0x...',
    value: 1000000n,
    nonce: 0n,
    deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
  },
});
```

## Signing transactions

Sign Ethereum transactions for later broadcast. The SDK serializes the transaction, signs it, and returns the fully signed transaction ready to submit to the network.

```ts
import 'dotenv/config'
import Openfort from '@openfort/openfort-node'
import { parseEther, parseGwei } from 'viem';

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
});

const account = await openfort.accounts.evm.backend.create();

// Sign an EIP-1559 transaction
const signedTx = await account.signTransaction({
  to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
  value: parseEther('0.001'),
  nonce: 0,
  gas: 21000n,
  maxFeePerGas: parseGwei('20'),
  maxPriorityFeePerGas: parseGwei('1'),
  chainId: 1,
});

// The signed transaction is ready to broadcast
console.log('Signed transaction:', signedTx);
```

### Transaction types

The `signTransaction` method accepts any viem `TransactionSerializable` type:

| Type | Required fields |
|------|-----------------|
| EIP-1559 | `chainId`, `maxFeePerGas`, `maxPriorityFeePerGas` |
| EIP-2930 | `chainId`, `gasPrice`, `accessList` |
| Legacy | `gasPrice` |

```ts
// Legacy transaction
const legacyTx = await account.signTransaction({
  to: '0x...',
  value: parseEther('0.1'),
  nonce: 0,
  gas: 21000n,
  gasPrice: parseGwei('30'),
});

// EIP-2930 transaction with access list
const accessListTx = await account.signTransaction({
  to: '0x...',
  value: 0n,
  nonce: 1,
  gas: 50000n,
  gasPrice: parseGwei('30'),
  chainId: 1,
  accessList: [
    {
      address: '0x...',
      storageKeys: ['0x...'],
    },
  ],
});
```

## Signing raw hashes

Sign a raw 32-byte hash without any additional encoding. Use this for custom signing schemes or when you need direct control over what gets signed.

```ts
import 'dotenv/config'
import Openfort from '@openfort/openfort-node'
import { keccak256, toBytes } from 'viem';

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
});

const account = await openfort.accounts.evm.backend.create();

// Compute a custom hash
const hash = keccak256(toBytes('Custom data to sign'));

// Sign the hash directly (no EIP-191 prefix)
const signature = await account.sign({ hash });
```

:::warning
Raw hash signing bypasses message prefixing. Only use this when you need to sign pre-computed hashes or implement custom signing schemes.
:::

## Using with viem clients

To send transactions through viem, wrap the Openfort backend wallet as a viem `LocalAccount` using [`toAccount`](https://viem.sh/docs/accounts/local/toAccount). This lets you use `createWalletClient` with all its standard methods (`sendTransaction`, `writeContract`, etc.) — viem handles nonce management, gas estimation, and broadcasting automatically.

### Sending a transaction

```ts
import 'dotenv/config'
import Openfort from '@openfort/openfort-node'
import { createWalletClient, http, parseEther } from 'viem'
import { toAccount } from 'viem/accounts'
import { baseSepolia } from 'viem/chains'

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
})

const account = await openfort.accounts.evm.backend.get({
  id: 'acc_...',
})

// Wrap the Openfort account as a viem LocalAccount
const viemAccount = toAccount({
  address: account.address,
  sign: async ({ hash }) => account.sign({ hash }),
  signMessage: async ({ message }) => account.signMessage({ message }),
  signTransaction: async (tx) => account.signTransaction(tx),
  signTypedData: async (typedData) => account.signTypedData(typedData),
})

const walletClient = createWalletClient({
  account: viemAccount,
  chain: baseSepolia,
  transport: http(),
})

// Send a native transfer
const hash = await walletClient.sendTransaction({
  to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
  value: parseEther('0.001'),
})
```

### Calling a contract

Use `writeContract` for type-safe contract interactions:

```ts
import 'dotenv/config'
import Openfort from '@openfort/openfort-node'
import { type Address, createWalletClient, erc20Abi, http, parseUnits } from 'viem'
import { toAccount } from 'viem/accounts'
import { baseSepolia } from 'viem/chains'

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
})

const account = await openfort.accounts.evm.backend.get({
  id: 'acc_...',
})

const viemAccount = toAccount({
  address: account.address,
  sign: async ({ hash }) => account.sign({ hash }),
  signMessage: async ({ message }) => account.signMessage({ message }),
  signTransaction: async (tx) => account.signTransaction(tx),
  signTypedData: async (typedData) => account.signTypedData(typedData),
})

const walletClient = createWalletClient({
  account: viemAccount,
  chain: baseSepolia,
  transport: http(),
})

// Transfer USDC
const hash = await walletClient.writeContract({
  address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
  abi: erc20Abi,
  functionName: 'transfer',
  args: ['0x...' as Address, parseUnits('10', 6)],
})
```

### Sending a gasless transaction

Use an Openfort backend wallet as the owner of a smart account, then send gasless transactions via the Openfort [bundler](/docs/products/infrastructure/bundler) and [paymaster](/docs/products/infrastructure/paymaster/ethereum).

```ts
import 'dotenv/config'
import Openfort from '@openfort/openfort-node'
import { createPublicClient, http } from 'viem'
import { toAccount } from 'viem/accounts'
import { baseSepolia } from 'viem/chains'
import {
  createBundlerClient,
  createPaymasterClient,
  toCoinbaseSmartAccount,
} from 'viem/account-abstraction'

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
})

const account = await openfort.accounts.evm.backend.get({
  id: 'acc_...',
})

// Wrap the Openfort backend wallet as a viem LocalAccount
const owner = toAccount({
  address: account.address,
  sign: async ({ hash }) => account.sign({ hash }),
  signMessage: async ({ message }) => account.signMessage({ message }),
  signTransaction: async (tx) => account.signTransaction(tx),
  signTypedData: async (typedData) => account.signTypedData(typedData),
})

const client = createPublicClient({
  chain: baseSepolia,
  transport: http(),
})

// Create a smart account with the backend wallet as owner
const smartAccount = await toCoinbaseSmartAccount({
  client,
  owners: [owner],
  version: '1.1',
})

// Create the paymaster client with Openfort
const paymasterClient = createPaymasterClient({
  transport: http('https://api.openfort.io/rpc/84532', {
    fetchOptions: {
      headers: {
        Authorization: `Bearer ${process.env.OPENFORT_PUBLISHABLE_KEY}`,
      },
    },
  }),
})

// Create a bundler client with Openfort bundler + paymaster
const bundlerClient = createBundlerClient({
  account: smartAccount,
  paymaster: paymasterClient,
  client,
  paymasterContext: {
    policyId: '{{POLICY_ID}}',
  },
  transport: http('https://api.openfort.io/rpc/84532', {
    fetchOptions: {
      headers: {
        Authorization: `Bearer ${process.env.OPENFORT_PUBLISHABLE_KEY}`,
      },
    },
  }),
})

// Send a sponsored UserOperation — the user pays no gas
const hash = await bundlerClient.sendUserOperation({
  calls: [
    {
      to: '{{CONTRACT_ADDRESS}}',
      value: 0n,
      data: '{{CALLDATA}}',
    },
  ],
})

const receipt = await bundlerClient.waitForUserOperationReceipt({ hash })
console.log('Transaction hash:', receipt.receipt.transactionHash)
```

:::tip
Replace `{{POLICY_ID}}` with your gas sponsorship ID from the [Openfort dashboard](/docs/configuration/gas-sponsorship). You can also omit `paymasterContext` to use project-scoped policies instead.
:::

## Type exports

The SDK re-exports viem types for convenience:

```ts
import type {
  Address,
  Hash,
  Hex,
  SignableMessage,
  TransactionSerializable,
  TypedData,
  TypedDataDefinition,
} from '@openfort/openfort-node';
```

## Account interface

Each Ethereum backend account exposes the following signing methods:

```ts
interface EvmAccount {
  /** Account unique identifier */
  id: string;
  /** Ethereum address */
  address: Address;
  /** Wallet identifier */
  walletId: string;
  /** Custody type (always 'Developer' for backend wallets) */
  custody: 'Developer';

  /** Sign a raw hash */
  sign(parameters: { hash: Hash }): Promise<Hex>;

  /** Sign a message (EIP-191) */
  signMessage(parameters: { message: SignableMessage }): Promise<Hex>;

  /** Sign a transaction */
  signTransaction(transaction: TransactionSerializable): Promise<Hex>;

  /** Sign typed data (EIP-712) */
  signTypedData<T extends TypedData>(
    parameters: TypedDataDefinition<T>
  ): Promise<Hex>;
}
```
