@nradko/metricamm-sdk
v0.0.1
Published
TypeScript SDK for interacting with MetricAMM contracts using viem
Maintainers
Readme
@nradko/metricamm-sdk
TypeScript SDK for interacting with MetricAMM contracts using viem.
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
npm install @nradko/metricamm-sdk viemor
yarn add @nradko/metricamm-sdk viemor
pnpm add @nradko/metricamm-sdk viemQuick Start
1. Import the SDK
import * as sdk from "@nradko/metricamm-sdk";
import { createPublicClient, createWalletClient, http } from "viem";2. Setup Clients
const publicClient = createPublicClient({
transport: http("https://rpc.example.com"),
});
const walletClient = createWalletClient({
account: yourAccount,
transport: http("https://rpc.example.com"),
});3. Read Pool State
// 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
// 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
// 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
// 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
// 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
// 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
// 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.5Position Queries
// 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 for comprehensive examples covering:
- Reading pool state
- Adding/removing liquidity
- Executing swaps (all types)
- Creating bin data for deployment
- Querying positions
Type Reference
LiquidityDelta
interface LiquidityDelta {
bin: number; // int24: Bin index
deltaShares: bigint; // int104: Positive = add, negative = remove
}RemoveLiquidityParams
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
interface BinData {
lengthE6: number; // uint16: Bin width (1e6 = 100%)
addFeeBuyE6: number; // uint16: Additional buy fee
addFeeSellE6: number; // uint16: Additional sell fee
}BinState
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
// 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
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
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
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
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 checksUsage with Scripts
The SDK is designed to work seamlessly with Hardhat scripts:
// 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 SDKadd-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:
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
// 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
// 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
// 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:
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
Always Set Deadlines: Prevent transactions from being executed far in the future
const deadline = BigInt(Math.floor(Date.now() / 1000) + 300); // 5 minutesUse Appropriate Slippage: Balance protection with execution certainty
const slippage = 0.5; // 0.5% for stable pairs const slippage = 1.0; // 1% for volatile pairsApprove Tokens: Ensure sufficient allowances before operations
// Approve max for convenience (user should be aware) await token.write.approve([spender, sdk.MAX_UINT128]);Check Balances: Verify user has sufficient tokens
const balance = await token.read.balanceOf([userAddress]); if (balance < requiredAmount) { throw new Error("Insufficient balance"); }Wait for Confirmations: Always wait for transaction receipts
const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") { throw new Error("Transaction failed"); }
Contributing
When adding new SDK functions:
- Add TypeScript types
- Add JSDoc comments
- Handle errors appropriately
- Add example usage to this README
- Update the specification document
Resources
License
See main project license.
