npm package: @prism-ing/onebalance
Description: OneBalance account abstraction provider for @prism-ing/wallet.
Latest version: 0.1.1

# @prism-ing/onebalance

OneBalance account abstraction provider for [`@prism-ing/wallet`](../wallet). Adds ERC-4337 smart account addresses, cross-chain balances, deposits, swaps, and client-side quote verification.

## Install

```bash
pnpm add @prism-ing/onebalance @prism-ing/wallet
```

**API key:** Get your OneBalance API key from the [OneBalance dashboard](https://onebalance.io). Set it as `ONEBALANCE_API_KEY` in your environment.

## Quickstart

```ts
import { createProductionWallet } from '@prism-ing/wallet';
import { createOneBalanceProvider } from '@prism-ing/onebalance';

const provider = createOneBalanceProvider({
  apiKey: process.env.ONEBALANCE_API_KEY,
});

const result = await createProductionWallet(
  { signer: { walletName: 'my-agent' } },
  { accountAbstraction: provider },
);

if (!result.ok) {
  console.error(result.error.code);
  process.exit(1);
}

const wallet = result.value;
console.log('EOA:', wallet.evmAddress);
console.log('Smart account:', wallet.smartAccountAddress);

// Balance, deposit, and status — all powered by OneBalance
const balance = await wallet.getBalance();
console.log(`Balance: $${balance.totalUsd}`);
```

## How It Works

`createOneBalanceProvider` returns an `AccountAbstractionProvider` — the interface defined by `@prism-ing/wallet`. When injected into `createWallet` or `createProductionWallet`, it enables:

| Capability | Method | What OneBalance Does |
|------------|--------|---------------------|
| Smart account address | `predictAddress()` | Computes counterfactual ERC-4337 address from signer's public key |
| Balance | `getBalance()` | Aggregates balances across all supported chains |
| Deposit | `deposit()` | Executes via OneBalance Resource Locks (quote -> sign -> execute) |
| Transaction status | `getTransactionStatus()` | Polls OneBalance execution status |

**Signing stays client-side.** OneBalance never sees your private keys. It receives only signed payloads (EIP-712 for EVM, transaction signatures for Solana).

## Supported Chains

| Chain | Chain ID | CAIP-2 |
|-------|----------|--------|
| Ethereum | 1 | `eip155:1` |
| Optimism | 10 | `eip155:10` |
| BNB Chain | 56 | `eip155:56` |
| Polygon | 137 | `eip155:137` |
| Base | 8453 | `eip155:8453` |
| Arbitrum One | 42161 | `eip155:42161` |
| Avalanche C-Chain | 43114 | `eip155:43114` |
| Linea | 59144 | `eip155:59144` |
| Blast | 81457 | `eip155:81457` |
| Solana | — | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` |

Use `toCaip2(chainId)` to convert numeric chain IDs, or import `SOLANA_CAIP2` for Solana.

## Asset IDs

OneBalance accepts two asset identifier formats:

| Format | Example | When to Use |
|--------|---------|-------------|
| Aggregated (`ob:`) | `'ob:usdc'`, `'ob:eth'` | Cross-chain operations — OneBalance resolves to the best chain automatically |
| CAIP-19 | `'eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'` | When you need a specific token on a specific chain |

Use `ob:` shorthand for cross-chain swaps. Use CAIP-19 when you need chain-specific precision.

## Cross-Chain Execution

For direct cross-chain swaps outside the wallet abstraction:

```ts
import { executeCrossChain } from '@prism-ing/onebalance';

const result = await executeCrossChain(
  oneBalanceClient,
  wallet.signer,
  {
    fromAsset: 'ob:usdc',
    toAsset: 'ob:eth',
    amount: '1000000',        // smallest unit (e.g., 1 USDC = 1000000)
    slippageTolerance: 50,    // basis points (50 = 0.5%)
    accountAddress: wallet.smartAccountAddress,
  },
  accounts,
);

if (result.ok) {
  console.log('Quote ID:', result.value.quoteId);
}
```

The flow is: get quote -> verify integrity -> sign operations -> submit to OneBalance.

## Quote Verification

`executeCrossChain` automatically verifies OneBalance quotes before signing. This prevents blind-signing attacks where a compromised API could present malicious EIP-712 payloads.

**6 checks performed automatically:**

1. Quote has not expired
2. OneBalance tamper-proof signature is present
3. Origin asset matches the requested `fromAsset`
4. Origin amount is within 1% of the requested amount (configurable)
5. Destination asset matches the requested `toAsset`
6. EVM operation `userOp.sender` matches `accountAddress` (when provided)

For manual verification:

```ts
import { verifyQuoteIntegrity, verifyEvmOperation } from '@prism-ing/onebalance';

const error = verifyQuoteIntegrity(quote, {
  fromAsset: 'ob:usdc',
  toAsset: 'ob:eth',
  amount: '1000000',
  accountAddress: '0x1234...',
  maxOriginSlippageFraction: 0.005,  // 0.5% tolerance (default 1%)
});
```

## API Reference

### `createOneBalanceProvider(config)`

Creates an `AccountAbstractionProvider` backed by OneBalance.

```ts
interface OneBalanceProviderConfig {
  apiKey: string;
  baseUrl?: string;      // Default: https://be.onebalance.io/api
  timeoutMs?: number;    // Default: 30000
  maxRetries?: number;   // Default: 3
  baseDelayMs?: number;  // Default: 1000
}
```

### `createOneBalanceProviderFromClient(client)`

Creates a provider from an existing `OneBalanceClient`. Useful for testing with mock clients.

### `createOneBalanceHttpClient(config)`

Lower-level: creates a raw `OneBalanceClient` with retry logic and exponential backoff. Use this when you need direct API access beyond what the provider exposes.

### `executeCrossChain(client, signer, params, accounts, options?)`

Orchestrates the full quote -> verify -> sign -> execute flow. Returns `Result<CrossChainResult, CrossChainError>`.

### `verifyQuoteIntegrity(quote, params)` / `verifyEvmOperation(operation, address)`

Client-side quote verification functions. Used automatically by `executeCrossChain`; exported for custom flows.

## Error Handling

All async operations return `Result<T, E>` — check `.ok` before using the value:

```ts
import { executeCrossChain } from '@prism-ing/onebalance';

const result = await executeCrossChain(client, wallet.signer, params, accounts);

if (!result.ok) {
  switch (result.error.code) {
    case 'SIGNING_REJECTED':
      // User cancelled biometric or denied signing
      console.error('Signing cancelled:', result.error.reason);
      break;
    case 'PROVIDER_TIMEOUT':
      // OneBalance API didn't respond — safe to retry
      console.error(`Timeout after ${result.error.durationMs}ms`);
      break;
    case 'PROVIDER_API_ERROR':
      // OneBalance returned an error — check statusCode
      console.error(`API ${result.error.statusCode}: ${result.error.body}`);
      break;
    case 'QUOTE_VERIFICATION_FAILED':
      // Quote integrity check failed — do NOT sign
      console.error('Quote tampered or expired');
      break;
  }
  return;
}

// result.value is CrossChainResult
console.log('Submitted:', result.value.quoteId);
```

HTTP-level errors from the client throw `OneBalanceHttpError` with `statusCode`, `body`, and `path` fields.

## Transaction Status Polling

After submitting a swap, poll for execution status:

```ts
const swap = result.value;

const poll = async () => {
  const status = await wallet.getTransactionStatus(swap.quoteId);
  switch (status.status) {
    case 'pending':
      // Still processing — poll again
      break;
    case 'completed':
      console.log('Swap completed');
      break;
    case 'failed':
      console.error('Swap failed');
      break;
  }
};
```

## Testing

Use `createOneBalanceProviderFromClient` to inject a mock client in tests:

```ts
import { createOneBalanceProviderFromClient } from '@prism-ing/onebalance';
import type { OneBalanceClient } from '@prism-ing/onebalance';

const mockClient: OneBalanceClient = {
  predictAddress: async () => ({ accounts: [], smartAccountAddress: '0x...' }),
  getQuote: async () => ({ /* mock quote */ }),
  executeQuote: async () => {},
  getExecutionStatus: async () => ({ quoteId: '...', status: 'completed' }),
  getAggregatedBalance: async () => ({ totalUsd: '100.00', breakdown: [] }),
};

const provider = createOneBalanceProviderFromClient(mockClient);
```

## Network Resilience

The HTTP client includes:
- Exponential backoff with jitter (3 retries, base delay 1s -> 2s -> 4s + random jitter)
- Retries only on: timeouts (AbortError), network errors (TypeError), and 5xx server errors
- No retry on 4xx client errors (bad request, auth failure, validation)
- Configurable via `OneBalanceProviderConfig`

## Documentation

- [SPEC.md](./SPEC.md) — Design decisions, security model, API types

## License

MIT