# Polyester TypeScript SDK

Typed client for the Polyester Exchange public API. Covers markets, orders,
balances, transfers, auth, and realtime streams over ConnectRPC.

Works in browsers, Node, Bun, and edge runtimes. ESM only.

Trading withdrawal payloads require WebCrypto `crypto.getRandomValues` to
generate nonces. Automatically generated order mutation request IDs require
`crypto.randomUUID` or `crypto.getRandomValues`; callers can also provide an
explicit `requestId`. Missing secure randomness raises `ConfigurationError`.

## Install

```bash
npm install @polyester/sdk
```

## Browser

Polyester accounts are smart accounts. You bring an owner wallet, the SDK
derives the smart account address, and the owner EOA signs the login challenge.
Nothing gets deployed just to log in.

```ts
import { PolyesterBrowserClient, POLYESTER_DEVNET_ENVIRONMENT } from "@polyester/sdk";
import type { AccountSigner } from "@polyester/sdk";
import { createPolyesterAccountSigner } from "@polyester/sdk/account-signer";

const environment = POLYESTER_DEVNET_ENVIRONMENT;
const { accountAddress } = createPolyesterAccountSigner({ environment, owner });

const accountSigner: AccountSigner = {
    environmentFingerprint: environment.fingerprint,
    accountAddress,
    ownerAddress: owner.address,
    // Login expects a raw 65-byte EIP-191 signature from the owner EOA.
    signMessage: (message) => owner.signMessage({ message }),
};

const client = new PolyesterBrowserClient({ environment, accountSigner });

await client.auth.login({ provider: "turnkey" });

const { orders } = await client.orders.listOpen();
```

`createPolyesterAccountSigner` on its own returns Safe/ERC-6492 wrapped
signatures. Those are accepted for subaccount creation but rejected by login,
which is why the quickstart only uses it to derive the account address.

## Server

`createPolyesterServerClientFromRequest` reads the session cookies off an
incoming request. Useful for SSR loaders and route handlers.

```ts
import {
    createPolyesterServerClientFromRequest,
    POLYESTER_DEVNET_ENVIRONMENT,
} from "@polyester/sdk";

const client = createPolyesterServerClientFromRequest({
    request,
    environment: POLYESTER_DEVNET_ENVIRONMENT,
});

if (client.hasUsableBearerToken) {
    const me = await client.verifySession();
}
```

Framework cookie stores are accepted too. Await asynchronous helpers before
passing the store. For example, with current Next.js versions:

```ts
import { cookies } from "next/headers";
import {
    createPolyesterServerClientFromCookies,
    POLYESTER_DEVNET_ENVIRONMENT,
} from "@polyester/sdk";

const client = createPolyesterServerClientFromCookies({
    cookies: await cookies(),
    environment: POLYESTER_DEVNET_ENVIRONMENT,
});
```

This factory accepts a `Request`, a `{ [name]: value }` record, or a synchronous
`.get(name)` store that returns either the cookie value or an object containing
the value.

The `polyester_auth_token` bearer cookie is read independently from the
`polyester_session_3` cookie. The latter contains unsigned display data for UI
hydration; it is not proof of authentication. The backend still verifies the
bearer token and authorizes every call.

For machine clients, pass an Ed25519 API key provider instead of a cookie
session:

```ts
import { PolyesterClient, POLYESTER_DEVNET_ENVIRONMENT } from "@polyester/sdk";

const client = new PolyesterClient({
    environment: POLYESTER_DEVNET_ENVIRONMENT,
    auth: {
        kind: "api-key-ed25519",
        getKeyId: () => process.env.POLYESTER_API_KEY_ID ?? null,
        getSecretKey: () => secretKeyBytes,
    },
});
```

## Realtime

Subscriptions start connecting immediately and return their own unsubscribe
function. The return value does not mean the channel is open. Wait for the first
`onOpen` callback to avoid the initial startup gap. This does not guarantee
delivery across a later disconnect; applications that require continuity must
reconcile after connection gaps. `onOpen` can run again after a successful
reconnect, so keep one-time writes outside the callback itself.

```ts
const unsubscribe = await new Promise<() => void>((resolve, reject) => {
    let stop = () => {};
    let pending = true;

    stop = client.orders.subscribe({
        accountId,
        onOpen: () => {
            if (!pending) return;
            pending = false;
            resolve(stop);
        },
        onEvent: (order) => console.log(order.orderId, order.status),
        onError: (ctx) => {
            console.warn(ctx.channel, ctx.error);
            if (!pending) return;
            pending = false;
            stop();
            reject(ctx.error);
        },
    });
});

await client.orders.create(order);
```

Call `unsubscribe()` when the subscription is no longer needed.

## Entry points

| Import                          | Contains                                        |
| ------------------------------- | ----------------------------------------------- |
| `@polyester/sdk`                | clients, environments, errors, types            |
| `@polyester/sdk/errors`         | error classes and codes on their own            |
| `@polyester/sdk/account-signer` | `createPolyesterAccountSigner`                  |
| `@polyester/sdk/smart-account`  | UserOperations and on-chain smart account calls |
| `@polyester/sdk/catalogs`       | symbol catalog and decimal scales               |
| `@polyester/sdk/server-session` | cookie parsing without the client graph         |
| `@polyester/sdk/unstable/gen`   | raw protobuf types and service descriptors      |

`account-signer` and `smart-account` are separate subpaths on purpose. Both pull
in a large viem graph, and keeping them out of the root barrel means the app
shell does not pay for them.

Anything under `unstable/` can change in a patch release.

## Errors

Every failure surfaces as a `PolyesterError` subclass with a stable `code`, so
you can branch without string matching.

```ts
import { StaleQuoteError, RateLimitError } from "@polyester/sdk/errors";

try {
    await client.orders.create(input);
} catch (error) {
    if (error instanceof StaleQuoteError) return refreshQuote();
    if (error instanceof RateLimitError) return backOff(error.retryAfterMs);
    throw error;
}
```

## Versioning

Pre-1.0, so the usual semver shift applies: patch bumps are compatible, minor
bumps can break. Pin with `^0.1.0` to ride `0.1.x` without jumping into `0.2.x`.

## Contributing

Setup, commands, and release process live in
[CONTRIBUTING.md](CONTRIBUTING.md). Security reports go to
[SECURITY.md](SECURITY.md), not the issue tracker.

Maintained by Fabric Labs and updated as the public API evolves.
