npm package: @shenzheyu/extended
Description: TypeScript SDK for the Extended Starknet API.
Latest version: 0.0.1-alpha.0

# Extended TypeScript SDK

用于调用 Extended API 的 TypeScript SDK，覆盖公开 REST、私有 REST、WebSocket 订阅与账户 Onboarding 流程。

## 环境要求

- Node.js >= 18（仅支持 Node 运行时，ESM 包）

## 依赖说明

运行时依赖均在 `package.json` 中固定版本，核心依赖包括：
- `@x10xchange/stark-crypto-wrapper-wasm`：Stark 签名/哈希（WASM 优先）
- `starknet`：JS 回退实现
- `ethers`：L1 签名（Onboarding）
- `zod` v4：Schema 校验
- `ws`：WebSocket 传输

## 请求头与错误处理

- SDK 默认会设置 `User-Agent`（`<包名>/<版本>`），可通过 `userAgent` 覆盖。
- 当 REST 返回 HTTP 200 但响应 `status=ERROR` 时，会抛出 `ApiError`，并附带 `code` 与原始响应数据。

## 兼容性说明

- `getPositions` 在部分环境会省略 `maxPositionSize`、`createdTime`、`updatedTime`，SDK 会将其解析为 `undefined`。

## 安装

如果已发布到 npm：

```bash
npm install extended-sdk
```

从源码使用：

```bash
npm install
npm run build
```

npm 包仅包含 dist 构建产物，源码请查看仓库。

## 开发与测试

单元测试覆盖 onboarding 客户端关键流程。

```bash
npm run test
```

### 集成测试（Onboarding）

集成测试会真实调用 Extended API，默认跳过。请复制 `.env.example` 为 `.env` 并填写：

- `EXTENDED_INTEGRATION_ENABLED=true`
- `EXTENDED_INTEGRATION_L1_PRIVATE_KEY=0x...`
- 可选：`EXTENDED_INTEGRATION_NETWORK=testnet`（或 `mainnet`）
- 可选：`EXTENDED_INTEGRATION_ONBOARDING_BASE_URL=https://...`
- 可选：`EXTENDED_INTEGRATION_SIGNING_DOMAIN=...`
- 可选（有副作用）：`EXTENDED_INTEGRATION_ENABLE_API_KEY=true`
- 可选（有副作用）：`EXTENDED_INTEGRATION_SUBACCOUNT_INDEX=2`

配置完成后执行 `npm run test` 即可运行集成测试。
启用副作用选项会在服务端创建新的 API key 或子账户，请确认测试环境可接受。

### 集成测试（PrivateClient）

私有接口集成测试会真实调用 `getPositions` 并打印结果，默认跳过。请补充以下变量：

- `EXTENDED_INTEGRATION_API_KEY=...`
- `EXTENDED_INTEGRATION_L2_PRIVATE_KEY=0x...`
- 可选：`EXTENDED_INTEGRATION_API_BASE_URL=https://...`

同样通过 `npm run test` 触发；未启用时会跳过该测试。

## 快速开始（PublicClient）

```ts
import { HttpTransport, PublicClient, STARKNET_MAINNET } from "extended-sdk";

const transport = new HttpTransport({
  baseUrl: STARKNET_MAINNET.apiBaseUrl,
  timeoutMs: 10_000,
});

const client = new PublicClient({
  endpoint: STARKNET_MAINNET,
  transport,
});

const markets = await client.info.getMarkets();
const orderbook = await client.info.getOrderbook("ETH-USD");
```

## 公开历史行情

```ts
const trades = await client.info.getMarketTrades("ETH-USD");
const candles = await client.info.getCandlesHistory("ETH-USD", "trades", {
  interval: "PT1M",
  limit: 100,
});

const funding = await client.info.getFundingRatesHistory("ETH-USD", {
  startTime: Date.now() - 7 * 24 * 60 * 60 * 1000,
  endTime: Date.now(),
  limit: 200,
});

const openInterests = await client.info.getOpenInterestHistory("ETH-USD", {
  interval: "P1H",
  startTime: Date.now() - 7 * 24 * 60 * 60 * 1000,
  endTime: Date.now(),
});

if (funding.pagination?.cursor) {
  await client.info.getFundingRatesHistory("ETH-USD", {
    startTime: Date.now() - 7 * 24 * 60 * 60 * 1000,
    endTime: Date.now(),
    cursor: funding.pagination.cursor,
    limit: 200,
  });
}
```

## 私有接口（PrivateClient）

私有接口需要 `apiKey` 与 `signer`。`signer` 用于生成订单相关的签名与密钥信息。

```ts
import {
  HttpTransport,
  PrivateClient,
  STARKNET_MAINNET,
  StarknetSigner,
} from "extended-sdk";

const transport = new HttpTransport({
  baseUrl: STARKNET_MAINNET.apiBaseUrl,
});

const signer = new StarknetSigner({
  privateKey: process.env.EXTENDED_L2_PRIVATE_KEY ?? "",
});

const client = new PrivateClient({
  endpoint: STARKNET_MAINNET,
  transport,
  apiKey: process.env.EXTENDED_API_KEY,
  signer,
});

const account = await client.account.getAccount();
const fees = await client.account.getFees({ market: "ETH-USD" });
```

下单时请按照 `OrderRequest` 构造参数并提供结算签名，字段含义请参考 `Extended-API.md`。

## 常用示例

### Onboarding（账户注册）

L1 签名使用 `ethers` v6：

```ts
import { Wallet } from "ethers";
import { HttpTransport, OnboardingClient, STARKNET_TESTNET } from "extended-sdk";

const transport = new HttpTransport({
  baseUrl: STARKNET_TESTNET.onboardingBaseUrl,
});

const wallet = new Wallet(process.env.EXTENDED_L1_PRIVATE_KEY ?? "");

const onboarding = new OnboardingClient({
  endpoint: STARKNET_TESTNET,
  transport,
  l1Signer: {
    getAddress: () => wallet.getAddress(),
    signMessage: (message) => wallet.signMessage(message),
    signTypedData: (domain, types, value) => wallet.signTypedData(domain, types, value),
  },
});

const onboarded = await onboarding.onboard();
const accountId = onboarded.account.accountId?.toString();
if (!accountId) {
  throw new Error("accountId missing");
}

const apiKey = await onboarding.createAccountApiKey(accountId, "trading api key");
```

### 私有接口初始化（下单/转账/提现通用）

```ts
import {
  HttpTransport,
  PrivateClient,
  STARKNET_TESTNET,
  StarknetSigner,
} from "extended-sdk";

const transport = new HttpTransport({
  baseUrl: STARKNET_TESTNET.apiBaseUrl,
});

const signer = new StarknetSigner({
  privateKey: process.env.EXTENDED_L2_PRIVATE_KEY ?? "",
});

const client = new PrivateClient({
  endpoint: STARKNET_TESTNET,
  transport,
  apiKey: process.env.EXTENDED_API_KEY,
  signer,
});
```

### 账户数据查询

```ts
const account = await client.account.getAccount();
const balance = await client.account.getBalance();
const positions = await client.account.getPositions({ market: ["ETH-USD"], side: "LONG" });

const { data: trades, pagination } = await client.account.getTrades({
  market: ["ETH-USD"],
  limit: 50,
});

if (pagination?.cursor) {
  const nextPage = await client.account.getTrades({
    market: ["ETH-USD"],
    cursor: pagination.cursor,
    limit: 50,
  });
  console.log(nextPage.data.length);
}
```

### 撤单/批量撤单/Dead Man's Switch

```ts
// 单笔撤单（按订单 ID）
await client.orders.cancelOrder("1784963886257016832");

// 单笔撤单（按 externalId）
await client.orders.cancelOrderByExternalId("ext-1");

// 批量撤单（可按 id / 市场 / 全部）
await client.orders.massCancel({
  orderIds: ["1784963886257016832", "1784963886257016833"],
});

// 设置 dead man's switch（秒），0 表示取消定时
await client.orders.setDeadmanswitch(60);
```

### 下单

```ts
import { createOrderRequest } from "extended-sdk";

const [market] = await client.info.getMarkets({ market: ["BTC-USD"] });
if (!market) {
  throw new Error("market not found");
}

const [fees] = await client.account.getFees({ market: "BTC-USD" });
if (!fees) {
  throw new Error("fees not found");
}

const account = await client.account.getAccount();

const orderRequest = await createOrderRequest({
  market,
  fees,
  signer,
  vaultId: account.l2Vault,
  side: "SELL",
  qty: "0.001",
  price: "43445.1168",
  type: "LIMIT",
  timeInForce: "GTT",
  selfTradeProtectionLevel: "ACCOUNT",
  starknetDomain: STARKNET_TESTNET.starknetDomain,
});

const placed = await client.orders.placeOrder(orderRequest);
```

### 转账（子账户间）

```ts
import { createTransferRequest } from "extended-sdk";

// onboarding 实例见上方 Onboarding 示例
const onboardingAccounts = await onboarding.getAccounts();
const fromAccount = onboardingAccounts[0]?.account;
const toAccount = onboardingAccounts[1]?.account;
if (!fromAccount || !toAccount) {
  throw new Error("accounts not found");
}

const transferRequest = await createTransferRequest({
  signer,
  endpoint: STARKNET_TESTNET,
  fromAccount,
  toAccount,
  amount: "1.1",
});

const transferResult = await client.transfers.transfer(transferRequest);
```

### 提现（Starknet）

```ts
import { createWithdrawalRequest } from "extended-sdk";

// onboarding 实例见上方 Onboarding 示例
const onboardingAccounts = await onboarding.getAccounts();
const account = onboardingAccounts[0]?.account;
if (!account) {
  throw new Error("account not found");
}

const withdrawalRequest = await createWithdrawalRequest({
  signer,
  endpoint: STARKNET_TESTNET,
  account,
  amount: "2",
  chainId: "STRK",
  asset: "USD",
  recipient: account.bridgeStarknetAddress ?? "0x0123", // 替换为实际 Starknet 地址
});

const withdrawalResult = await client.withdrawals.withdraw(withdrawalRequest);
```

## WebSocket 订阅（StreamClient）

```ts
import { StreamClient, STARKNET_MAINNET, WebSocketTransport } from "extended-sdk";

const transport = new WebSocketTransport({
  baseUrl: STARKNET_MAINNET.streamBaseUrl,
  reconnect: {
    enabled: true,
    maxAttempts: 10,
  },
  keepAlive: {
    intervalMs: 30_000,
    pongTimeoutMs: 10_000,
  },
});

const stream = new StreamClient({
  endpoint: STARKNET_MAINNET,
  transport,
  apiKey: process.env.EXTENDED_API_KEY,
});

const subscription = await stream.stream.publicTrades("ETH-USD", (message) => {
  console.log(message);
});

process.on("SIGINT", async () => {
  await subscription.unsubscribe();
});
```

如需订阅账户相关推送（`accountUpdates`），必须提供 `apiKey`。

### Orderbook Stream + 增量合并器

订单簿流是快照 + 增量（delta）模式，如果收到 `seq` 乱序请重连。

```ts
import {
  StreamClient,
  STARKNET_MAINNET,
  WebSocketTransport,
  OrderbookTracker,
} from "extended-sdk";

const transport = new WebSocketTransport({
  baseUrl: STARKNET_MAINNET.streamBaseUrl,
});

const stream = new StreamClient({
  endpoint: STARKNET_MAINNET,
  transport,
});

const tracker = new OrderbookTracker();
const subscription = await stream.stream.orderbooks("ETH-USD", (message) => {
  tracker.apply(message);
  const snapshot = tracker.getSnapshot("ETH-USD");
  if (snapshot) {
    console.log(snapshot.bids[0], snapshot.asks[0]);
  }
});

process.on("SIGINT", async () => {
  await subscription.unsubscribe();
});
```

## 端点配置

内置 `STARKNET_MAINNET` 与 `STARKNET_TESTNET`，也可以自行覆盖。`apiBaseUrl` 与 `streamBaseUrl` 仅填写协议+域名，SDK 会在请求时拼接 `/api/v1` 与 `/stream.extended.exchange/v1` 前缀：

```ts
import type { EndpointConfig } from "extended-sdk";

const customEndpoint: EndpointConfig = {
  apiBaseUrl: "https://example",
  streamBaseUrl: "wss://example",
  onboardingBaseUrl: "https://example",
  chainRpcUrl: "https://example/rpc",
  signingDomain: "example",
  collateralAssetId: "0x1",
  collateralDecimals: 6,
};
```

## 相关文档

- Extended API 说明：`Extended-API.md`
- 类型与 Schema：SDK 导出 `schemas`，用于直接复用响应与请求结构。