npm package: @oneswap/sdk
Description: Official TypeScript SDK for the OneSwap API
Author: OneSwap
Latest version: 0.2.6

# @oneswap/sdk

Official TypeScript SDK for the OneSwap API. Typed methods for pools, tokens, quotes, wallet auth, swaps, and wallet-scoped tracking — with built-in awaitable swap lifecycle. Works in Node.js and browsers.

## Install

```bash
npm install @oneswap/sdk
```

Maintainers: see `PUBLISHING.md` for the release workflow and the local ignored npm auth-file setup used in this repo.

## Quick Start

```ts
import { OneSwap } from '@oneswap/sdk'

const apiKey = 'os_live_...'
const authClient = new OneSwap({ apiKey })
const partyId = 'alice::12205a8c...'

const challenge = await authClient.walletAuth.requestChallenge(partyId)
const signature = await wallet.signMessage(challenge.message)

const verified = await authClient.walletAuth.verifyChallenge({
  partyId,
  nonce: challenge.nonce,
  signature,
  publicKey: wallet.publicKey,
})

const client = new OneSwap({
  apiKey,
  walletToken: verified.token,
})

// Get a traffic-aware quote for the same party that will deposit
const quote = await client.quotes.get({
  from: 'Amulet',
  to: 'USDCx',
  amount: '100',
  receiverParty: partyId,
})
console.log(`Output: ${quote.outputAmount} ${quote.outputToken}`)
console.log(`Traffic: ${quote.trafficFeeInInput ?? '0'} ${quote.inputToken}`)

// Create a swap and wait for completion
const intent = await client.swaps.create({
  fromToken: 'Amulet',
  toToken: 'USDCx',
  amount: '100',
  walletAddress: partyId,
})

console.log(`Deposit to: ${intent.depositAddress}`)
console.log(`Transfer reference: ${intent.depositReference}`)

// Listen for status changes
intent.on('processing', () => console.log('Deposit detected'))
intent.on('completed', (status) => console.log(`Swap done: ${status.actualOutput}`))

// Await final result
const result = await intent.wait()
console.log(`Received: ${result.actualOutput} ${result.outputToken}`)
```

Use `os_live_...` SDK keys for integrations. Pair the key with a verified wallet token from `client.walletAuth.*` for user-bound actions. SDK keys are created and managed through the wallet-authenticated OneSwap developer portal.

`walletAddress` is the user's actual Canton party ID. Deposits go directly to the pool party, and swap output returns directly to that same source party in the current execution flow. If your wallet flow supports Canton reason/reference metadata, pass through `intent.depositReference` for faster fallback matching. The SDK does not expose wallet registration, wallet balance lookup, LP add, LP withdraw, or other liquidity management.

If token names are unique across pools, name-only selection is still fine. If the same symbol exists under multiple admins, pass `poolId` or both token admins so the API cannot guess the wrong pool.

## Environment Targeting

Mainnet remains the default:

```ts
const client = new OneSwap({ apiKey })
```

Devnet now has a built-in default backend too:

```ts
const client = new OneSwap({
  apiKey,
  environment: 'devnet',
})
```

That currently resolves to `https://devnet.api.oneswap.cc`.

For devnet integrations, create and manage SDK keys from the devnet developer portal at:

- `https://devnet.oneswap.cc/developer/keys`

You can still override it with an explicit backend URL:

```ts
const client = new OneSwap({
  apiKey,
  environment: 'devnet',
  baseUrl: 'https://devnet-backend.example.com',
})
```

If you omit `baseUrl`, the SDK defaults to:

- Mainnet: `https://api.oneswap.cc`
- Devnet: `https://devnet.api.oneswap.cc`

## Getting An SDK API Key

Developer access is wallet-authenticated.
SDK developer auth and SDK swap execution do not require a site-access token or access code.

1. Authenticate a Canton wallet with OneSwap.
2. Exchange that wallet session on `POST /api/sdk/wallet-login`.
3. Use the returned user JWT on `/api/sdk/keys` to create or rotate `os_live_...` keys.
4. Use the resulting API key in `new OneSwap({ apiKey })`.

### Devnet SDK Keys

Use the devnet site when you want a devnet SDK key:

1. Open `https://devnet.oneswap.cc/developer/keys`.
2. Connect and authenticate the devnet wallet you want to use.
3. Create or rotate the SDK key from the devnet developer portal.
4. Use that key with:

```ts
const client = new OneSwap({
  apiKey: 'os_live_...',
  environment: 'devnet',
})
```

That targets `https://devnet.api.oneswap.cc` by default.

`POST /api/sdk/register` and `POST /api/sdk/login` are retired compatibility endpoints and now return `410 Gone`.

## API Reference

### `new OneSwap(config)`

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiKey` | `string` | *required* | Your OneSwap SDK API key (`os_live_...`) |
| `environment` | `'mainnet' \| 'devnet'` | `'mainnet'` | Logical target environment. |
| `baseUrl` | `string` | Mainnet: `https://api.oneswap.cc`, Devnet: `https://devnet.api.oneswap.cc` | Override the API origin. Use this for alternate Devnet or self-hosted backends. |
| `walletToken` | `string \| (() => string \| Promise<string \| undefined> \| undefined)` | none | Optional end-user wallet token, or a lazy getter, for wallet-authenticated requests |
| `timeout` | `number` | `30000` | Request timeout (ms) |

### Pools

| Method | Returns | Description |
|--------|---------|-------------|
| `client.pools.list()` | `PoolListResponse` | List all pools |
| `client.pools.get(poolId)` | `PoolDetail` | Get pool details |
| `client.pools.getStats(poolId, { days? })` | `PoolStats` | Get pool APR/volume/TVL. Defaults to a 7-day annualized view. |

### Tokens

| Method | Returns | Description |
|--------|---------|-------------|
| `client.tokens.list()` | `TokenListResponse` | List available tokens |

`client.tokens.list()` returns `{ name, admin }` entries. Duplicate token names are allowed when admins differ.

### Quotes

| Method | Returns | Description |
|--------|---------|-------------|
| `client.quotes.get({ from, to, amount, receiverParty?, poolId?, fromAdmin?, toAdmin? })` | `Quote` | Get swap quote. Pass `poolId` or both admins when token names are ambiguous. `outputAmount` already reflects any traffic-fee deduction. |

Quotes are directional execution estimates, not inverse mid-prices.

- OneSwap prices swaps with the current pool reserves, the configured swap fee, and constant-product slippage.
- Traffic recovery is deducted from the input side before pricing, so `outputAmount` is based on the effective input, not always the raw requested input.
- `CC -> USDCx` and `USDCx -> CC` therefore should not be exact reciprocals, and a round-trip loses value to fee, slippage, and possibly traffic recovery on both legs.
- If `receiverParty` is provided, quote-time traffic estimation follows the current payout path. CC output and utility-token output can use different transfer machinery, so direction can matter even more.

For all-in comparisons, check these fields together:

- `inputAmount`
- `trafficFeeInInput`
- `inputAmountAfterTrafficFee`
- `outputAmount`
- `rate`

`rate` is based on the effective post-traffic input amount, not necessarily the original requested input amount.

### Swaps

| Method | Returns | Description |
|--------|---------|-------------|
| `client.swaps.create(params)` | `SwapIntent` | Create swap intent |
| `client.swaps.getStatus(intentId)` | `SwapStatusResponse` | Get swap status |
| `client.swaps.cancel(intentId)` | `{ success: boolean }` | Cancel a pending swap intent |

`outputAddress` is deprecated on `client.swaps.create(...)` and ignored by the SDK client. Current OneSwap execution always returns output to the same source party that makes the deposit.

`SwapIntent` includes `depositReference`. Treat it as optional metadata: pass it through when your wallet flow supports Canton reason/reference fields, but do not depend on it as the only way a swap will settle.

### Disambiguating Duplicate Symbols

```ts
const tokens = await client.tokens.list()

const usdc = tokens.tokens.find(
  (token) => token.name === 'USDCx' && token.admin === 'admin::issuer-a'
)

const quote = await client.quotes.get({
  from: 'USDCx',
  to: 'Amulet',
  amount: '250',
  fromAdmin: usdc?.admin,
  toAdmin: 'DSO',
})

const intent = await client.swaps.create({
  fromToken: 'USDCx',
  toToken: 'Amulet',
  amount: '250',
  walletAddress: 'alice::12205a8c...',
  poolId: quote.poolId,
  fromAdmin: usdc?.admin,
  toAdmin: 'DSO',
})
```

If you omit those fields for an ambiguous pair, the API returns `409 ambiguous_pool_pair` and the SDK throws `AmbiguousPoolPairError`.

**`SwapIntent`** (with event emitter):
- `.wait(opts?)` — Polls until terminal state. Resolves with `SwapStatusResponse`.
- `.cancel()` — Cancels a pending swap intent.
- `.on(event, handler)` / `.once(event, handler)` — Listen for status changes.
- Events: `pending`, `matched`, `deposit_received`, `forwarded`, `processing`, `sending_output`, `completed`, `slippage_failed`, `price_impact_exceeded`, `insufficient_liquidity`, `insufficient_amount`, `output_failed`, `no_output_holdings`, `failed`, `refund_failed`, `expired`, `cancelled`

### Track

| Method | Returns | Description |
|--------|---------|-------------|
| `client.track(partyId)` | `TrackResponse` | Get swap intents only for the currently authenticated wallet party |

`client.track(...)` does not return liquidity intents or LP positions. LP add and LP withdraw remain website-only and are intentionally absent from the SDK surface.

## Error Handling

All errors extend `OneSwapError` so you can catch them uniformly:

```ts
import {
  OneSwap,
  AuthError,
  ValidationError,
  AmbiguousPoolPairError,
  SlippageError,
  PriceImpactError,
  InsufficientLiquidityError,
  InsufficientAmountError,
  OutputFailedError,
  SwapFailedError,
  RefundFailedError,
  ExpiredError,
  TimeoutError,
  NetworkError,
} from '@oneswap/sdk'

try {
  const intent = await client.swaps.create({ ... })
  const result = await intent.wait()
} catch (err) {
  if (err instanceof SlippageError) {
    console.log('Slippage exceeded, swap refunded')
  } else if (err instanceof PriceImpactError) {
    console.log('Price impact exceeded the server cap')
  } else if (err instanceof InsufficientLiquidityError) {
    console.log('Not enough liquidity in pool')
  } else if (err instanceof InsufficientAmountError) {
    console.log('Amount too low to cover traffic cost')
  } else if (err instanceof OutputFailedError) {
    console.log('Output transfer failed - contact support')
  } else if (err instanceof SwapFailedError) {
    console.log('Swap failed - retry after reviewing the pool state')
  } else if (err instanceof RefundFailedError) {
    console.log('Refund failed - manual intervention is required')
  } else if (err instanceof ExpiredError) {
    console.log('Intent expired before deposit')
  } else if (err instanceof TimeoutError) {
    console.log('Request timed out')
  } else if (err instanceof NetworkError) {
    console.log('Network connectivity issue')
  } else if (err instanceof AuthError) {
    console.log('Invalid API key')
  } else if (err instanceof ValidationError) {
    console.log('Invalid params:', err.message)
  } else if (err instanceof AmbiguousPoolPairError) {
    console.log('Specify poolId or token admins:', err.candidates)
  }
}
```

Quote-time or create-time price-impact rejections still arrive as `ValidationError` because the API responds with HTTP `400`. `PriceImpactError` is reserved for an already-created intent that later reaches `price_impact_exceeded` while `wait()` is polling.

## Wait Options

```ts
const result = await intent.wait({
  pollInterval: 5000,  // Poll every 5s (default: 3s)
  timeout: 600000,     // Timeout after 10min (default: 35min)
})
```

## License

MIT