npm package: @nradko/metricamm-sdk
Description: TypeScript SDK for interacting with MetricAMM contracts using viem
Latest version: 0.0.1

# @nradko/metricamm-sdk

TypeScript SDK for interacting with MetricAMM contracts using [viem](https://viem.sh/).

**THIS IS A NON PRODUCTION READY RELEASE**

**This Readme file was AI generated**

## Features

- ✅ Pool state reading and management
- ✅ **MetricAmmPoolStateView integration for efficient off-chain reads**
- ✅ Direct liquidity position management
- ✅ Swap Router with slippage protection
- ✅ Bin data packing utilities
- ✅ Price conversion helpers (Q64 format)
- ✅ TypeScript types for all contract interfaces
- ✅ Pre-bundled contract ABIs with full TypeScript inference
- ✅ **Liquidity removal by percentage**

## Installation

```bash
npm install @nradko/metricamm-sdk viem
```

or

```bash
yarn add @nradko/metricamm-sdk viem
```

or

```bash
pnpm add @nradko/metricamm-sdk viem
```

## Quick Start

### 1. Import the SDK

```typescript
import * as sdk from "@nradko/metricamm-sdk";
import { createPublicClient, createWalletClient, http } from "viem";
```

### 2. Setup Clients

```typescript
const publicClient = createPublicClient({
  transport: http("https://rpc.example.com"),
});

const walletClient = createWalletClient({
  account: yourAccount,
  transport: http("https://rpc.example.com"),
});
```

### 3. Read Pool State

```typescript
// Option 1: Direct pool reads
const immutables = await sdk.getPoolImmutables(publicClient, poolAddress);
const state = await sdk.getPoolState(publicClient, poolAddress);
const binState = await sdk.getBinState(publicClient, poolAddress, 0);

// Option 2: Using StateView (recommended for efficiency)
const stateViewAddress = "0x..."; // MetricAmmPoolStateView address

// Get all slot0 data in one call
const slot0 = await sdk.getSlot0(publicClient, stateViewAddress, poolAddress);
console.log(`Current Bin: ${slot0.curBinIdx}`);
console.log(`Position: ${slot0.curPosInBin}`);
console.log(`Drift: ${slot0.driftE8 / 1e6}%`);

// Get performance fees (in external token decimals)
const slot1 = await sdk.getSlot1(publicClient, stateViewAddress, poolAddress);
console.log(
  `Collected fees: ${slot1.performanceFeeAmount0}, ${slot1.performanceFeeAmount1}`,
);

// Batch read multiple bins efficiently
const binIndices = [-2, -1, 0, 1, 2];
const binStates = await sdk.getBinStatesExternal(
  publicClient,
  stateViewAddress,
  poolAddress,
  binIndices,
);
console.log(`Read ${binStates.token0Balances.length} bins in one call`);

// Get single bin with balances in external units
const binExternal = await sdk.getBinStateExternal(
  publicClient,
  stateViewAddress,
  poolAddress,
  0,
);
console.log(`Token0: ${binExternal.token0Balance} (native decimals)`);
```

### 4. Execute a Swap

```typescript
// Setup swap parameters
const swapParams: sdk.SwapExactInputParams = {
  pool: poolAddress,
  recipient: userAddress,
  zeroForOne: true, // token0 -> token1
  amountIn: parseEther("1"),
  priceLimitX64: sdk.MAX_UINT128, // No limit
  minAmountOut: parseEther("0.99"), // 1% slippage tolerance
  deadline: BigInt(Date.now() / 1000 + 300), // 5 minutes
};

// Execute swap
const hash = await sdk.swapExactInput(
  publicClient,
  walletClient,
  routerAddress,
  swapParams,
  userAddress,
);

// Wait for confirmation
await publicClient.waitForTransactionReceipt({ hash });
```

### 5. Add Liquidity

```typescript
// Define liquidity deltas (positive to add, negative to remove)
const deltas: sdk.LiquidityDelta[] = [
  { bin: -1, deltaShares: 1000n },
  { bin: 0, deltaShares: 2000n },
  { bin: 1, deltaShares: 1000n },
];

// Add liquidity to a position
// Note: You need to approve token transfers before calling this
const { hash, amount0Delta, amount1Delta } = await sdk.modifyLiquidity(
  publicClient,
  walletClient,
  poolAddress,
  0n, // salt - unique identifier for your position
  deltas,
  userAddress,
  sdk.NO_SLIPPAGE_LIMIT, // or specify max token0 to spend
  sdk.NO_SLIPPAGE_LIMIT, // or specify max token1 to spend
);

console.log(`Transaction: ${hash}`);
console.log(`Token0 spent: ${amount0Delta}`);
console.log(`Token1 spent: ${amount1Delta}`);
```

### 6. Query Position Shares

```typescript
// Get all shares for a position across bins
const positionShares = await sdk.getPositionShares(
  publicClient,
  stateViewAddress,
  poolAddress,
  ownerAddress, // Position owner
  0n, // salt
  -10, // lowerBin
  10, // upperBin
);

for (const { binIdx, shares } of positionShares) {
  console.log(`Bin ${binIdx}: ${shares} shares`);
}
```

### 7. Remove Liquidity by Percentage

```typescript
// Remove 50% of liquidity from a position
const { hash } = await sdk.removeLiquidity(
  publicClient,
  walletClient,
  stateViewAddress,
  poolAddress,
  ownerAddress,
  0n, // salt
  userAddress, // account executing tx
  { percentageToRemove: 50 },
);

// Remove 100% (all liquidity) from specific bin range
const { hash: hash2 } = await sdk.removeLiquidity(
  publicClient,
  walletClient,
  stateViewAddress,
  poolAddress,
  ownerAddress,
  0n, // salt
  userAddress,
  {
    percentageToRemove: 100,
    lowerBin: -5,
    upperBin: 5,
  },
);

// Or prepare the deltas manually for more control
const deltas = await sdk.prepareRemoveLiquidityDeltas(
  publicClient,
  stateViewAddress,
  poolAddress,
  ownerAddress,
  0n, // salt
  { percentageToRemove: 25 },
);

// Then execute with modifyLiquidity
const { hash: hash3 } = await sdk.modifyLiquidity(
  publicClient,
  walletClient,
  poolAddress,
  0n, // salt
  deltas,
  userAddress,
);
```

## Utility Functions

### Bin Data Packing

```typescript
// Create uniform bins for pool deployment
const bins = sdk.createUniformBins(
  520, // Total bins
  10_000, // Length: 1% per bin (10000 / 1e6)
  0, // No additional buy fee
  0, // No additional sell fee
);

// Pack into uint256 array (5 bins per uint256)
const packed = sdk.packBinDataArray(bins);

// Unpack a single bin
const binData = sdk.unpackBinData(packed[0]);
console.log(`Length: ${binData.lengthE6}`);
console.log(`Buy Fee: ${binData.addFeeBuyE6}`);
console.log(`Sell Fee: ${binData.addFeeSellE6}`);
```

### Price Conversion

```typescript
// Convert regular price to Q64 format
const priceQ64 = sdk.priceToQ64(1.5); // 1.5 as Q64

// Convert Q64 back to regular price
const price = sdk.q64ToPrice(priceQ64); // 1.5
```

### Position Queries

```typescript
// Get user's shares in a specific bin
const shares = await sdk.getPositionBinShares(
  publicClient,
  poolAddress,
  userAddress,
  salt, // Position salt (uint72)
  binIndex, // Bin index (int24)
);

console.log(`User has ${shares} shares in bin ${binIndex}`);
```

## Complete Examples

See [scripts/examples/sdk-usage.ts](../scripts/examples/sdk-usage.ts) for comprehensive examples covering:

- Reading pool state
- Adding/removing liquidity
- Executing swaps (all types)
- Creating bin data for deployment
- Querying positions

## Type Reference

### LiquidityDelta

```typescript
interface LiquidityDelta {
  bin: number; // int24: Bin index
  deltaShares: bigint; // int104: Positive = add, negative = remove
}
```

### RemoveLiquidityParams

```typescript
interface RemoveLiquidityParams {
  percentageToRemove: number; // 0-100: Percentage of position to remove
  lowerBin?: number; // Lower bin index (default: -10)
  upperBin?: number; // Upper bin index (default: 10)
}
```

### BinData

```typescript
interface BinData {
  lengthE6: number; // uint16: Bin width (1e6 = 100%)
  addFeeBuyE6: number; // uint16: Additional buy fee
  addFeeSellE6: number; // uint16: Additional sell fee
}
```

### BinState

```typescript
interface BinState {
  token0BalanceScaled: bigint; // uint104: Scaled token0 balance
  token1BalanceScaled: bigint; // uint104: Scaled token1 balance
  lengthE6: number; // uint16: Bin width
  addFeeBuyE6: number; // uint16: Buy fee adjustment
  addFeeSellE6: number; // uint16: Sell fee adjustment
}

// External units (from StateView)
interface BinStateExternal {
  token0Balance: bigint; // Unscaled, in native token decimals
  token1Balance: bigint; // Unscaled, in native token decimals
  lengthE6: number;
  addFeeBuyE6: number;
  addFeeSellE6: number;
}
```

### Slot0 and Slot1

```typescript
// All slot0 values in one call
interface Slot0 {
  curBinIdx: number; // Current bin index
  curPosInBin: bigint; // Position within bin (0 to MAX_UINT104)
  curBinDistFromProvidedPriceE6: number; // Distance in units
  driftE8: number; // Current drift (1e8 = 100%)
  performanceFeeE6: number; // Performance fee (1e6 = 100%)
}

// Performance fee amounts
interface Slot1 {
  performanceFeeAmount0: bigint; // Collected token0 fees (external units)
  performanceFeeAmount1: bigint; // Collected token1 fees (external units)
}
```

### FeeConfig

```typescript
interface FeeConfig {
  adminAddr: Address; // Admin address
  protocolFee: number; // Protocol fee (1e6 = 100%)
  adminFee: number; // Admin fee (1e6 = 100%)
  adminFeeDest: Address; // Admin fee destination address
}
```

### SwapExactInputParams

```typescript
interface SwapExactInputParams {
  pool: Address;
  recipient: Address;
  zeroForOne: boolean; // true = token0->token1, false = token1->token0
  amountIn: bigint; // Input amount
  priceLimitX64: bigint; // Price limit (Q64 format)
  minAmountOut: bigint; // Minimum output (slippage protection)
  deadline: bigint; // Unix timestamp
}
```

### SwapExactOutputParams

```typescript
interface SwapExactOutputParams {
  pool: Address;
  recipient: Address;
  zeroForOne: boolean;
  amountOut: bigint; // Desired output amount
  priceLimitX64: bigint;
  maxAmountIn: bigint; // Maximum input willing to pay
  deadline: bigint;
}
```

## Constants

```typescript
sdk.Q64; // 2^64 - Q64 format multiplier
sdk.MAX_UINT128; // 2^128-1 - Max uint128 (no price limit)
sdk.MAX_UINT104; // 2^104-1 - Max bin position
sdk.MAX_INT104; // 2^103-1 - Max signed int104 (for deltaShares)
sdk.MAX_INT128; // 2^127-1 - Max signed int128
sdk.NO_SLIPPAGE_LIMIT; // Max int128 - Use for specAmount to disable slippage checks
```

## Usage with Scripts

The SDK is designed to work seamlessly with Hardhat scripts:

```typescript
// In a Hardhat script
import { network } from "hardhat";
import * as sdk from "../sdk/index.js";

const { viem } = await network.connect();
const publicClient = await viem.getPublicClient();
const [walletClient] = await viem.getWalletClients();

// Use SDK functions
const state = await sdk.getPoolState(publicClient, poolAddress);
```

See updated scripts in `scripts/actions/`:

- `swap-with-sdk.ts` - Swap using SDK
- `add-liquidity-with-sdk.ts` - Liquidity management using SDK

## Advanced Usage

### Using MetricAmmPoolStateView for Efficient Reads

The StateView contract provides optimized read functions that are especially useful for off-chain operations:

```typescript
const stateViewAddress = "0x..."; // Deploy MetricAmmPoolStateView once

// Get complete state efficiently
const slot0 = await sdk.getSlot0(publicClient, stateViewAddress, poolAddress);
const slot1 = await sdk.getSlot1(publicClient, stateViewAddress, poolAddress);

// Read fee configuration
const feeConfig = await sdk.getFeeConfig(
  publicClient,
  stateViewAddress,
  poolAddress,
);
console.log(`Admin: ${feeConfig.adminAddr}`);
console.log(`Protocol fee: ${feeConfig.protocolFee / 10000}%`);
console.log(`Admin fee: ${feeConfig.adminFee / 10000}%`);
console.log(`Admin fee dest: ${feeConfig.adminFeeDest}`);

// Get price provider
const priceProvider = await sdk.getPriceProvider(
  publicClient,
  stateViewAddress,
  poolAddress,
);

// Get last trade timestamp
const lastTrade = await sdk.getLastTradeTimestamp(
  publicClient,
  stateViewAddress,
  poolAddress,
);

// Get bin total shares
const totalShares = await sdk.getBinTotalShares(
  publicClient,
  stateViewAddress,
  poolAddress,
  0, // bin index
);

// Query user position via StateView
const userShares = await sdk.getPositionBinSharesFromStateView(
  publicClient,
  stateViewAddress,
  poolAddress,
  userAddress,
  salt,
  binIndex,
);

// Batch read bins for liquidity distribution
const binIndices = Array.from({ length: 21 }, (_, i) => i - 10); // -10 to 10
const distribution = await sdk.getBinStatesExternal(
  publicClient,
  stateViewAddress,
  poolAddress,
  binIndices,
);

// Analyze liquidity distribution
for (let i = 0; i < binIndices.length; i++) {
  console.log(`Bin ${binIndices[i]}: ${distribution.totalShares[i]} shares`);
}
```

**Benefits of StateView:**

- ✅ Returns balances in native token decimals (external units)
- ✅ Batch reads multiple bins in single call
- ✅ No gas cost when called off-chain
- ✅ Efficient EXTSLOAD implementation
- ✅ Convenient single-call functions (slot0, slot1, feeConfig)

### Custom Bin Configurations

```typescript
// Create custom bin configurations
const customBins: sdk.BinData[] = [
  { lengthE6: 5000, addFeeBuyE6: 0, addFeeSellE6: 0 }, // 0.5% wide, no fees
  { lengthE6: 10000, addFeeBuyE6: 1000, addFeeSellE6: 0 }, // 1% wide, 0.1% extra buy fee
  { lengthE6: 15000, addFeeBuyE6: 0, addFeeSellE6: 500 }, // 1.5% wide, 0.05% extra sell fee
];

const packed = sdk.packBinDataArray(customBins);
```

### Multi-Bin Liquidity

```typescript
// Add liquidity across multiple bins with custom amounts
const deltas: sdk.LiquidityDelta[] = [];

for (let i = -5; i <= 5; i++) {
  // More liquidity near the center
  const shares = 1000n - BigInt(Math.abs(i)) * 100n;
  deltas.push({ bin: i, deltaShares: shares > 100n ? shares : 100n });
}

// Approve tokens first (example)
// await token0.write.approve([poolAddress, maxAmount0]);
// await token1.write.approve([poolAddress, maxAmount1]);

const { hash, amount0Delta, amount1Delta } = await sdk.modifyLiquidity(
  publicClient,
  walletClient,
  poolAddress,
  0n, // salt - unique identifier for this position
  deltas,
  userAddress,
  sdk.NO_SLIPPAGE_LIMIT, // or specify max token0 to spend
  sdk.NO_SLIPPAGE_LIMIT, // or specify max token1 to spend
);

console.log(
  `Added liquidity - Token0: ${amount0Delta}, Token1: ${amount1Delta}`,
);
```

### Price Impact Estimation

```typescript
// Estimate output for a given input (simplified version)
async function estimateSwapOutput(
  publicClient: PublicClient,
  poolAddress: Address,
  amountIn: bigint,
  zeroForOne: boolean,
): Promise<bigint> {
  const state = await sdk.getPoolState(publicClient, poolAddress);
  const binState = await sdk.getBinState(
    publicClient,
    poolAddress,
    state.curBinIdx,
  );

  // Simplified: assumes swap stays in one bin
  // Production should iterate through bins
  const availableOutput = zeroForOne
    ? binState.token1BalanceScaled
    : binState.token0BalanceScaled;

  // Apply fee (simplified)
  const outputBeforeFee = amountIn; // Assumes 1:1 price
  const fee = (outputBeforeFee * 3000n) / 1_000_000n; // 0.3%
  const output = outputBeforeFee - fee;

  return output < availableOutput ? output : availableOutput;
}
```

## Error Handling

The SDK functions will throw errors that can be caught and handled:

```typescript
try {
  const hash = await sdk.swapExactInput(
    walletClient,
    routerAddress,
    swapParams,
    userAddress,
  );
  console.log("Swap successful:", hash);
} catch (error: any) {
  if (error.message.includes("InsufficientOutput")) {
    console.error("Slippage too high, try increasing tolerance");
  } else if (error.message.includes("TransactionExpired")) {
    console.error("Transaction deadline passed");
  } else {
    console.error("Swap failed:", error.message);
  }
}
```

## Best Practices

1. **Always Set Deadlines**: Prevent transactions from being executed far in the future

   ```typescript
   const deadline = BigInt(Math.floor(Date.now() / 1000) + 300); // 5 minutes
   ```

2. **Use Appropriate Slippage**: Balance protection with execution certainty

   ```typescript
   const slippage = 0.5; // 0.5% for stable pairs
   const slippage = 1.0; // 1% for volatile pairs
   ```

3. **Approve Tokens**: Ensure sufficient allowances before operations

   ```typescript
   // Approve max for convenience (user should be aware)
   await token.write.approve([spender, sdk.MAX_UINT128]);
   ```

4. **Check Balances**: Verify user has sufficient tokens

   ```typescript
   const balance = await token.read.balanceOf([userAddress]);
   if (balance < requiredAmount) {
     throw new Error("Insufficient balance");
   }
   ```

5. **Wait for Confirmations**: Always wait for transaction receipts
   ```typescript
   const receipt = await publicClient.waitForTransactionReceipt({ hash });
   if (receipt.status !== "success") {
     throw new Error("Transaction failed");
   }
   ```

## Contributing

When adding new SDK functions:

1. Add TypeScript types
2. Add JSDoc comments
3. Handle errors appropriately
4. Add example usage to this README
5. Update the specification document

## Resources

- [Viem Documentation](https://viem.sh/)
- [MetricAMM Contracts Specification](../CONTRACTS_SPECIFICATION.md)
- [Example Scripts](../scripts/examples/)

## License

See main project license.