TypeScript SDK for the CoralSwap Protocol -- a V2 AMM on Stellar/Soroban with dynamic fees and flash loans.
Upgrading from v1? See the Migration Guide for breaking changes, before/after examples, and step-by-step instructions.
Contract-first, API-optional. This SDK interacts directly with CoralSwap's Soroban smart contracts through Soroban RPC. No centralized API gateway, no API keys, no single points of failure.
Application
|
@coralswap/sdk
|
Soroban RPC (direct)
|
CoralSwap Contracts (on-chain)
npm install @coralswap/sdknpm install @coralswap/sdkimport { CoralSwapClient, Network } from "@coralswap/sdk";
// Initialize the client
const client = new CoralSwapClient({
network: Network.TESTNET,
rpcUrl: "https://soroban-testnet.stellar.org",
secretKey: "S...", // Your secret key
});
// Check health
const healthy = await client.isHealthy();
console.log("RPC healthy:", healthy);The network enum selects a built-in preset that supplies the RPC URL, Stellar network passphrase, and deployed contract addresses.
| Preset | Enum value | Passphrase | Contract addresses | RPC URL |
|---|---|---|---|---|
| Testnet | Network.TESTNET |
Test SDF Network ; September 2015 |
✅ Populated | soroban-testnet.stellar.org |
| Mainnet | Network.MAINNET |
Public Global Stellar Network ; September 2015 |
soroban.stellar.org |
|
| Staging | Network.STAGING |
Test SDF Network ; September 2015 |
✅ Same as testnet | soroban-testnet.stellar.org |
STAGING and TESTNET share the same passphrase and contract addresses. Transactions signed for one network are valid on the other. Use STAGING only when the backend or infrastructure team explicitly designates it; otherwise prefer TESTNET for clarity.
The Stellar network passphrase is baked into every transaction envelope. A signer must receive the correct passphrase so that the signed XDR is valid on the target network.
import { CoralSwapClient, Network } from "@coralswap/sdk";
// KeypairSigner (built-in) — passphrase is set automatically from the preset
const client = new CoralSwapClient({
network: Network.TESTNET,
secretKey: "S...",
});
// Custom Signer (e.g. Freighter) — the SDK passes the preset passphrase to
// TransactionBuilder automatically; the wallet handles it.
const clientWithWallet = new CoralSwapClient({
network: Network.TESTNET,
signer: freighterSigner, // implements the Signer interface
});If you call setNetwork() at runtime, the passphrase used for signing updates automatically:
client.setNetwork(Network.MAINNET); // passphrase now = "Public Global Stellar Network ; September 2015"Override the built-in RPC endpoint with a single URL or a list of fallback URLs:
const client = new CoralSwapClient({
network: Network.TESTNET,
rpcUrl: ["https://my-rpc.example.com", "https://soroban-testnet.stellar.org"],
});- Contract addresses are empty. The
factoryAddressandrouterAddressforNetwork.MAINNETare placeholders until CoralSwap contracts are deployed to public Stellar. Operations that depend on them (client.factory,client.router) will throw. - Use real funds carefully. Mainnet transactions are irreversible. Always test on testnet first.
- RPC rate limits. The public
soroban.stellar.orgendpoint enforces rate limits. Consider supplying a dedicated RPC URL viarpcUrland/or aRateLimiterfor production traffic.
import { SwapModule, TradeType, toSorobanAmount } from "@coralswap/sdk";
const swap = new SwapModule(client);
// Get a quote for swapping 0.1 TokenA for TokenB
const quote = await swap.getQuote({
tokenIn: "CDLZ...", // TokenA contract address
tokenOut: "CBQH...", // TokenB contract address
amount: toSorobanAmount("0.1", 7), // 0.1 tokens with 7 decimals
tradeType: TradeType.EXACT_IN,
slippageBps: 50, // 0.5% slippage tolerance
});
console.log("Expected output:", quote.amountOut);
console.log("Dynamic fee:", quote.feeBps, "bps");
console.log("Price impact:", quote.priceImpactBps, "bps");
// Execute the swap
const result = await swap.execute({
tokenIn: "CDLZ...",
tokenOut: "CBQH...",
amount: toSorobanAmount("0.1", 7),
tradeType: TradeType.EXACT_IN,
slippageBps: 50,
deadline: Math.floor(Date.now() / 1000) + 60, // 1 minute deadline
});
console.log("Transaction hash:", result.hash);import { LiquidityModule, toSorobanAmount } from "@coralswap/sdk";
const liquidity = new LiquidityModule(client);
// Get a quote for adding liquidity
const quote = await liquidity.getAddLiquidityQuote(
"CDLZ...", // TokenA contract address
"CBQH...", // TokenB contract address
toSorobanAmount("100", 7), // 100 TokenA
toSorobanAmount("200", 7), // 200 TokenB
);
console.log("Amount A needed:", quote.amountA);
console.log("Amount B needed:", quote.amountB);
console.log("LP tokens to receive:", quote.liquidity);
// Add liquidity to the pool
const result = await liquidity.addLiquidity({
tokenA: "CDLZ...",
tokenB: "CBQH...",
amountADesired: quote.amountA,
amountBDesired: quote.amountB,
amountAMin: (quote.amountA * 99n) / 100n, // 1% slippage
amountBMin: (quote.amountB * 99n) / 100n, // 1% slippage
to: client.publicKey,
deadline: Math.floor(Date.now() / 1000) + 300, // 5 minutes deadline
});
console.log("Transaction hash:", result.hash);// Get a quote for removing liquidity
const quote = await liquidity.getRemoveLiquidityQuote(
"CDLZ...", // TokenA contract address
"CBQH...", // TokenB contract address
toSorobanAmount("50", 7), // 50 LP tokens
);
console.log("Amount A to receive:", quote.amountA);
console.log("Amount B to receive:", quote.amountB);
// Remove liquidity from the pool
const result = await liquidity.removeLiquidity({
tokenA: "CDLZ...",
tokenB: "CBQH...",
liquidity: toSorobanAmount("50", 7), // 50 LP tokens
amountAMin: (quote.amountA * 99n) / 100n, // 1% slippage
amountBMin: (quote.amountB * 99n) / 100n, // 1% slippage
to: client.publicKey,
deadline: Math.floor(Date.now() / 1000) + 300, // 5 minutes deadline
});
console.log("Transaction hash:", result.hash);import { SwapModule, TradeType } from "@coralswap/sdk";
const swap = new SwapModule(client);
// Get a quote
const quote = await swap.getQuote({
tokenIn: "CDLZ...",
tokenOut: "CBQH...",
amount: 1000000n, // 0.1 tokens (7 decimals)
tradeType: TradeType.EXACT_IN,
slippageBps: 50, // 0.5%
});
console.log("Expected output:", quote.amountOut);
console.log("Dynamic fee:", quote.feeBps, "bps");
console.log("Price impact:", quote.priceImpactBps, "bps");
// Execute the swap
const result = await swap.execute({
tokenIn: "CDLZ...",
tokenOut: "CBQH...",
amount: 1000000n,
tradeType: TradeType.EXACT_IN,
});import { LiquidityModule, toSorobanAmount } from "@coralswap/sdk";
const liquidity = new LiquidityModule(client);
// Get add-liquidity quote
const quote = await liquidity.getAddLiquidityQuote(
"CDLZ...",
"CBQH...",
toSorobanAmount("100", 7),
);
// Add liquidity
const result = await liquidity.addLiquidity({
tokenA: "CDLZ...",
tokenB: "CBQH...",
amountADesired: quote.amountA,
amountBDesired: quote.amountB,
amountAMin: (quote.amountA * 99n) / 100n,
amountBMin: (quote.amountB * 99n) / 100n,
to: client.publicKey,
});import { FlashLoanModule } from "@coralswap/sdk";
const flash = new FlashLoanModule(client);
// Estimate fee
const fee = await flash.estimateFee(pairAddress, tokenAddress, 1000000000n);
console.log("Flash loan fee:", fee.feeAmount, "(", fee.feeBps, "bps)");
// Execute flash loan
const result = await flash.execute({
pairAddress: "CDLZ...",
token: "CBQH...",
amount: 1000000000n,
receiverAddress: "CXYZ...", // Your flash receiver contract
callbackData: Buffer.from("{}"),
});import { FeeModule } from "@coralswap/sdk";
const fees = new FeeModule(client);
// Get current fee for a pair
const estimate = await fees.getCurrentFee(pairAddress);
console.log("Current fee:", estimate.currentFeeBps, "bps");
console.log("Stale?", estimate.isStale);
// Compare fees across pairs
const comparison = await fees.compareFees([pair1, pair2, pair3]);import { OracleModule } from "@coralswap/sdk";
const oracle = new OracleModule(client);
// Record observations over time
await oracle.observe(pairAddress);
// ... wait some time ...
await oracle.observe(pairAddress);
// Get TWAP
const twap = await oracle.getTWAP(pairAddress);
if (twap) {
console.log("TWAP price0:", twap.price0TWAP);
console.log("Time window:", twap.timeWindow, "seconds");
}The SDK supports the native Stellar asset (XLM) via the Stellar Asset Contract (SAC). You can pass "XLM" or "native" as a token identifier in swap and multi-hop methods; it is resolved to the network’s XLM SAC address automatically.
import { getNativeAssetContractAddress, resolveTokenIdentifier, isNativeToken } from "@coralswap/sdk";
// Resolve "XLM" to the SAC contract address for the current network
const passphrase = client.networkConfig.networkPassphrase;
const xlmAddress = getNativeAssetContractAddress(passphrase);
// Or resolve any identifier (e.g. "XLM" or a contract address)
const resolved = resolveTokenIdentifier("XLM", passphrase);
// Check if an identifier is native XLM
if (isNativeToken("XLM")) {
// use resolved address for contract calls
}Swap and multi-hop methods accept "XLM" as tokenIn/tokenOut or as an element in path; no need to look up the SAC address when using the high-level API.
import {
toSorobanAmount,
fromSorobanAmount,
formatAmount,
sortTokens,
isValidAddress,
withRetry,
DeadlineError,
} from "@coralswap/sdk";
// Amount conversions
const amount = toSorobanAmount("1.5", 7); // 15000000n
const display = fromSorobanAmount(15000000n, 7); // "1.5000000"
const formatted = formatAmount(15000000n, 7, 2); // "1.50"
// Address utilities
const [token0, token1] = sortTokens(tokenA, tokenB);
const valid = isValidAddress("GABC...");
// Retry with backoff
const result = await withRetry(() => client.factory.getAllPairs(), {
maxRetries: 5,
baseDelayMs: 500,
});
// Bound the total time spent on a single RPC call (including retries).
// Once the deadline elapses, retries stop and a DeadlineError is thrown —
// useful for hard latency caps on user-facing requests.
const bounded = await withRetry(() => client.factory.getAllPairs(), {
maxRetries: 5,
baseDelayMs: 500,
});
// Or set it once on the client so every RPC call shares the bound.
const client = new CoralSwapClient({
network: Network.TESTNET,
secretKey: "S...",
deadlineMs: 5000, // give up after 5 seconds total, DeadlineError is thrown
});
try {
const healthy = await client.isHealthy();
} catch (err) {
if (err instanceof DeadlineError) {
console.log("RPC retries exceeded the 5s deadline");
}
}import {
CoralSwapSDKError,
SlippageError,
DeadlineError,
InsufficientLiquidityError,
mapError,
} from "@coralswap/sdk";
try {
await swap.execute(request);
} catch (err) {
const sdkError = mapError(err);
switch (sdkError.code) {
case "SLIPPAGE_EXCEEDED":
console.log("Increase slippage tolerance");
break;
case "DEADLINE_EXCEEDED":
console.log("Transaction expired, retry");
break;
case "INSUFFICIENT_LIQUIDITY":
console.log("Not enough liquidity");
break;
default:
console.error("Unexpected:", sdkError.message);
}
}High-throughput integrations (trading bots, aggregators, dashboards) should tune caching, RPC failover, and connection pooling. See docs/PERFORMANCE.md for use-case profiles, TTL guidance, benchmark numbers, and copy-paste configuration examples.
| Principle | Implementation |
|---|---|
| Contract-first | Direct Soroban RPC, no API gateway |
| Type-safe | Full TypeScript with BigInt for i128 |
| Trustless | No API keys, no centralized dependencies |
| Modular | Import only what you need |
| Testable | Pure math functions, mockable contract clients |
| Resilient | Built-in retry with exponential backoff |
The integration suite runs the full add-liquidity → swap → remove-liquidity lifecycle against real testnet contracts.
- Fund a testnet account at https://friendbot.stellar.org.
- Deploy (or note the addresses of) two SEP-41 tokens on testnet.
- Export the required environment variables:
export STELLAR_TESTNET=true
export TEST_KEYPAIR=S... # funded testnet secret key
export TEST_TOKEN_A=C... # contract address of token A
export TEST_TOKEN_B=C... # contract address of token B
# optional — defaults to https://soroban-testnet.stellar.org
export TEST_RPC_URL=https://...- Run:
npm run test:integrationThe tests are idempotent — if the pair already exists it is reused, so you can run the suite multiple times without conflicts.
Integration tests run automatically on pull requests to main via .github/workflows/integration.yml. The job is marked continue-on-error for fork PRs (which cannot access repository secrets), so they will not block merges from external contributors.
Add the following secrets to your repository (Settings → Secrets → Actions):
| Secret | Description |
|---|---|
TEST_KEYPAIR |
Funded testnet secret key |
TEST_TOKEN_A |
Testnet token A contract address |
TEST_TOKEN_B |
Testnet token B contract address |
| TEST_RWA_POOL | Deployed RWA pool contract address on testnet |
| TEST_NAV_FEED_ID | RedStone NAV feed id for the pool's underlying asset |
MIT