<div align="center">

# 🧩 telegram-fragment-api

**Unofficial, fully-typed TypeScript client for [Fragment.com](https://fragment.com)**

Buy Telegram Stars, check Premium prices, resolve usernames and move TON — class-based, `Result`-safe, no exceptions.

[![npm version](https://img.shields.io/npm/v/telegram-fragment-api?color=cb3837&logo=npm)](https://www.npmjs.com/package/telegram-fragment-api)
[![npm downloads](https://img.shields.io/npm/dm/telegram-fragment-api?color=cb3837&logo=npm)](https://www.npmjs.com/package/telegram-fragment-api)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Tests](https://img.shields.io/badge/tests-vitest-6E9F18?logo=vitest&logoColor=white)](https://vitest.dev/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
[![GitHub stars](https://img.shields.io/github/stars/dreminDev/telegram-fragment-api?style=social)](https://github.com/dreminDev/telegram-fragment-api)

**English** · [Русский](./README.ru.md)

</div>

---

> ### ⭐ Enjoying this library?
> **Please [star the repository on GitHub](https://github.com/dreminDev/telegram-fragment-api)** — it takes one click, costs you nothing, and genuinely helps the project grow and get maintained. Thank you! 🙏

---

## ✨ Features

- 🏛️ **Class-based, namespaced API** — `client.stars.getPrice({ quantity })`, `client.ton.wallet.v4r2.send({ … })`
- 🧯 **No exceptions** — every method returns a `Result` (`{ ok: true, data } | { ok: false, error }`)
- 🧩 **Typed errors** — structured `FragmentError` with a `code` (`VALIDATION`, `AUTH`, `NETWORK`, `API`, …)
- 🔭 **Username lookup**, ⭐ **Stars** pricing + purchase flow, 👑 **Premium** prices
- 💎 **TON blockchain** — liteservers, wallet balances, and **v4r2 transfers**
- 🔑 **Auto hash** — scrape the per-session api hash with `client.auth.fetchHash()`
- 🧾 **Payload decoder**, 👤 **profile & sessions**
- 🟦 **100% TypeScript** + 🧪 **fully mock-tested** (Vitest), ESM **and** CommonJS, powered by **axios**

## 📦 Installation

```bash
# npm
npm install telegram-fragment-api

# yarn
yarn add telegram-fragment-api

# pnpm
pnpm add telegram-fragment-api

# bun
bun add telegram-fragment-api
```

> Works on **Node.js ≥ 18**, **Bun**, and any runtime with a global `fetch`.
> Ships dual **ESM + CommonJS** builds, so `import` and `require` both work.

## 🚀 Quick start

```ts
import { Fragment } from "telegram-fragment-api";

// Create a client with your credentials (see "Getting credentials" below)
const client = new Fragment({
  hash: "your_hash_here",
  stelSsid: "your_stel_ssid",
  stelToken: "your_stel_token",
  stelTonToken: "your_stel_ton_token",
  toncenterApiKey: "your_toncenter_api_key",
  walletSeed: "word1 word2 ... word24",
});

const res = await client.stars.getPrice({ quantity: 5050 });
if (res.ok) {
  console.log(res.data.curPrice); // { TON: "...", USDT: "..." }
} else {
  console.error(res.error.code, res.error.message);
}
```

> **CommonJS?** `const { Fragment } = require("telegram-fragment-api");` works too — dual ESM/CJS builds.

## 🧯 Error handling — the `Result` pattern

No method ever throws. Each returns a discriminated union you narrow with `res.ok`:

```ts
const res = await client.users.nickToHash({ nickname: "durov" });

if (res.ok) {
  // res.data is fully typed here
  console.log(res.data.found?.recipient);
} else {
  // res.error is a FragmentError
  switch (res.error.code) {
    case "AUTH":      /* bad cookies / hash */ break;
    case "NOT_FOUND": /* username has no recipient */ break;
    case "NETWORK":   /* timeout / connection */ break;
    default:          console.error(res.error.message);
  }
}
```

Error codes: `VALIDATION` · `AUTH` · `NETWORK` · `API` · `PARSE` · `NOT_FOUND` · `INSUFFICIENT_FUNDS` · `UNKNOWN`.

## 🔑 Getting credentials

| Field | Where to get it |
|---|---|
| `hash` | The `hash` query param on any `fragment.com/api` XHR request — or call `client.auth.fetchHash()` |
| `stelSsid`, `stelToken`, `stelTonToken` | Cookies on `fragment.com` (DevTools → Application → Cookies) |
| `stelDt` | Cookie `stel_dt` (timezone offset, usually `"-180"`) |
| `toncenterApiKey` | Free key from [@tonapibot](https://t.me/tonapibot) / [toncenter.com](https://toncenter.com) |
| `tonconsoleApiKey` | Alternative balance source — [tonconsole.com](https://tonconsole.com) (tonapi.io) |
| `walletSeed` | Your wallet's 24-word mnemonic — **required only for sending TON** |

> 🔒 **Security:** treat these like passwords. Load them from environment variables, never hard-code or commit them. Anyone with your `walletSeed` can drain your wallet.

You can also update credentials after construction:

```ts
client.configure({ hash: "new_hash" });
await client.auth.fetchHash(); // scrapes & stores the hash automatically
```

## 📚 API reference

Every network call returns a `Promise<Result<T>>`.

### `client.users` / `client.utils`

```ts
await client.users.nickToHash({ nickname: "durov" });        // → recipient hash
client.utils.decodePayload({ payload: "te6ccg..." });        // → { decoded } (sync)
client.utils.fromNano("456100000");                          // → "0.4561"  (nanoTON → TON)
client.utils.toNano("0.4561");                               // → 456100000n (TON → nanoTON)
```

### `client.stars`

```ts
await client.stars.getPrice({ quantity: 5050 });                            // → { curPrice: { TON, USDT } }
await client.stars.purchase({ recipient, quantity: 50 });                   // → end-to-end: init → getBuyStarsLink(account) → send → wait on-chain
await client.stars.initPayment({ recipient, quantity: 50 });                // → { req_id, amount }  (payment_method: "ton" by default)
await client.stars.getPaymentInfo({ requestId, account });                  // → { transaction, … }  (pass `account` — binds the order to your wallet)
await client.ton.wallet.waitForConfirmation({ boc, address });              // ← wait for the TON transfer to confirm on-chain
```

> ⚠️ **How Stars get credited.** Fragment binds the order to the wallet
> `account` you send on `getBuyStarsLink`, then **credits the on-chain payment
> that arrives from that wallet** (matched to the order). So the paying wallet
> **must be the v4r2 wallet connected to your Fragment session** (derived from
> `walletSeed`) — pay from a different wallet and the TON debits but no Stars
> arrive. The high-level `client.stars.purchase({ recipient, quantity })` does
> the whole flow (including waiting for on-chain confirmation) for you.
>
> 💡 **Buying Stars requirements.** `initPayment` sends `payment_method` (default
> `"ton"`) — Fragment **requires** it; without it you get `Access denied`. The
> Fragment account must also have a **TON wallet connected** (otherwise
> `initPayment` returns an `AUTH` error with `need_ton`).

### `client.premium`

```ts
const res = await client.premium.getPrice({ months: 12 });
if (res.ok) console.log(res.data.tonRate, res.data.options);
```

### `client.ton`

```ts
await client.ton.getRandomLiteServer();                          // → { ip_readable, port }
await client.ton.wallet.getAddress();                            // → { friendly, raw }  (from walletSeed, no network)
await client.ton.wallet.getAccount();                            // → { address, publicKey, chain, walletStateInit }  (TonConnect JSON, no network)
await client.ton.wallet.getBalance({ address: "UQ..." });        // → { nano, ton, source }

// pass a human amount in TON …
await client.ton.wallet.v4r2.send({ destinationAddress: "UQ...", amount: 0.21 });

// … or an exact Fragment payment — pass amount + payload straight from getPaymentInfo:
await client.ton.wallet.v4r2.send({
  destinationAddress: msg.address,
  amountNano: msg.amount,                // exact nanoTON, no rounding
  payloadCell: msg.payload,              // exact BoC cell (byte-matches the website)
});                                       // → { sender, amount, amountNano, boc, balanceBefore }
```

> 💡 For Stars/Fragment payments use **`amountNano`** + **`payloadCell`** (both straight
> from `getPaymentInfo`). `amountNano` avoids the `/1e9` float footgun; `payloadCell`
> sends Fragment's **exact** payload cell — a re-encoded text comment (`payload`) may not
> byte-match. The `boc` field in the result is the broadcast external-message BoC —
> pass it verbatim to `stars.confirmPayment(...)` (see below) so Fragment credits the Stars.

### `client.account`

```ts
await client.account.getProfile();   // → { username, verified, wallet, ... }
await client.account.getSessions();  // → { account, sessions: [...] }
```

### `client.auth`

```ts
await client.auth.fetchHash();                       // scrape hash from fragment.com/
await client.auth.fetchHash({ url: ".../stars" });   // from a specific page
```

## 🔗 Full workflow: buy Stars end-to-end

### One-shot (recommended)

```ts
import { Fragment } from "telegram-fragment-api";

const client = new Fragment({
  hash: process.env.FRAGMENT_HASH!,
  stelSsid: process.env.FRAGMENT_STEL_SSID!,
  stelToken: process.env.FRAGMENT_STEL_TOKEN!,
  stelTonToken: process.env.FRAGMENT_STEL_TON_TOKEN!,
  toncenterApiKey: process.env.TONCENTER_API_KEY!,
  walletSeed: process.env.WALLET_SEED!,
});

const user = await client.users.nickToHash({ nickname: "maksim_dremin" });
if (!user.ok) throw new Error(user.error.message);

const res = await client.stars.purchase({
  recipient: user.data.found!.recipient,
  quantity: 50,
});
console.log(res.ok ? `Sent ✅ req=${res.data.reqId}` : res.error.message);
```

`purchase()` runs all four real steps: `initBuyStarsRequest` → `getBuyStarsLink`
→ `wallet.v4r2.send` → `confirm_method` POST. **The last step is the one most
implementations get wrong** — skip it and the TON debits but Stars never arrive.

### Manual, step-by-step

Use this when you need each intermediate result (e.g. to log the on-chain hash, ask
the user to confirm, or retry only the confirm leg):

```ts
const init = await client.stars.initPayment({ recipient, quantity: 50 });
if (!init.ok) throw new Error(init.error.message);

// Fetch the paying wallet and pass its `account` to getPaymentInfo — Fragment
// binds the order to it, then credits the on-chain payment that arrives from
// it. The wallet must be the one connected to your Fragment session.
const account = await client.ton.wallet.getAccount();
if (!account.ok) throw new Error(account.error.message);

const info = await client.stars.getPaymentInfo({
  requestId: init.data.req_id,
  account: account.data,
});
if (!info.ok) throw new Error(info.error.message);

const msg = info.data.transaction!.messages[0]!;

const tx = await client.ton.wallet.v4r2.send({
  destinationAddress: msg.address,
  amountNano: msg.amount,        // exact nanoTON — no conversion
  payloadCell: msg.payload,      // exact BoC cell — byte-matches the website
});
if (!tx.ok) throw new Error(tx.error.message);

// Wait for the TON transfer to confirm on-chain — Fragment credits the Stars
// off the confirmed payment. No confirm POST, no TON Connect proof needed.
const confirmed = await client.ton.wallet.waitForConfirmation({
  boc: tx.data.boc,
  address: tx.data.sender,
});
console.log(confirmed.ok ? `Stars credited ✅ tx ${confirmed.data.txHash}` : confirmed.error.message);
```

> `initPayment` syncs the (volatile) TON price internally and retries on
> `Price was changed`, so you don't have to. The Fragment account must have a
> connected TON wallet, and you need a real `toncenterApiKey` + `walletSeed` to sign.

## 🧰 Multiple accounts

Each `Fragment` instance is fully isolated — just create more:

```ts
const a = new Fragment({ hash: "..." });
const b = new Fragment({ hash: "..." });
```

## 🧪 Testing

The whole library is covered with mock tests (Vitest + `axios-mock-adapter`, with `@ton/ton` mocked):

```bash
npm test            # run the suite
npm run test:coverage
```

## 🎯 Design principles

- **Class-based** — `new Fragment({ … })`, one fully isolated client per account.
- **Object params** — `getPrice({ quantity })` reads clearly and stays easy to extend.
- **`Result` instead of exceptions** — check `res.ok` instead of `try/catch`.
- **`async` + camelCase** throughout, strict types everywhere.

## ⚠️ Disclaimer

This is an **unofficial** library, not affiliated with or endorsed by Telegram or Fragment. It works against undocumented endpoints that can change at any time. Use it responsibly, at your own risk, and in compliance with Fragment's terms of service. You are solely responsible for your credentials and funds.

## 🤝 Contributing

Issues and pull requests are welcome! [Open an issue](https://github.com/dreminDev/telegram-fragment-api/issues).

## ⭐ Support the project

If this saved you time, the best thank-you is a **[star on GitHub](https://github.com/dreminDev/telegram-fragment-api)** ⭐ — it helps others discover the library and keeps development going.

## 📄 License

[MIT](./LICENSE) © Maksim Dremin
